Skip to content
Merged
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
2 changes: 1 addition & 1 deletion out/Solver7702Delegate.sol/Solver7702Delegate.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion snapshots/Solver7702DelegateTest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"approved caller target payload - success - forwards": "31104",
"approved caller target return data - success - bubbles return data": "24915",
"approved caller target revert data - reverts - bubbles non-empty data": "25584",
"unauthorized caller - success - receives ETH": "21199",
"self call no value - success - bypasses auth": "21930",
"unauthorized caller - success - receives ETH": "21221",
"unauthorized caller no value - reverts - unauthorized": "21930"
}
14 changes: 10 additions & 4 deletions src/Solver7702Delegate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ contract Solver7702Delegate {
) return _callThrough();

// Accept ETH from anyone, even if unauthorized
if (msg.value > 0) return;
Comment thread
kaze-cow marked this conversation as resolved.
// We do this to preserve the behavior of an EOA address (which doesn't revert when ETH is sent) as much as
// reasonably possible. As it is unlikely that a contract will treat a solver address as a contract and
// simultaneously send ETH, this should be reasonbly safe.
// We also return gracefully on calls to self to prevent compatibility errors with existing solvers that expect
// to be able to cancel their transaction by call to self.
if (msg.value > 0 || msg.sender == address(this)) return;
revert Unauthorized(msg.sender);
}

Expand All @@ -55,9 +60,10 @@ contract Solver7702Delegate {
address target = address(bytes20(msg.data[0:20]));

assembly {
// Extract calldata in range (target, len(msg.data)).
// We take full control of memory in this inline assembly block because it will not return to Solidity code.
// This is why we overwrite the Solidity scratch pad at memory position 0.
// Extract calldata in range (target, len(msg.data)). For efficiency and simplicity, we overwrite from
// memory position 0 (the solidity scratch pad). If `calldatasize()` is larger than 84, the Solidity free
// memory pointer and "zero" slot may also be overwritten with arbitrary data.
// Since we do not return to Solidity code, this is not a problem.
calldatacopy(0x00, 20, sub(calldatasize(), 20))

// Call the implementation
Expand Down
12 changes: 12 additions & 0 deletions test/Solver7702Delegate.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ contract Solver7702DelegateTest is BaseTest {
assertEq(returnData, expectedReturnData, "target return data should bubble");
}

function test_unit_fallback_success_selfCallDoesNotRevert() public {
bytes memory payload = hex"12345678";

vm.prank(address(delegateContract));
(bool success, bytes memory returnData) =
address(delegateContract).call(_packedCalldata(fallbackTarget, payload));
vm.snapshotGasLastCall("self call no value - success - bypasses auth");

assertTrue(success, "self call should succeed");
assertEq(returnData.length, 0, "self call should return empty response");
}

function test_unit_fallback_revertsWith_UnauthorizedCaller() public {
bytes memory payload = hex"12345678";

Expand Down
Loading