Skip to content

Commit 13b0919

Browse files
committed
Add support for signing EIP-712 typed data
1 parent ea4a921 commit 13b0919

File tree

5 files changed

+85
-8
lines changed

5 files changed

+85
-8
lines changed

script/universal/MultisigDeploy.sol

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ contract MultisigDeployScript is Script {
135135
console.log(" Salt Nonce:", saltNonce);
136136

137137
// Resolve owner addresses (combine direct owners + referenced safe addresses)
138-
address[] memory resolvedOwners = resolveOwnerAddresses(config, safes);
138+
address[] memory resolvedOwners = resolveOwnerAddresses(config);
139139

140140
console.log(" Total Owners:", resolvedOwners.length);
141141
console.log(" Direct Owners:", config.owners.length);
@@ -186,11 +186,7 @@ contract MultisigDeployScript is Script {
186186
}
187187
}
188188

189-
function resolveOwnerAddresses(SafeWallet memory config, SafeWallet[] memory safes)
190-
internal
191-
view
192-
returns (address[] memory)
193-
{
189+
function resolveOwnerAddresses(SafeWallet memory config) internal view returns (address[] memory) {
194190
uint256 totalOwners = config.owners.length + config.ownerRefIndices.length;
195191
address[] memory resolved = new address[](totalOwners);
196192

script/universal/MultisigScript.sol

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {console} from "forge-std/console.sol";
66
import {Script} from "forge-std/Script.sol";
77
import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol";
88
import {Vm} from "forge-std/Vm.sol";
9+
import {stdJson} from "forge-std/StdJson.sol";
910

1011
import {IGnosisSafe, Enum} from "./IGnosisSafe.sol";
1112
import {Signatures} from "./Signatures.sol";
@@ -332,7 +333,7 @@ abstract contract MultisigScript is Script {
332333
return safes;
333334
}
334335

335-
function _transactionDatas(address[] memory _safes) private view returns (bytes[] memory datas, uint256 value) {
336+
function _transactionDatas(address[] memory _safes) internal view returns (bytes[] memory datas, uint256 value) {
336337
// Build the calls and sum the values
337338
IMulticall3.Call3Value[] memory calls = _buildCalls();
338339
for (uint256 i = 0; i < calls.length; i++) {
@@ -375,7 +376,8 @@ abstract contract MultisigScript is Script {
375376
}
376377

377378
function _printDataToSign(address _safe, bytes memory _data, uint256 _value) internal {
378-
bytes memory txData = _encodeTransactionData(_safe, _data, _value);
379+
bytes memory txData =
380+
_printDataHashes() ? _encodeTransactionData(_safe, _data, _value) : _encodeEIP712Json(_safe, _data, _value);
379381
bytes32 hash = _getTransactionHash(_safe, _data, _value);
380382

381383
emit DataToSign(txData);
@@ -397,6 +399,14 @@ abstract contract MultisigScript is Script {
397399
console.log("###############################");
398400
}
399401

402+
// Controls whether the safe tx is printed as structured EIP-712 data, or just hashes.
403+
//
404+
// If you want to print and sign hashed EIP-712 data (domain + message hash) rather than the
405+
// typed EIP-712 data struct, override this function and return `true`.
406+
function _printDataHashes() internal view virtual returns (bool) {
407+
return false;
408+
}
409+
400410
function _executeTransaction(
401411
address _safe,
402412
bytes memory _data,
@@ -566,6 +576,39 @@ abstract contract MultisigScript is Script {
566576
});
567577
}
568578

579+
function _encodeEIP712Json(address _safe, bytes memory _data, uint256 _value) internal returns (bytes memory) {
580+
string memory types = '{"EIP712Domain":[' '{"name":"chainId","type":"uint256"},'
581+
'{"name":"verifyingContract","type":"address"}],' '"SafeTx":[' '{"name":"to","type":"address"},'
582+
'{"name":"value","type":"uint256"},' '{"name":"data","type":"bytes"},'
583+
'{"name":"operation","type":"uint8"},' '{"name":"safeTxGas","type":"uint256"},'
584+
'{"name":"baseGas","type":"uint256"},' '{"name":"gasPrice","type":"uint256"},'
585+
'{"name":"gasToken","type":"address"},' '{"name":"refundReceiver","type":"address"},'
586+
'{"name":"nonce","type":"uint256"}]}';
587+
588+
string memory domain = stdJson.serialize("domain", "chainId", uint256(block.chainid));
589+
domain = stdJson.serialize("domain", "verifyingContract", address(_safe));
590+
591+
string memory message = stdJson.serialize("message", "to", MULTICALL3_ADDRESS);
592+
message = stdJson.serialize("message", "value", _value);
593+
message = stdJson.serialize("message", "data", _data);
594+
message = stdJson.serialize(
595+
"message", "operation", uint256(_value == 0 ? Enum.Operation.DelegateCall : Enum.Operation.Call)
596+
);
597+
message = stdJson.serialize("message", "safeTxGas", uint256(0));
598+
message = stdJson.serialize("message", "baseGas", uint256(0));
599+
message = stdJson.serialize("message", "gasPrice", uint256(0));
600+
message = stdJson.serialize("message", "gasToken", address(0));
601+
message = stdJson.serialize("message", "refundReceiver", address(0));
602+
message = stdJson.serialize("message", "nonce", _getNonce(_safe));
603+
604+
string memory json = stdJson.serialize("", "primaryType", string("SafeTx"));
605+
json = stdJson.serialize("", "types", types);
606+
json = stdJson.serialize("", "domain", domain);
607+
json = stdJson.serialize("", "message", message);
608+
609+
return abi.encodePacked(json);
610+
}
611+
569612
function _execTransactionCalldata(address _safe, bytes memory _data, uint256 _value, bytes memory _signatures)
570613
internal
571614
pure

test/universal/DoubleNestedMultisigBuilder.t.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ contract DoubleNestedMultisigBuilderTest is Test, DoubleNestedMultisigBuilder {
7777
return safe4;
7878
}
7979

80+
function _printDataHashes() internal view override returns (bool) {
81+
return true;
82+
}
83+
8084
function test_sign_double_nested_safe1() external {
8185
vm.recordLogs();
8286
bytes memory txData = abi.encodeCall(DoubleNestedMultisigBuilder.sign, (safe1, safe3));

test/universal/MultisigBuilder.t.sol

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ contract MultisigBuilderTest is Test, MultisigBuilder {
2020
Counter internal counter = new Counter(address(safe));
2121

2222
function () internal view returns (IMulticall3.Call3Value[] memory) buildCallsInternal;
23+
function () internal view returns (bool) printDataHashesInternal = printDataHashesEnabled;
2324

2425
bytes internal dataToSignNoValue =
2526
// solhint-disable-next-line max-line-length
@@ -29,6 +30,10 @@ contract MultisigBuilderTest is Test, MultisigBuilder {
2930
// solhint-disable-next-line max-line-length
3031
hex"1901d4bb33110137810c444c1d9617abe97df097d587ecde64e6fcb38d7f49e1280cd150dbb03d4bb38e5325a914ff3861da880437fd5856c0f7e39054e64e05aed0";
3132

33+
string internal dataToSignTyped =
34+
// solhint-disable-next-line max-line-length
35+
'{"domain":{"chainId":31337,"verifyingContract":"0x00000000000000000000000000000000000003e9"},"message":{"baseGas":0,"data":"0x174dea710000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000005615deb798bb3e4dfa0139dfa1b3d433cc23b72f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004d09de08a00000000000000000000000000000000000000000000000000000000","gasPrice":0,"gasToken":"0x0000000000000000000000000000000000000000","nonce":0,"operation":1,"refundReceiver":"0x0000000000000000000000000000000000000000","safeTxGas":0,"to":"0xcA11bde05977b3631167028862bE2a173976CA11","value":0},"primaryType":"SafeTx","types":{"EIP712Domain":[{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"SafeTx":[{"name":"to","type":"address"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"},{"name":"operation","type":"uint8"},{"name":"safeTxGas","type":"uint256"},{"name":"baseGas","type":"uint256"},{"name":"gasPrice","type":"uint256"},{"name":"gasToken","type":"address"},{"name":"refundReceiver","type":"address"},{"name":"nonce","type":"uint256"}]}}';
36+
3237
function setUp() public {
3338
vm.etch(safe, Preinstalls.getDeployedCode(Preinstalls.Safe_v130, block.chainid));
3439
vm.etch(Preinstalls.MultiCall3, Preinstalls.getDeployedCode(Preinstalls.MultiCall3, block.chainid));
@@ -54,6 +59,10 @@ contract MultisigBuilderTest is Test, MultisigBuilder {
5459
return address(safe);
5560
}
5661

62+
function _printDataHashes() internal view override returns (bool) {
63+
return printDataHashesInternal();
64+
}
65+
5766
function test_sign_no_value() external {
5867
buildCallsInternal = _buildCallsNoValue;
5968

@@ -78,6 +87,19 @@ contract MultisigBuilderTest is Test, MultisigBuilder {
7887
assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSignWithValue)));
7988
}
8089

90+
function test_sign_typed_data() external {
91+
buildCallsInternal = _buildCallsNoValue;
92+
printDataHashesInternal = printDataHashesDisabled;
93+
94+
vm.recordLogs();
95+
bytes memory txData = abi.encodeCall(MultisigBuilder.sign, ());
96+
vm.prank(wallet1.addr);
97+
(bool success,) = address(this).call(txData);
98+
vm.assertTrue(success);
99+
Vm.Log[] memory logs = vm.getRecordedLogs();
100+
assertEq(keccak256(logs[logs.length - 1].data), keccak256(abi.encode(dataToSignTyped)));
101+
}
102+
81103
function test_run() external {
82104
buildCallsInternal = _buildCallsNoValue;
83105
(uint8 v1, bytes32 r1, bytes32 s1) = vm.sign(wallet1, keccak256(dataToSignNoValue));
@@ -111,4 +133,12 @@ contract MultisigBuilderTest is Test, MultisigBuilder {
111133

112134
return calls;
113135
}
136+
137+
function printDataHashesEnabled() internal pure returns (bool) {
138+
return true;
139+
}
140+
141+
function printDataHashesDisabled() internal pure returns (bool) {
142+
return false;
143+
}
114144
}

test/universal/NestedMultisigBuilder.t.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ contract NestedMultisigBuilderTest is Test, NestedMultisigBuilder {
7171
return address(safe3);
7272
}
7373

74+
function _printDataHashes() internal view override returns (bool) {
75+
return true;
76+
}
77+
7478
function test_sign_safe1() external {
7579
vm.recordLogs();
7680
bytes memory txData = abi.encodeCall(NestedMultisigBuilder.sign, (safe1));

0 commit comments

Comments
 (0)