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
1 change: 1 addition & 0 deletions interfaces/L1/IOptimismPortal2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface IOptimismPortal2 is IProxyAdminOwnedBase {
error OptimismPortal_CallPaused();
error OptimismPortal_CalldataTooLarge();
error OptimismPortal_NotAllowedOnCGTMode();
error OptimismPortal_DelegatedTargetCallFailed();
error OptimismPortal_GasEstimation();
error OptimismPortal_GasLimitTooLow();
error OptimismPortal_ImproperDisputeGame();
Expand Down
5 changes: 5 additions & 0 deletions snapshots/abi/OptimismPortal2.json
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,11 @@
"name": "OptimismPortal_CalldataTooLarge",
"type": "error"
},
{
"inputs": [],
"name": "OptimismPortal_DelegatedTargetCallFailed",
"type": "error"
},
{
"inputs": [],
"name": "OptimismPortal_GasEstimation",
Expand Down
12 changes: 6 additions & 6 deletions snapshots/semver-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
"sourceCodeHash": "0x1f3e2c1d357ca4ab1973be2e6a49c67ccac1a325f3ca618dceff54ef47d20ea8"
},
"src/L1/L1ERC721Bridge.sol:L1ERC721Bridge": {
"initCodeHash": "0x2cd7959d1f40e525328f4c1fa42f9ef18ac73fdda954155a99450ce5835f4630",
"initCodeHash": "0x4ab53d1f58ddf1d106f805ad4633cf76a493af324f62405d994e427f17165a53",
"sourceCodeHash": "0x052fa698c0526d026bc91ebc033d75160ce9f0e3aa395834c00707e2896a3c3c"
},
"src/L1/L1StandardBridge.sol:L1StandardBridge": {
"initCodeHash": "0x788cb66ad7c4efbcc55629cb407735c1f3a77633410a123a018ac77585db69af",
"initCodeHash": "0xb19625cd26ea7d8f79a19e292e409ab505e730e5273f2179eba8b2c6008480e8",
"sourceCodeHash": "0x12df1f137e66f1c023d0496f6ecd300c3cf54926de7af4fd4720f1fc8ce82689"
},
"src/L1/OptimismPortal2.sol:OptimismPortal2": {
"initCodeHash": "0x121346929ba2c2fc47a18a863c8dde6ecea27a70e7d0182910fbe3199e9ee71d",
"sourceCodeHash": "0x4bdaf6c4018e104b13f43676a5a389c9ac18181438a9100298bb0050a2319bd7"
"initCodeHash": "0xbf002905aef9f976be34670f737a71fa6292fe469ad3d1469ec815dd3712cd42",
"sourceCodeHash": "0xe7ec1b8d91402b825f30135afb1a1394a889313c016cb989e2646522db7dfecf"
},
"src/L1/ProtocolVersions.sol:ProtocolVersions": {
"initCodeHash": "0xbca4c0d69dcb7a196a9ebfcc8cb77162fa090377c5dda99831d61c6c68ba4352",
Expand Down Expand Up @@ -80,11 +80,11 @@
"sourceCodeHash": "0x12ea125038b87e259a0d203e119faa6e9726ab2bdbc30430f820ccd48fe87e14"
},
"src/L2/L2ERC721Bridge.sol:L2ERC721Bridge": {
"initCodeHash": "0x863f0f5b410983f3e51cd97c60a3a42915141b7452864d0e176571d640002b81",
"initCodeHash": "0xe9d79ab92871ff7d1336059a8756041e052bca05f2c0e98d20ed97f545162191",
"sourceCodeHash": "0x032bda19a2c0a05e8abfdac182982ad14256cbcebc1896ba503bb1a2a681e81f"
},
"src/L2/L2StandardBridge.sol:L2StandardBridge": {
"initCodeHash": "0xba5b288a396b34488ba7be68473305529c7da7c43e5f1cfc48d6a4aecd014103",
"initCodeHash": "0x61f1e19928b92a5455d04c990071728b006d2083887250f473f147cd9002345f",
"sourceCodeHash": "0xc53a2f1fc472b15aa2891d48845b078ef12fdae869a4b74bef19b19d9d20ed5f"
},
"src/L2/L2ToL1MessagePasser.sol:L2ToL1MessagePasser": {
Expand Down
15 changes: 12 additions & 3 deletions src/L1/OptimismPortal2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase
/// @notice Thrown when a CGT withdrawal is not allowed.
error OptimismPortal_NotAllowedOnCGTMode();

/// @notice Thrown when the call to the target of a withdrawal fails and the target is a 7702 delegated EOA.
error OptimismPortal_DelegatedTargetCallFailed();

/// @notice Thrown when a gas estimation transaction is being executed.
error OptimismPortal_GasEstimation();

Expand Down Expand Up @@ -202,9 +205,9 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase
error OptimismPortal_Unproven();

/// @notice Semantic version.
/// @custom:semver 7.0.0
/// @custom:semver 7.0.1
function version() public pure virtual returns (string memory) {
return "7.0.0";
return "7.0.1";
}

/// @param _proofMaturityDelaySeconds The proof maturity delay in seconds.
Expand Down Expand Up @@ -472,7 +475,13 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ReinitializableBase
// Reset the l2Sender back to the default value.
l2Sender = Constants.DEFAULT_L2_SENDER;

// All withdrawals are immediately finalized. Replayability can
// Revert if the call failed and the target is a 7702 delegated EOA. The withdrawal is not
// finalized, so it can be finalized once the target accepts the call.
if (!success && EOA.isDelegated(_tx.target)) {
revert OptimismPortal_DelegatedTargetCallFailed();
}

// All other withdrawals are immediately finalized. Replayability can
// be achieved through contracts built on top of this contract
emit WithdrawalFinalized(withdrawalHash, success);

Expand Down
21 changes: 12 additions & 9 deletions src/libraries/EOA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ library EOA {
/// @notice Returns true if sender address is an EOA.
/// @return isEOA_ True if the sender address is an EOA.
function isSenderEOA() internal view returns (bool isEOA_) {
if (msg.sender == tx.origin) {
isEOA_ = true;
} else if (address(msg.sender).code.length == 23) {
// If the sender is not the origin, check for 7702 delegated EOAs.
// If the sender is not the origin, check for 7702 delegated EOAs.
isEOA_ = msg.sender == tx.origin || isDelegated(msg.sender);
}

/// @notice Returns true if the account is a 7702 delegated EOA.
/// @param _account Address of the account to check.
/// @return isDelegated_ True if the account is a 7702 delegated EOA.
function isDelegated(address _account) internal view returns (bool isDelegated_) {
// A 7702 delegated EOA has exactly 23 bytes of code, starting with 0xEF0100.
if (_account.code.length == 23) {
assembly {
let ptr := mload(0x40)
mstore(0x40, add(ptr, 0x20))
extcodecopy(caller(), ptr, 0, 0x20)
isEOA_ := eq(shr(232, mload(ptr)), 0xEF0100)
extcodecopy(_account, ptr, 0, 0x20)
isDelegated_ := eq(shr(232, mload(ptr)), 0xEF0100)
}
} else {
// If more or less than 23 bytes of code, not a 7702 delegated EOA.
isEOA_ = false;
}
}
}
55 changes: 55 additions & 0 deletions test/L1/OptimismPortal2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,61 @@ contract OptimismPortal2_FinalizeWithdrawalTransaction_Test is OptimismPortal2_T
assertEq(address(optimismPortal2).balance, portalBalanceBefore);
}

/// @notice Tests that `finalizeWithdrawalTransaction` reverts if the target is a 7702 delegated
/// EOA that rejects the call, and finalizes the withdrawal once the delegation is
/// cleared.
function test_finalizeWithdrawalTransaction_delegatedTargetFails_reverts() external {
// Delegate bob to a contract with just the invalid opcode.
address delegate = makeAddr("delegate");
vm.etch(delegate, hex"fe");
vm.etch(bob, abi.encodePacked(hex"EF0100", delegate));

_proveDefaultWithdrawal();
_resolveGameAndWarpPastProofMaturity(game);

uint256 bobBalanceBefore = address(bob).balance;
uint256 portalBalanceBefore = address(optimismPortal2).balance;

vm.expectRevert(IOptimismPortal.OptimismPortal_DelegatedTargetCallFailed.selector);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);

// The withdrawal is not finalized and the ETH is still in the portal.
assertFalse(optimismPortal2.finalizedWithdrawals(_withdrawalHash));
assertEq(address(bob).balance, bobBalanceBefore);
assertEq(address(optimismPortal2).balance, portalBalanceBefore);

// Clear the delegation of bob.
vm.etch(bob, hex"");

vm.expectEmit(true, true, true, true);
emit WithdrawalFinalized(_withdrawalHash, true);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);

assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash));
assertEq(address(bob).balance, bobBalanceBefore + _defaultTx.value);
}

/// @notice Tests that `finalizeWithdrawalTransaction` succeeds if the target is a 7702 delegated
/// EOA that accepts the call.
function test_finalizeWithdrawalTransaction_delegatedTargetAccepts_succeeds() external {
// Delegate bob to a contract with just the stop opcode.
address delegate = makeAddr("delegate");
vm.etch(delegate, hex"00");
vm.etch(bob, abi.encodePacked(hex"EF0100", delegate));

_proveDefaultWithdrawal();
_resolveGameAndWarpPastProofMaturity(game);

uint256 bobBalanceBefore = address(bob).balance;

vm.expectEmit(true, true, true, true);
emit WithdrawalFinalized(_withdrawalHash, true);
optimismPortal2.finalizeWithdrawalTransaction(_defaultTx);

assertTrue(optimismPortal2.finalizedWithdrawals(_withdrawalHash));
assertEq(address(bob).balance, bobBalanceBefore + _defaultTx.value);
}

/// @notice Tests that `finalizeWithdrawalTransaction` reverts if the withdrawal has already
/// been finalized.
function test_finalizeWithdrawalTransaction_onReplay_reverts() external {
Expand Down
56 changes: 56 additions & 0 deletions test/libraries/EOA.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ contract EOA_Harness {
function isSenderEOA() external view returns (bool isEOA_) {
return EOA.isSenderEOA();
}

/// @notice Returns true if the account is a 7702 delegated EOA.
/// @param _account Address of the account to check.
/// @return isDelegated_ True if the account is a 7702 delegated EOA.
function isDelegated(address _account) external view returns (bool isDelegated_) {
return EOA.isDelegated(_account);
}
}

/// @title EOA_isSenderEOA_Test
Expand Down Expand Up @@ -88,3 +95,52 @@ contract EOA_isSenderEOA_Test is Test {
sender_ = vm.addr(boundPrivateKey(_privateKey));
}
}

