Skip to content

Commit 53f953f

Browse files
committed
more testing
1 parent 3f3e74d commit 53f953f

File tree

3 files changed

+223
-45
lines changed

3 files changed

+223
-45
lines changed

packages/ethereum-contracts/contracts/interfaces/utils/IUserDefinedMacro.sol

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,19 @@ interface IUserDefinedMacro {
4545
*/
4646
}
4747

48+
// Interface for a macro used with the Only712MacroForwarder.
49+
// Metaphor: a macro is like an api, an action is like an endpoint.
50+
// Each action can have its own type definition (list of arguments).
51+
// TODO: for multi-action macros, the getters probably all need to get the encoded message as an argument
4852
interface IUserDefined712Macro is IUserDefinedMacro {
49-
// TODO: this probably needs to be a function of the message, for the dispatching pattern
50-
// the metaphor being: a macro is like an api, an action is like an endpoint (defining the set of arguments)
51-
function getMessageTypeHash() external view returns (bytes32);
52-
// TODO: should it take the known and required fields already decoded instead?
53+
// Primary type name (required by the EIP712 type definition), usually rendered prominently by wallets.
54+
// From a users perspective, it should concisely name the action/intent to be signed.
55+
function getPrimaryTypeName() external view returns (string memory);
56+
57+
// The EIP-712 type definition of the action, required by Only712MacroForwarder.
58+
function getMessageTypeDefinition() external view returns (string memory);
59+
60+
// The struct hash of the action, required by Only712MacroForwarder.
61+
// This hash must be constructed based on the type definition and the data, according to the EIP-712 standard.
5362
function getMessageStructHash(bytes memory message) external view returns (bytes32);
5463
}

