Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/ethereum-contracts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Added

- `Only712MacroForwarder`: a new macro forwarder that executes EIP-712-signed meta-transactions with properties giving it additional security guarantees.
- `SuperToken`: the contract admin can enable/disable a _Yield Backend_ in order to generate a yield on the underlying asset.
- `SuperToken`: added `VERSION()` which returns the version string of the logic contract set for the SuperToken, and inline CHANGELOG.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ interface IUserDefinedMacro {
/*
* Additional to the required interface, we recommend to implement one or multiple view functions
* which take operation specific typed arguments and return the abi encoded bytes.
* As a convention, the name of those functions shall start with `params`.
* As a convention, the name of those functions shall start with `encode`.
*
* Implementing this view function(s) has several advantages:
* - Allows to build more complex macros with internally encapsulated dispatching logic
Expand All @@ -44,3 +44,20 @@ interface IUserDefinedMacro {
* You can consult the related test code in `MacroForwarderTest.t.sol` for examples.
*/
}

// Interface for a macro used with the Only712MacroForwarder.
// Metaphor: a macro is like an api, an action is like an endpoint.
// Each action can have its own type definition (list of arguments).
interface IUserDefined712Macro is IUserDefinedMacro {
// Primary type name (required by the EIP712 type definition), usually rendered prominently by wallets.
// From a users perspective, it should concisely name the action/intent to be signed.
function getPrimaryTypeName(bytes memory params) external view returns (string memory);

// The EIP-712 type definition of the action, required by Only712MacroForwarder.
// Note that the name of this type must be "Action", only its content is free to choose.
function getActionTypeDefinition(bytes memory params) external view returns (string memory);

// The struct hash of the action, required by Only712MacroForwarder.
// This hash must be constructed based on the type definition and the data, according to the EIP-712 standard.
function getActionStructHash(bytes memory params) external view returns (bytes32);
}
13 changes: 9 additions & 4 deletions packages/ethereum-contracts/contracts/utils/ForwarderBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,26 @@ abstract contract ForwarderBase {
return _forwardBatchCall(ops);
}

// helper defaulting to msg.sender as the sender and 0 value to forward
function _forwardBatchCall(ISuperfluid.Operation[] memory ops) internal returns (bool) {
return _forwardBatchCallWithValue(ops, 0);
return _forwardBatchCallWithSenderAndValue(ops, msg.sender, 0);
}

function _forwardBatchCallWithValue(ISuperfluid.Operation[] memory ops, uint256 valueToForward)
function _forwardBatchCallWithSenderAndValue(
ISuperfluid.Operation[] memory ops,
address msgSender,
uint256 valueToForward
)
internal
returns (bool)
{
bytes memory fwBatchCallData = abi.encodeCall(_host.forwardBatchCall, (ops));

// https://eips.ethereum.org/EIPS/eip-2771
// we encode the msg.sender as the last 20 bytes per EIP-2771 to extract the original txn signer later on
// we encode the msgSender as the last 20 bytes per EIP-2771 to extract the original txn signer later on
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returnedData) = address(_host)
.call{value: valueToForward}(abi.encodePacked(fwBatchCallData, msg.sender));
.call{value: valueToForward}(abi.encodePacked(fwBatchCallData, msgSender));