/// @title EOA_isDelegated_Test
/// @notice Tests the `isDelegated` function of the `EOA` library.
contract EOA_isDelegated_Test is Test {
EOA_Harness harness;
address account;

/// @notice Sets up the test.
function setUp() public {
harness = new EOA_Harness();
account = makeAddr("account");
}

/// @notice Tests that an account without code is not detected as a 7702 delegated EOA.
function test_isDelegated_noCode_succeeds() external view {
assertFalse(harness.isDelegated(account));
}

/// @notice Tests that a 7702 EOA is detected as a 7702 delegated EOA.
/// @param _7702Target The target of the 7702 EOA.
function testFuzz_isDelegated_is7702EOA_succeeds(address _7702Target) external {
// Delegating to address(0) revokes the delegation per EIP-7702, so exclude it.
vm.assume(_7702Target != address(0));

vm.etch(account, abi.encodePacked(hex"EF0100", _7702Target));

assertTrue(harness.isDelegated(account));
}

/// @notice Tests that a contract is not detected as a 7702 delegated EOA.
/// @param _code The code of the contract.
function testFuzz_isDelegated_isContract_succeeds(bytes memory _code) external {
// Avoid empty code and the 0xEF prefix space, which includes 7702 delegation code.
if (_code.length == 0 || _code[0] == 0xEF) {
_code = bytes.concat(hex"FFFFFF", _code);
}

vm.etch(account, _code);

assertFalse(harness.isDelegated(account));
}

/// @notice Tests that a contract with 23 bytes of code is not detected as a 7702 delegated EOA.
function test_isDelegated_isContract23BytesNot7702_succeeds() external {
vm.etch(account, abi.encodePacked(hex"FE", bytes22(0)));

assertFalse(harness.isDelegated(account));
}
}
Loading