packages/ethereum-contracts/contracts/utils/Only712MacroForwarder.sol

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ForwarderBase } from "./ForwarderBase.sol";
1515
contract Only712MacroForwarder is ForwarderBase, EIP712 {
1616

1717
// top-level data structure
18+
// TODO: is "payload" a good name? Does EIP-712 give a good hint for naming this? Something "primary"?
1819
struct Payload {
1920
PayloadMeta meta;
2021
PayloadMessage message;
@@ -26,7 +27,8 @@ contract Only712MacroForwarder is ForwarderBase, EIP712 {
2627
//string language;
2728
//string disclaimer;
2829
}
29-
bytes32 internal constant _TYPEHASH_META = keccak256("Meta(string domain,string version)");
30+
bytes internal constant _TYPEDEF_META = "Meta(string domain,string version)";
31+
bytes32 internal constant _TYPEHASH_META = keccak256(_TYPEDEF_META);
3032
struct PayloadMessage {
3133
string title;
3234
//string description;
@@ -39,14 +41,16 @@ contract Only712MacroForwarder is ForwarderBase, EIP712 {
3941
//uint256 validBefore;
4042
uint256 nonce;
4143
}
42-
bytes32 internal constant _TYPEHASH_SECURITY = keccak256("Security(string provider,uint256 nonce)");
44+
bytes internal constant _TYPEDEF_SECURITY = "Security(string provider,uint256 nonce)";
45+
bytes32 internal constant _TYPEHASH_SECURITY = keccak256(_TYPEDEF_SECURITY);
4346

4447
error InvalidPayload(string message);
4548
error InvalidProvider(string provider);
4649
error InvalidSignature();
4750

48-
// TODO: should this be something like "Clear Sign" instead?
49-
constructor(ISuperfluid host, address /*registry*/) ForwarderBase(host) EIP712("Only712MacroForwarder", "1") {}
51+
// Here EIP712 domain name and version are set.
52+
// TODO: should the name include "Superfluid"?
53+
constructor(ISuperfluid host, address /*registry*/) ForwarderBase(host) EIP712("ClearSigning", "1") {}
5054

5155
/**
5256
* @dev Run the macro with encoded payload (generic + macro specific fragments).
@@ -83,11 +87,41 @@ contract Only712MacroForwarder is ForwarderBase, EIP712 {
8387
return retVal;
8488
}
8589

90+
// TODO: should this exist?
91+
function getTypeDefinition(IUserDefined712Macro m) external view returns (string memory) {
92+
return _getTypeDefinition(m);
93+
}
94+
95+
// TODO: should this exist?
96+
function getTypeHash(IUserDefined712Macro m) public view returns (bytes32) {
97+
return keccak256(abi.encodePacked(_getTypeDefinition(m)));
98+
}
99+
100+
// TODO: should this exist?
101+
function getStructHash(IUserDefined712Macro m, bytes calldata params) external view returns (bytes32) {
102+
return _getStructHash(m, abi.decode(params, (Payload)));
103+
}
104+
86105
function getDigest(IUserDefined712Macro m, bytes calldata params) external view returns (bytes32) {
87106
return _getDigest(m, abi.decode(params, (Payload)));
88107
}
89108

90-
function _getDigest(IUserDefined712Macro m, Payload memory payload) internal view returns (bytes32) {
109+
// ==============================
110+
// Internal functions
111+
// ==============================
112+
113+
function _getTypeDefinition(IUserDefined712Macro m) internal view returns (string memory) {
114+
return string(abi.encodePacked(
115+
m.getPrimaryTypeName(),
116+
"(Meta meta,Message message,Security security)",
117+
// nested components need to be in alphabetical order
118+
m.getMessageTypeDefinition(),
119+
_TYPEDEF_META,
120+
_TYPEDEF_SECURITY
121+
));
122+
}
123+
124+
function _getStructHash(IUserDefined712Macro m, Payload memory payload) internal view returns (bytes32) {
91125
bytes32 metaStructHash = _getMetaStructHash(payload.meta);
92126

93127
// the message fragment is handled by the user macro.
@@ -98,29 +132,23 @@ contract Only712MacroForwarder is ForwarderBase, EIP712 {
98132
bytes32 securityStructHash = _getSecurityStructHash(payload.security);
99133

100134
// get the typehash
101-
bytes32 primaryTypeHash = keccak256(
102-
abi.encodePacked(
103-
// TODO: shall we name it "ClearSign"?
104-
"Payload(Meta meta,Message message,Security security)",
105-
// nested components need to be in alphabetical order
106-
m.getMessageTypeHash(),
107-
_TYPEHASH_META,
108-
_TYPEHASH_SECURITY
135+
bytes32 primaryTypeHash = getTypeHash(m);
136+
137+
// calculate the struct hash
138+
bytes32 structHash = keccak256(
139+
abi.encode(
140+
primaryTypeHash,
141+
metaStructHash,
142+
messageStructHash,
143+
securityStructHash
109144
)
110145
);
146+
return structHash;
147+
}
111148

112-
// calculate the digest of the entire payload
113-
bytes32 digest = _hashTypedDataV4(
114-
keccak256(
115-
abi.encode(
116-
primaryTypeHash,
117-
metaStructHash,
118-
messageStructHash,
119-
securityStructHash
120-
)
121-
)
122-
);
123-
return digest;
149+
function _getDigest(IUserDefined712Macro m, Payload memory payload) internal view returns (bytes32) {
150+
bytes32 structHash = _getStructHash(m, payload);
151+
return _hashTypedDataV4(structHash);
124152
}
125153

126154
function _getMetaStructHash(PayloadMeta memory meta) internal pure returns (bytes32) {

packages/ethereum-contracts/test/foundry/utils/Only712MacroForwarder.t.sol

Lines changed: 157 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,34 @@
22
pragma solidity ^0.8.23;
33

44
import { VmSafe } from "forge-std/Vm.sol";
5+
import { console } from "forge-std/console.sol";
56
import { ISuperfluid, ISuperfluidToken } from "../../../contracts/interfaces/superfluid/ISuperfluid.sol";
67
import { IUserDefined712Macro } from "../../../contracts/interfaces/utils/IUserDefinedMacro.sol";
78
import { Only712MacroForwarder } from "../../../contracts/utils/Only712MacroForwarder.sol";
89
import { FoundrySuperfluidTester } from "../FoundrySuperfluidTester.t.sol";
910

10-
// ============== Minimal mock macro for Only712MacroForwarder ==============
11+
string constant MESSAGE_TITLE = "Hello 712";
12+
string constant PRIMARY_TYPE_NAME = "MinimalExample";
13+
string constant META_DOMAIN = "minimalmacro.xyz";
14+
string constant META_VERSION = "1";
15+
string constant SECURITY_PROVIDER = "macros.superfluid.eth";
16+
17+
// returns the encoded payload for the example macro
18+
function getTestPayload() pure returns (bytes memory) {
19+
Only712MacroForwarder.Payload memory payload = Only712MacroForwarder.Payload({
20+
meta: Only712MacroForwarder.PayloadMeta({ domain: META_DOMAIN, version: META_VERSION }),
21+
message: Only712MacroForwarder.PayloadMessage({ title: MESSAGE_TITLE, customPayload: new bytes(0) }),
22+
security: Only712MacroForwarder.PayloadSecurity({ provider: SECURITY_PROVIDER, nonce: 1 })
23+
});
24+
return abi.encode(payload);
25+
}
26+
27+
// ============== Minimal macro for Only712MacroForwarder ==============
1128
// Implements IUserDefined712Macro and uses *no* postCheck logic.
1229
// Message has only the required `title`; `customPayload` is expected to be empty.
1330
contract Minimal712Macro is IUserDefined712Macro {
14-
bytes32 internal constant MESSAGE_TYPEHASH = keccak256("Message(string title)");
15-
bytes32 internal constant EXPECTED_TITLE_HASH = keccak256(bytes("Hello 712"));
31+
32+
string public constant MESSAGE_TYPE_DEFINITION = "Message(string title)";
1633

1734
function buildBatchOperations(ISuperfluid, bytes memory, address)
1835
external
@@ -27,15 +44,20 @@ contract Minimal712Macro is IUserDefined712Macro {
2744
// intentionally empty
2845
}
2946

30-
function getMessageTypeHash() external pure override returns (bytes32) {
31-
return MESSAGE_TYPEHASH;
47+
function getMessageTypeDefinition() external pure override returns (string memory) {
48+
return MESSAGE_TYPE_DEFINITION;
49+
}
50+
51+
function getPrimaryTypeName() external pure override returns (string memory) {
52+
return PRIMARY_TYPE_NAME;
3253
}
3354

3455
function getMessageStructHash(bytes memory message) external pure override returns (bytes32) {
3556
(string memory title, bytes memory customPayload) = abi.decode(message, (string, bytes));
36-
require(keccak256(bytes(title)) == EXPECTED_TITLE_HASH, "wrong title");
57+
require(keccak256(bytes(title)) == keccak256(bytes(MESSAGE_TITLE)), "wrong title");
3758
require(customPayload.length == 0, "customPayload not empty");
38-
return keccak256(abi.encode(MESSAGE_TYPEHASH, keccak256(bytes(title))));
59+
bytes32 messageTypeHash = keccak256(abi.encodePacked(MESSAGE_TYPE_DEFINITION));
60+
return keccak256(abi.encode(messageTypeHash, keccak256(bytes(title))));
3961
}
4062
}
4163

@@ -60,9 +82,9 @@ contract Only712MacroForwarderTest is FoundrySuperfluidTester {
6082
* @dev Smoke test: build payload, get digest via getDigest(), sign with vm.createWallet + vm.sign,
6183
* call runMacro(m, params, signer, signature), assert success.
6284
*/
63-
function test_Minimal712Macro_runMacro_smoke() external {
85+
function testRunMacro() external {
6486
VmSafe.Wallet memory signer = vm.createWallet("signer");
65-
bytes memory params = _buildPayload();
87+
bytes memory params = getTestPayload();
6688
bytes32 digest = forwarder.getDigest(IUserDefined712Macro(address(minimal712Macro)), params);
6789
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signer, digest);
6890
bytes memory signatureVRS = abi.encodePacked(r, s, v);
@@ -72,12 +94,131 @@ contract Only712MacroForwarderTest is FoundrySuperfluidTester {
7294
assertTrue(ok);
7395
}
7496

75-
function _buildPayload() internal pure returns (bytes memory) {
76-
Only712MacroForwarder.Payload memory payload = Only712MacroForwarder.Payload({
77-
meta: Only712MacroForwarder.PayloadMeta({ domain: "test.xyz", version: "1" }),
78-
message: Only712MacroForwarder.PayloadMessage({ title: "Hello 712", customPayload: new bytes(0) }),
79-
security: Only712MacroForwarder.PayloadSecurity({ provider: "macros.superfluid.eth", nonce: 1 })
80-
});
81-
return abi.encode(payload);
97+
function testDigestCalculation() external view {
98+
// check the type definition
99+
string memory typeDefinition = forwarder.getTypeDefinition(minimal712Macro);
100+
string memory expectedTypeDefinition = "MinimalExample(Meta meta,Message message,Security security)Message(string title)Meta(string domain,string version)Security(string provider,uint256 nonce)";
101+
assertEq(typeDefinition, expectedTypeDefinition, "typeDefinition mismatch");
102+
103+
// check the type hash
104+
bytes32 typeHash = forwarder.getTypeHash(minimal712Macro);
105+
bytes32 expectedTypeHash = vm.eip712HashType(expectedTypeDefinition);
106+
assertEq(typeHash, expectedTypeHash, "typeHash mismatch");
107+
108+
// check the struct hash (includes type hash and the struct data)
109+
bytes memory payload = getTestPayload();
110+
bytes32 structHash = forwarder.getStructHash(minimal712Macro, payload);
111+
bytes32 expectedStructHash = vm.eip712HashStruct(typeDefinition, payload);
112+
assertEq(structHash, expectedStructHash, "structHash mismatch");
113+
114+
// check the digest
115+
bytes32 digest = forwarder.getDigest(minimal712Macro, payload);
116+
string memory dataToBeSignedJson = getDataToBeSignedJson();
117+
console.log(dataToBeSignedJson);
118+
bytes32 expectedDigest = vm.eip712HashTypedData(dataToBeSignedJson);
119+
assertEq(digest, expectedDigest, "digest mismatch");
120+
}
121+
122+
// example: https://github.com/vaquita-fi/vaquita-lisk/blob/c4964af9157c9cca9cfb167ac1a4450e36edb29e/contracts/test/VaquitaPool.t.sol#L142
123+
// The splitting up into many functions avoids stack too deep error.
124+
function getDataToBeSignedJson() internal view returns (string memory) {
125+
return string(abi.encodePacked(
126+
'{',
127+
'"types": {', _getTypesJson(), '},',
128+
'"primaryType": "MinimalExample",', // leaving this as literal in order to fit onto the stack
129+
'"domain": {', _getDomainJson(), '},',
130+
'"message": {',
131+
'"meta": {', _getMetaJson(), '},',
132+
'"message": {', _getMessageJson(), '},',
133+
'"security": {', _getSecurityJson(), '}',
134+
'}',
135+
'}'
136+
));
137+
}
138+
139+
function _getTypesJson() internal pure returns (string memory) {
140+
return string(abi.encodePacked(
141+
_getEIP712DomainTypeJson(),
142+
_getMinimalExampleTypeJson(),
143+
_getMessageTypeJson(),
144+
_getMetaTypeJson(),
145+
_getSecurityTypeJson()
146+
));
147+
}
148+
149+
function _getEIP712DomainTypeJson() internal pure returns (string memory) {
150+
return string(abi.encodePacked(
151+
'"EIP712Domain": [',
152+
'{"name": "name", "type": "string"},',
153+
'{"name": "version", "type": "string"},',
154+
'{"name": "chainId", "type": "uint256"},',
155+
'{"name": "verifyingContract", "type": "address"}',
156+
'],'
157+
));
158+
}
159+
160+
function _getMinimalExampleTypeJson() internal pure returns (string memory) {
161+
return string(abi.encodePacked(
162+
'"MinimalExample": [',
163+
'{"name": "meta", "type": "Meta"},',
164+
'{"name": "message", "type": "Message"},',
165+
'{"name": "security", "type": "Security"}',
166+
'],'
167+
));
168+
}
169+
170+
function _getMessageTypeJson() internal pure returns (string memory) {
171+
return string(abi.encodePacked(
172+
'"Message": [',
173+
'{"name": "title", "type": "string"}',
174+
'],'
175+
));
176+
}
177+
178+
function _getMetaTypeJson() internal pure returns (string memory) {
179+
return string(abi.encodePacked(
180+
'"Meta": [',
181+
'{"name": "domain", "type": "string"},',
182+
'{"name": "version", "type": "string"}',
183+
'],'
184+
));
185+
}
186+
187+
function _getSecurityTypeJson() internal pure returns (string memory) {
188+
return string(abi.encodePacked(
189+
'"Security": [',
190+
'{"name": "provider", "type": "string"},',
191+
'{"name": "nonce", "type": "uint256"}',
192+
']'
193+
));
194+
}
195+
196+
function _getDomainJson() internal view returns (string memory) {
197+
return string(abi.encodePacked(
198+
'"name": "ClearSigning",',
199+
'"version": "1",',
200+
'"chainId": ', vm.toString(block.chainid), ',',
201+
'"verifyingContract": "', vm.toString(address(forwarder)), '"'
202+
));
203+
}
204+
205+
function _getMetaJson() internal pure returns (string memory) {
206+
return string(abi.encodePacked(
207+
'"domain": "', META_DOMAIN, '",',
208+
'"version": "', META_VERSION, '"'
209+
));
210+
}
211+
212+
function _getMessageJson() internal pure returns (string memory) {
213+
return string(abi.encodePacked(
214+
'"title": "', MESSAGE_TITLE, '"'
215+
));
216+
}
217+
218+
function _getSecurityJson() internal pure returns (string memory) {
219+
return string(abi.encodePacked(
220+
'"provider": "', SECURITY_PROVIDER, '",',
221+
'"nonce": ', '1'
222+
));
82223
}
83224
}

0 commit comments

Comments
 (0)