if (!success) {
CallUtils.revertFromReturnedData(returnedData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ contract MacroForwarder is ForwarderBase {
function runMacro(IUserDefinedMacro m, bytes calldata params) external payable returns (bool)
{
ISuperfluid.Operation[] memory operations = buildBatchOperations(m, params);
bool retVal = _forwardBatchCallWithValue(operations, msg.value);
bool retVal = _forwardBatchCallWithSenderAndValue(operations, msg.sender, msg.value);
m.postCheck(_host, params, msg.sender);
return retVal;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// SPDX-License-Identifier: AGPLv3
pragma solidity ^0.8.23;

import { EIP712 } from "@openzeppelin-v5/contracts/utils/cryptography/EIP712.sol";
import { SignatureChecker } from "@openzeppelin-v5/contracts/utils/cryptography/SignatureChecker.sol";
import { IAccessControl } from "@openzeppelin-v5/contracts/access/IAccessControl.sol";
import { IUserDefined712Macro } from "../interfaces/utils/IUserDefinedMacro.sol";
import { ISuperfluid } from "../interfaces/superfluid/ISuperfluid.sol";
import { ForwarderBase } from "./ForwarderBase.sol";


/**
* Nonce management functionality following the semantics of ERC-4337.
* Each nonce consists of a 192-bit key and a 64-bit sequence number.
* This allows senders to both have a practically unlimited number of parallel operations
* (meaning signed pending transactions can't block each other), and also the option to enforce
* sequential execution according to the sequence number.
*/
abstract contract NonceManager {
/// nonce already used or out of sequence
error InvalidNonce(address sender, uint256 nonce);

/// data structure keeping track of the next sequence number by sender and key
mapping(address => mapping(uint192 => uint256)) internal _nonceSequenceNumber;

/// Returns the next nonce for a given sender and key
function getNonce(address sender, uint192 key) public virtual view returns (uint256 nonce) {
return _nonceSequenceNumber[sender][key] | (uint256(key) << 64);
}

/// validates the nonce and updates the data structure for correct sequencing
function _validateAndUpdateNonce(address sender, uint256 nonce) internal virtual {
uint192 key = uint192(nonce >> 64);
uint64 seq = uint64(nonce);
if (_nonceSequenceNumber[sender][key]++ != seq) {
revert InvalidNonce(sender, nonce);
}
}
}

/**
* @dev EIP-712-aware macro forwarder (clear signing).
* In this minimal iteration: decodes payload as appParams and passes through to the macro.
* Envelope verification, nonce, and registry checks to be added in follow-up.
*
* TODO:
* -[X] use SimpleACL for provider authorization
* -[X] add nonce verification
* -[X] add timeframe (validAfter, validBefore) validation
* -[] add missing fields
* -[] extract interface definition
* -[] review naming
*/
contract Only712MacroForwarder is ForwarderBase, EIP712, NonceManager {

// STRUCTS, CONSTANTS, IMMUTABLES

// top-level data structure
struct PrimaryType {
ActionType action;
SecurityType security;
}
struct ActionType {
bytes actionParams;
}
// the action typehash is macro specific
struct SecurityType {
string domain;
string provider;
uint256 validAfter;
uint256 validBefore;
uint256 nonce;
}
bytes internal constant _TYPEDEF_SECURITY =
"Security(string domain,string provider,uint256 validAfter,uint256 validBefore,uint256 nonce)";

bytes32 internal constant _TYPEHASH_SECURITY = keccak256(_TYPEDEF_SECURITY);

IAccessControl internal immutable _providerACL;

// ERRORS

error InvalidPayload(string message);
error OutsideValidityWindow(uint256 blockTimestamp, uint256 validBefore, uint256 validAfter);
error ProviderNotAuthorized(string provider, address msgSender);
error InvalidSignature();

// INITIALIZATION

// Here EIP712 domain name and version are set.
// TODO: should the name include "Superfluid"?
constructor(ISuperfluid host) ForwarderBase(host) EIP712("ClearSigning", "1") {
_providerACL = IAccessControl(host.getSimpleACL());
}

// PUBLIC FUNCTIONS

/**
* @dev Run the macro with encoded payload (generic + macro specific fragments).
* @param m Target macro.
* @param params Encoded payload
* @param signer The signer of the payload
* @param signature The signature of the payload
* @return bool True if the macro was executed successfully
*/
function runMacro(IUserDefined712Macro m, bytes calldata params, address signer, bytes calldata signature)
external payable
returns (bool)
{
// decode the payload
PrimaryType memory payload = abi.decode(params, (PrimaryType));
bytes32 providerRole = keccak256(bytes(payload.security.provider));
if (!_providerACL.hasRole(providerRole, msg.sender)) {
revert ProviderNotAuthorized(payload.security.provider, msg.sender);
}

_validateAndUpdateNonce(signer, payload.security.nonce);

if (block.timestamp < payload.security.validAfter) {
revert OutsideValidityWindow(block.timestamp, payload.security.validBefore, payload.security.validAfter);
}
if (payload.security.validBefore != 0 && block.timestamp > payload.security.validBefore) {
revert OutsideValidityWindow(block.timestamp, payload.security.validBefore, payload.security.validAfter);
}

bytes32 digest = _getDigest(m, params);

// verify the signature - this also works for ERC1271 (contract signatures)
if (!SignatureChecker.isValidSignatureNow(signer, digest, signature)) {
revert InvalidSignature();
}

// get the operations array from the user macro based on the action params
ISuperfluid.Operation[] memory operations =
m.buildBatchOperations(_host, payload.action.actionParams, signer);

// forward the operations
bool retVal = _forwardBatchCallWithSenderAndValue(operations, signer, msg.value);
m.postCheck(_host, payload.action.actionParams, signer);
return retVal;
}

/**
* @dev Encode action and security params into the payload bytes expected by runMacro.
* @param actionParams params specific to the macro action, already ABI-encoded by the caller.
* @param security security related parameters
* @return Encoded payload to pass to runMacro()
*/
function encodeParams(bytes calldata actionParams, SecurityType calldata security)
external pure
returns (bytes memory)
{
PrimaryType memory payload = PrimaryType({
action: ActionType({ actionParams: actionParams }),
security: security
});
return abi.encode(payload);
}

// TODO: should this exist?
function getTypeDefinition(IUserDefined712Macro m, bytes calldata params) external view returns (string memory) {
return _getTypeDefinition(m, params);
}

// TODO: should this exist?
function getTypeHash(IUserDefined712Macro m, bytes calldata params) public view returns (bytes32) {
return keccak256(abi.encodePacked(_getTypeDefinition(m, params)));
}

// TODO: should this exist?
function getStructHash(IUserDefined712Macro m, bytes calldata params) external view returns (bytes32) {
return _getStructHash(m, params);
}

function getDigest(IUserDefined712Macro m, bytes calldata params) external view returns (bytes32) {
return _getDigest(m, params);
}

// INTERNAL FUNCTIONS

function _getTypeDefinition(IUserDefined712Macro m, bytes calldata params) internal view returns (string memory) {
return string(abi.encodePacked(
m.getPrimaryTypeName(params),
"(Action action,Security security)",
// nested components need to be in alphabetical order
m.getActionTypeDefinition(params),
_TYPEDEF_SECURITY
));
}

function _getStructHash(IUserDefined712Macro m, bytes calldata params) internal view returns (bytes32) {
PrimaryType memory payload = abi.decode(params, (PrimaryType));
// the action fragment is handled by the user macro.
bytes32 actionStructHash = m.getActionStructHash(payload.action.actionParams);

bytes32 securityStructHash = _getSecurityStructHash(payload.security);

// get the typehash
bytes32 primaryTypeHash = getTypeHash(m, params);

// calculate the struct hash
bytes32 structHash = keccak256(
abi.encode(
primaryTypeHash,
actionStructHash,
securityStructHash
)
);
return structHash;
}

function _getDigest(IUserDefined712Macro m, bytes calldata params) internal view returns (bytes32) {
bytes32 structHash = _getStructHash(m, params);
return _hashTypedDataV4(structHash);
}

function _getSecurityStructHash(SecurityType memory security) internal pure returns (bytes32) {
return keccak256(abi.encode(
_TYPEHASH_SECURITY,
keccak256(bytes(security.domain)),
keccak256(bytes(security.provider)),
security.validAfter,
security.validBefore,
security.nonce
));
}
}
15 changes: 15 additions & 0 deletions packages/ethereum-contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,18 @@ verbosity = 2

[profile.ci.fuzz]
runs = 1000

[lint]
exclude_lints = [
"asm-keccak256",
"erc20-unchecked-transfer",
"mixed-case-variable",
"unused-import",
"mixed-case-function",
"divide-before-multiply",
"unaliased-plain-import",
"screaming-snake-case-const",
"screaming-snake-case-immutable",
"pascal-case-struct",
"incorrect-shift",
]
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const SuperfluidLoader = artifacts.require("SuperfluidLoader");
const CFAv1Forwarder = artifacts.require("CFAv1Forwarder");
const GDAv1Forwarder = artifacts.require("GDAv1Forwarder");
const MacroForwarder = artifacts.require("MacroForwarder");
const Only712MacroForwarder = artifacts.require("Only712MacroForwarder");

/**
* @dev Deploy specified contract at a deterministic address (defined by sender, nonce)
Expand Down Expand Up @@ -89,6 +90,12 @@ module.exports = eval(`(${S.toString()})()`)(async function (
console.log(
`setting up MacroForwarder for chainId ${chainId}, host ${hostAddr}`
);
} else if (contractName === "Only712MacroForwarder") {
ContractArtifact = Only712MacroForwarder;
deployArgs = [hostAddr];
console.log(
`setting up Only712MacroForwarder for chainId ${chainId}, host ${hostAddr}`
);
} else {
throw new Error("Contract unknown / not supported");
}
Expand Down
2 changes: 1 addition & 1 deletion packages/ethereum-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"test-coverage:foundry": "yarn run-foundry coverage --gas-limit 999999999999 --report lcov",
"test-slither": "tasks/test-slither.sh",
"lint": "run-s lint:*",
"lint:sol": "solhint -w 0 `find contracts -name *.sol` && echo '✔ Your .sol files look good.'",
"lint:sol": "solhint --disc -w 0 `find contracts -name *.sol` && echo '✔ Your .sol files look good.'",
"lint-js": "eslint test -c .eslintrc.json --ext .js",
"lint:js-eslint": "yarn lint-js --max-warnings=0 --report-unused-disable-directives && echo '✔ Your .js files look good.'",
"lint-ts": "eslint test -c .eslintrc.ts.json --ext .ts",
Expand Down
Loading
Loading