Skip to content

Commit 25f1e93

Browse files
legion2002Vectorized
authored andcommitted
✨ Add new calldata optimal execution mode for ERC7821 (#1485)
* feat: add new commonTo execution mode, for calldata optimization * feat: add address(0) replacement to new execution mode * chore: use memory-safe-assembly tag * chore: natspec * fix: clean to address, before checking if it is 0 * feat: replace commonTo mode with calldata optimal mode * chore: replace datas with dataArr
1 parent fda607f commit 25f1e93

File tree

3 files changed

+185
-1
lines changed

3 files changed

+185
-1
lines changed

src/accounts/ERC7821.sol

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ contract ERC7821 is Receiver {
5555
/// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
5656
/// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
5757
/// - `0x01000000000078210002...`: Batch of batches.
58+
/// - `0x01000000000078210003...`: Single batch with common `to` address and optional `opData`.
5859
///
5960
/// For the "batch of batches" mode, each batch will be recursively passed into
6061
/// `execute` internally with mode `0x01000000000078210001...`.
@@ -73,8 +74,10 @@ contract ERC7821 is Receiver {
7374
function execute(bytes32 mode, bytes calldata executionData) public payable virtual {
7475
uint256 id = _executionModeId(mode);
7576
if (id == 3) return _executeBatchOfBatches(mode, executionData);
77+
if (id == 4) return _executeCalldataOptimal(mode, executionData);
7678
Call[] calldata calls;
7779
bytes calldata opData;
80+
7881
/// @solidity memory-safe-assembly
7982
assembly {
8083
if iszero(id) {
@@ -126,7 +129,40 @@ contract ERC7821 is Receiver {
126129
id := eq(m, 0x01000000000000000000) // 1.
127130
id := or(shl(1, eq(m, 0x01000000000078210001)), id) // 2.
128131
id := or(mul(3, eq(m, 0x01000000000078210002)), id) // 3.
132+
id := or(mul(4, eq(m, 0x01000000000078210003)), id) // 4.
133+
}
134+
}
135+
136+
/// @dev For execution of a batch of batches with a common `to` address.
137+
/// @dev if to == address(0), it will be replaced with address(this)
138+
/// Execution Data: abi.encode(address to, bytes[] dataArr, bytes opData)
139+
function _executeCalldataOptimal(bytes32 mode, bytes calldata executionData) internal virtual {
140+
address to;
141+
bytes[] calldata dataArr;
142+
bytes calldata opData;
143+
144+
/// @solidity memory-safe-assembly
145+
assembly {
146+
to := calldataload(executionData.offset)
147+
148+
let dataOffset :=
149+
add(executionData.offset, calldataload(add(0x20, executionData.offset)))
150+
dataArr.offset := add(dataOffset, 0x20)
151+
dataArr.length := calldataload(dataOffset)
152+
153+
// This line is needed to ensure that opdata is valid in all code paths.
154+
// Otherwise the compiler complains.
155+
opData.length := 0
156+
// If the offset of `executionData` allows for `opData`, and the mode supports it.
157+
if gt(calldataload(add(0x20, executionData.offset)), 0x40) {
158+
let opDataOffset :=
159+
add(executionData.offset, calldataload(add(0x40, executionData.offset)))
160+
opData.offset := add(opDataOffset, 0x20)
161+
opData.length := calldataload(opDataOffset)
162+
}
129163
}
164+
165+
_execute(mode, executionData, to, dataArr, opData);
130166
}
131167

132168
/// @dev For execution of a batch of batches.
@@ -190,6 +226,29 @@ contract ERC7821 is Receiver {
190226
revert(); // In your override, replace this with logic to operate on `opData`.
191227
}
192228

229+
/// @dev Executes the calls.
230+
/// Reverts and bubbles up error if any call fails.
231+
/// The `mode` and `executionData` are passed along in case there's a need to use them.
232+
function _execute(
233+
bytes32 mode,
234+
bytes calldata executionData,
235+
address to,
236+
bytes[] calldata dataArr,
237+
bytes calldata opData
238+
) internal virtual {
239+
// Silence compiler warning on unused variables.
240+
mode = mode;
241+
executionData = executionData;
242+
// Very basic auth to only allow this contract to be called by itself.
243+
// Override this function to perform more complex auth with `opData`.
244+
if (opData.length == uint256(0)) {
245+
require(msg.sender == address(this));
246+
// Remember to return `_execute(calls, extraData)` when you override this function.
247+
return _execute(dataArr, to, bytes32(0));
248+
}
249+
revert(); // In your override, replace this with logic to operate on `opData`.
250+
}
251+
193252
/// @dev Executes the calls.
194253
/// Reverts and bubbles up error if any call fails.
195254
/// `extraData` can be any supplementary data (e.g. a memory pointer, some hash).
@@ -204,6 +263,28 @@ contract ERC7821 is Receiver {
204263
}
205264
}
206265

266+
/// @dev Executes the dataArr, with a common `to` address.
267+
/// @dev if to == address(0), it will be replaced with address(this)
268+
/// @dev value for all calls is set to 0
269+
/// Reverts and bubbles up error if any call fails.
270+
/// `extraData` can be any supplementary data (e.g. a memory pointer, some hash).
271+
function _execute(bytes[] calldata dataArr, address to, bytes32 extraData) internal virtual {
272+
unchecked {
273+
uint256 i;
274+
// If `to` is address(0), it will be replaced with address(this)
275+
/// @solidity memory-safe-assembly
276+
assembly {
277+
let t := shr(96, shl(96, to))
278+
to := or(mul(address(), iszero(t)), t)
279+
}
280+
if (dataArr.length == uint256(0)) return;
281+
do {
282+
283+
_execute(to, 0, dataArr[i], extraData);
284+
} while (++i != dataArr.length);
285+
}
286+
}
287+
207288
/// @dev Executes the call.
208289
/// Reverts and bubbles up error if any call fails.
209290
/// `extraData` can be any supplementary data (e.g. a memory pointer, some hash).

test/ERC7821.t.sol

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ contract ERC7821Test is SoladyTest {
1313
address target;
1414

1515
bytes32 internal constant _SUPPORTED_MODE = bytes10(0x01000000000078210001);
16-
16+
bytes32 internal constant _CALLDATA_OPTIMAL_MODE = bytes10(0x01000000000078210003);
1717
bytes[] internal _bytes;
1818

1919
function setUp() public {
@@ -54,6 +54,21 @@ contract ERC7821Test is SoladyTest {
5454
mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, data);
5555
}
5656

57+
function testERC7821CalldataOptimalGas() public {
58+
vm.pauseGasMetering();
59+
vm.deal(address(this), 1 ether);
60+
61+
bytes[] memory dataArr = new bytes[](2);
62+
63+
dataArr[0] = abi.encodeWithSignature("returnsBytes(bytes)", "hehe");
64+
dataArr[1] = abi.encodeWithSignature("returnsHash(bytes)", "lol");
65+
66+
bytes memory data = abi.encode(target, dataArr);
67+
vm.resumeGasMetering();
68+
69+
mbe.execute(_CALLDATA_OPTIMAL_MODE, data);
70+
}
71+
5772
function testERC7821(bytes memory opData) public {
5873
vm.deal(address(this), 1 ether);
5974

@@ -72,6 +87,18 @@ contract ERC7821Test is SoladyTest {
7287
assertEq(mbe.lastOpData(), opData);
7388
}
7489

90+
function testERC7821CalldataOptimal(bytes memory opData) public {
91+
vm.deal(address(this), 1 ether);
92+
93+
bytes[] memory dataArr = new bytes[](2);
94+
dataArr[0] = abi.encodeWithSignature("returnsBytes(bytes)", "hehe");
95+
dataArr[1] = abi.encodeWithSignature("returnsHash(bytes)", "lol");
96+
97+
mbe.execute(_CALLDATA_OPTIMAL_MODE, _encodeCalldataOptimal(target, dataArr, opData));
98+
99+
assertEq(mbe.lastOpData(), opData);
100+
}
101+
75102
function testERC7821ForRevert() public {
76103
ERC7821.Call[] memory calls = new ERC7821.Call[](1);
77104
calls[0].to = target;
@@ -82,6 +109,14 @@ contract ERC7821Test is SoladyTest {
82109
mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, _encode(calls, ""));
83110
}
84111

112+
function testERC7821CalldataOptimalForRevert() public {
113+
bytes[] memory dataArr = new bytes[](1);
114+
dataArr[0] = abi.encodeWithSignature("revertsWithCustomError()");
115+
116+
vm.expectRevert(CustomError.selector);
117+
mbe.execute(_CALLDATA_OPTIMAL_MODE, _encodeCalldataOptimal(target, dataArr, ""));
118+
}
119+
85120
function _encode(ERC7821.Call[] memory calls, bytes memory opData)
86121
internal
87122
returns (bytes memory)
@@ -90,6 +125,14 @@ contract ERC7821Test is SoladyTest {
90125
return abi.encode(calls, opData);
91126
}
92127

128+
function _encodeCalldataOptimal(address to, bytes[] memory dataArr, bytes memory opData)
129+
internal
130+
returns (bytes memory)
131+
{
132+
if (_randomChance(2) && opData.length == 0) return abi.encode(to, dataArr);
133+
return abi.encode(to, dataArr, opData);
134+
}
135+
93136
struct Payload {
94137
bytes data;
95138
uint256 mode;
@@ -125,6 +168,34 @@ contract ERC7821Test is SoladyTest {
125168
}
126169
}
127170

171+
function testERC7821CalldataOptimal(bytes32) public {
172+
vm.deal(address(this), 1 ether);
173+
174+
bytes[] memory dataArr = new bytes[](_randomUniform() & 3);
175+
Payload[] memory payloads = new Payload[](dataArr.length);
176+
177+
for (uint256 i; i < dataArr.length; ++i) {
178+
bytes memory data = _truncateBytes(_randomBytes(), 0x1ff);
179+
payloads[i].data = data;
180+
if (_randomChance(2)) {
181+
payloads[i].mode = 0;
182+
dataArr[i] = abi.encodeWithSignature("returnsBytes(bytes)", data);
183+
} else {
184+
payloads[i].mode = 1;
185+
dataArr[i] = abi.encodeWithSignature("returnsHash(bytes)", data);
186+
}
187+
}
188+
189+
mbe.executeDirect(dataArr, target);
190+
191+
if (dataArr.length != 0 && _randomChance(32)) {
192+
dataArr[_randomUniform() % dataArr.length] =
193+
abi.encodeWithSignature("revertsWithCustomError()");
194+
vm.expectRevert(CustomError.selector);
195+
mbe.executeDirect(dataArr, target);
196+
}
197+
}
198+
128199
function _totalValue(ERC7821.Call[] memory calls) internal pure returns (uint256 result) {
129200
unchecked {
130201
for (uint256 i; i < calls.length; ++i) {
@@ -133,6 +204,7 @@ contract ERC7821Test is SoladyTest {
133204
}
134205
}
135206

207+
136208
function testERC7821ExecuteBatchOfBatches() public {
137209
bytes32 mode = bytes32(0x0100000000007821000200000000000000000000000000000000000000000000);
138210
bytes[] memory batchBytes = new bytes[](3);
@@ -182,4 +254,17 @@ contract ERC7821Test is SoladyTest {
182254
function pushBytes(bytes memory x) public {
183255
_bytes.push(x);
184256
}
257+
258+
function testERC7821CalldataOptimalWithZeroAddress() public {
259+
// Test that when to=address(0), it gets replaced with address(this) (the MockERC7821 contract)
260+
// We'll call executeDirect which directly calls the internal _execute function
261+
bytes[] memory dataArr = new bytes[](1);
262+
dataArr[0] = abi.encodeWithSignature("setAuthorizedCaller(address,bool)", address(0x123), true);
263+
264+
// This should replace address(0) with address(mbe) and call setAuthorizedCaller on itself
265+
mbe.executeDirect(dataArr, address(0));
266+
267+
// Verify the call succeeded by checking that address(0x123) is now authorized
268+
assertTrue(mbe.isAuthorizedCaller(address(0x123)));
269+
}
185270
}

test/utils/mocks/MockERC7821.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ contract MockERC7821 is ERC7821, Brutalizer {
2222
_execute(calls, bytes32(0));
2323
}
2424

25+
function _execute(
26+
bytes32,
27+
bytes calldata,
28+
address to,
29+
bytes[] calldata dataArr,
30+
bytes calldata opData
31+
) internal virtual override {
32+
lastOpData = opData;
33+
_execute(dataArr, to, bytes32(0));
34+
}
35+
2536
function execute(bytes32 mode, bytes calldata executionData) public payable virtual override {
2637
if (!isAuthorizedCaller[msg.sender]) revert Unauthorized();
2738
super.execute(mode, executionData);
@@ -34,6 +45,13 @@ contract MockERC7821 is ERC7821, Brutalizer {
3445
_checkMemory();
3546
}
3647

48+
function executeDirect(bytes[] calldata dataArr, address to) public payable virtual {
49+
_misalignFreeMemoryPointer();
50+
_brutalizeMemory();
51+
_execute(dataArr, to, bytes32(0));
52+
_checkMemory();
53+
}
54+
3755
function setAuthorizedCaller(address target, bool status) public {
3856
isAuthorizedCaller[target] = status;
3957
}

0 commit comments

Comments
 (0)