Skip to content

Commit dd72de1

Browse files
authored
Merge pull request #15 from base-org/amie/block-delegatecalls-until-initialized
Prevent all delegated calls unless proxy has been initialized
2 parents 9b7b59c + ee0747d commit dd72de1

File tree

5 files changed

+170
-18
lines changed

5 files changed

+170
-18
lines changed

src/EIP7702Proxy.sol

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pragma solidity ^0.8.23;
44
import {Proxy} from "openzeppelin-contracts/contracts/proxy/Proxy.sol";
55
import {ERC1967Utils} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol";
66
import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
7-
import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol";
7+
import {StorageSlot} from "openzeppelin-contracts/contracts/utils/StorageSlot.sol";
88

99
/// @title EIP7702Proxy
1010
/// @notice Proxy contract designed for EIP-7702 smart accounts
@@ -24,6 +24,12 @@ contract EIP7702Proxy is Proxy {
2424
/// @notice Function selector on the implementation that is guarded from direct calls
2525
bytes4 immutable guardedInitializer;
2626

27+
/// @dev Storage slot with the initialized flag, conforms to ERC-7201
28+
bytes32 internal constant INITIALIZED_SLOT =
29+
keccak256(
30+
abi.encode(uint256(keccak256("EIP7702Proxy.initialized")) - 1)
31+
) & ~bytes32(uint256(0xff));
32+
2733
/// @notice Emitted when the implementation is upgraded
2834
event Upgraded(address indexed implementation);
2935

@@ -33,8 +39,8 @@ contract EIP7702Proxy is Proxy {
3339
/// @notice Emitted when the `guardedInitializer` is called
3440
error InvalidInitializer();
3541

36-
/// @notice Emitted when initialization is attempted on a non-initial implementation
37-
error InvalidImplementation();
42+
/// @notice Emitted when trying to delegate before initialization
43+
error ProxyNotInitialized();
3844

3945
/// @notice Emitted when constructor arguments are zero
4046
error ZeroValueConstructorArguments();
@@ -52,6 +58,11 @@ contract EIP7702Proxy is Proxy {
5258
guardedInitializer = initializer;
5359
}
5460

61+
/// @dev Checks if proxy has been initialized by checking the initialized flag
62+
function _isInitialized() internal view returns (bool) {
63+
return StorageSlot.getBooleanSlot(INITIALIZED_SLOT).value;
64+
}
65+
5566
/// @notice Initializes the proxy and implementation with a signed payload
5667
///
5768
/// @dev Signature must be from this contract's address
@@ -62,17 +73,20 @@ contract EIP7702Proxy is Proxy {
6273
bytes calldata args,
6374
bytes calldata signature
6475
) external {
65-
// construct hash incompatible with wallet RPCs to avoid phishing
76+
// Construct hash without Ethereum signed message prefix to prevent phishing via standard wallet signing.
77+
// Since this proxy is designed for EIP-7702 (where the proxy address is an EOA),
78+
// using a raw hash ensures that initialization signatures cannot be obtained through normal
79+
// wallet "Sign Message" prompts. This prevents malicious dapps from tricking users into
80+
// initializing their account via standard wallet signing flows.
81+
// Wallets must implement custom signing logic at a lower level to support initialization.
6682
bytes32 hash = keccak256(abi.encode(proxy, args));
6783
address recovered = ECDSA.recover(hash, signature);
6884
if (recovered != address(this)) revert InvalidSignature();
6985

70-
// enforce initialization only on initial implementation
71-
address implementation = _implementation();
72-
if (implementation != initialImplementation)
73-
revert InvalidImplementation();
86+
// Set initialized flag before upgrading
87+
StorageSlot.getBooleanSlot(INITIALIZED_SLOT).value = true;
7488

75-
// Set the ERC-1967 implementation slot, emit Upgraded event, call the initializer on the initial implementation
89+
// Set the ERC-1967 implementation slot, emit Upgraded event, call the initializer
7690
ERC1967Utils.upgradeToAndCall(
7791
initialImplementation,
7892
abi.encodePacked(guardedInitializer, args)
@@ -91,6 +105,9 @@ contract EIP7702Proxy is Proxy {
91105
bytes32 hash,
92106
bytes calldata signature
93107
) external returns (bytes4) {
108+
// Check initialization status first
109+
if (!_isInitialized()) revert ProxyNotInitialized();
110+
94111
// First try delegatecall to implementation
95112
(bool success, bytes memory result) = _implementation().delegatecall(
96113
msg.data
@@ -117,22 +134,19 @@ contract EIP7702Proxy is Proxy {
117134
return ERC1271_FAIL_VALUE;
118135
}
119136

120-
/// @inheritdoc Proxy
121-
function _implementation() internal view override returns (address) {
122-
address implementation = ERC1967Utils.getImplementation();
123-
return
124-
implementation != address(0)
125-
? implementation
126-
: initialImplementation;
127-
}
128-
129137
/// @inheritdoc Proxy
130138
/// @dev Handles ERC-1271 signature validation by enforcing an ecrecover check if signatures fail `isValidSignature` check
131139
/// @dev Guards a specified initializer function from being called directly
132140
function _fallback() internal override {
141+
if (!_isInitialized()) revert ProxyNotInitialized();
142+
133143
// block guarded initializer from being called
134144
if (msg.sig == guardedInitializer) revert InvalidInitializer();
135145

136146
_delegate(_implementation());
137147
}
148+
149+
function _implementation() internal view override returns (address) {
150+
return ERC1967Utils.getImplementation();
151+
}
138152
}

test/EIP7702Proxy/coinbaseImplementation.t.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ contract CoinbaseImplementationTest is Test {
167167
uint256 amount
168168
) public {
169169
vm.assume(recipient != address(0));
170+
vm.assume(recipient != address(_eoa));
170171
assumeNotPrecompile(recipient);
171172
assumePayable(recipient);
172173
vm.assume(amount > 0 && amount <= 100 ether);
@@ -193,6 +194,7 @@ contract CoinbaseImplementationTest is Test {
193194
) public {
194195
vm.assume(nonOwner != address(0));
195196
vm.assume(nonOwner != _newOwner); // Ensure caller isn't the actual owner
197+
vm.assume(nonOwner != _eoa); // Ensure caller isn't the EOA address
196198

197199
address newImpl = address(new CoinbaseSmartWallet());
198200

test/EIP7702Proxy/delegate.t.sol

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,60 @@ contract DelegateTest is EIP7702ProxyBase {
6868
"State should not change when write fails"
6969
);
7070
}
71+
72+
function test_reverts_whenCallingBeforeInitialization() public {
73+
// Deploy a fresh proxy without initializing it
74+
address payable uninitProxy = payable(makeAddr("uninitProxy"));
75+
_deployProxy(uninitProxy);
76+
77+
vm.expectRevert(EIP7702Proxy.ProxyNotInitialized.selector);
78+
MockImplementation(payable(uninitProxy)).owner();
79+
}
80+
81+
function test_reverts_whenCallingWithArbitraryDataBeforeInitialization(
82+
bytes memory arbitraryCalldata
83+
) public {
84+
// Deploy a fresh proxy without initializing it
85+
address payable uninitProxy = payable(makeAddr("uninitProxy"));
86+
_deployProxy(uninitProxy);
87+
88+
// Test that it reverts with the correct error
89+
vm.expectRevert(EIP7702Proxy.ProxyNotInitialized.selector);
90+
address(uninitProxy).call(arbitraryCalldata);
91+
92+
// Also verify the low-level call fails
93+
(bool success, ) = address(uninitProxy).call(arbitraryCalldata);
94+
assertFalse(success, "Low-level call should fail");
95+
}
96+
97+
function test_continues_delegating_afterUpgrade() public {
98+
// Setup will have already initialized the proxy with initial implementation and an owner
99+
100+
// Deploy a new implementation
101+
MockImplementation newImplementation = new MockImplementation();
102+
103+
// Upgrade to the new implementation
104+
vm.prank(_newOwner);
105+
MockImplementation(_eoa).upgradeToAndCall(
106+
address(newImplementation),
107+
""
108+
);
109+
110+
// Verify the implementation was changed
111+
assertEq(
112+
_getERC1967Implementation(_eoa),
113+
address(newImplementation),
114+
"Implementation should be updated"
115+
);
116+
117+
// Try to make a call through the proxy
118+
vm.prank(_newOwner);
119+
MockImplementation(_eoa).mockFunction();
120+
121+
// Verify the call succeeded
122+
assertTrue(
123+
MockImplementation(_eoa).mockFunctionCalled(),
124+
"Should be able to call through proxy after upgrade"
125+
);
126+
}
71127
}

test/EIP7702Proxy/initialize.t.sol

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {EIP7702ProxyBase} from "../base/EIP7702ProxyBase.sol";
55
import {EIP7702Proxy} from "../../src/EIP7702Proxy.sol";
66
import {MockImplementation, RevertingInitializerMockImplementation} from "../mocks/MockImplementation.sol";
77
import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
8+
import {ERC1967Utils} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol";
89

910
contract InitializeTest is EIP7702ProxyBase {
1011
function test_succeeds_withValidSignatureAndArgs(address newOwner) public {
@@ -157,4 +158,70 @@ contract InitializeTest is EIP7702ProxyBase {
157158
vm.expectRevert(EIP7702Proxy.ZeroValueConstructorArguments.selector);
158159
new EIP7702Proxy(address(_implementation), bytes4(0));
159160
}
161+
162+
function test_succeeds_whenImplementationSlotAlreadySetToDifferentAddress(
163+
address mockPreviousImpl,
164+
address newOwner,
165+
uint128 uninitProxyPk
166+
) public {
167+
vm.assume(mockPreviousImpl != address(0));
168+
vm.assume(mockPreviousImpl != address(_implementation));
169+
vm.assume(mockPreviousImpl != address(_eoa));
170+
vm.assume(newOwner != address(0));
171+
vm.assume(newOwner != mockPreviousImpl);
172+
vm.assume(newOwner != _eoa);
173+
assumeNotPrecompile(mockPreviousImpl);
174+
assumeNotPrecompile(newOwner);
175+
vm.assume(uninitProxyPk != 0);
176+
vm.assume(uninitProxyPk != _EOA_PRIVATE_KEY);
177+
178+
// Derive address for uninitProxy from private key
179+
address payable uninitProxy = payable(vm.addr(uninitProxyPk));
180+
181+
// Deploy proxy template and etch its code at the target address
182+
EIP7702Proxy proxyTemplate = new EIP7702Proxy(
183+
address(_implementation),
184+
_initSelector
185+
);
186+
bytes memory proxyCode = address(proxyTemplate).code;
187+
vm.etch(uninitProxy, proxyCode);
188+
189+
// Set the implementation slot to some other address, simulating a previous implementation
190+
vm.store(
191+
uninitProxy,
192+
ERC1967Utils.IMPLEMENTATION_SLOT,
193+
bytes32(uint256(uint160(mockPreviousImpl)))
194+
);
195+
196+
// Verify implementation slot is set to the previous implementation
197+
assertEq(
198+
_getERC1967Implementation(uninitProxy),
199+
mockPreviousImpl,
200+
"Implementation slot should be set to previous implementation"
201+
);
202+
203+
// Initialize the proxy
204+
bytes memory initArgs = _createInitArgs(_newOwner);
205+
bytes32 initHash = keccak256(
206+
abi.encode(address(proxyTemplate), initArgs)
207+
);
208+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(uninitProxyPk, initHash);
209+
bytes memory signature = abi.encodePacked(r, s, v);
210+
211+
EIP7702Proxy(uninitProxy).initialize(initArgs, signature);
212+
213+
// Verify implementation slot was changed to the correct implementation
214+
assertEq(
215+
_getERC1967Implementation(uninitProxy),
216+
address(_implementation),
217+
"Implementation slot should be set to correct implementation"
218+
);
219+
220+
// Verify we can make calls through the proxy now
221+
assertEq(
222+
MockImplementation(payable(uninitProxy)).owner(),
223+
_newOwner,
224+
"Should be able to call through proxy after initialization"
225+
);
226+
}
160227
}

test/EIP7702Proxy/isValidSignature.t.sol

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ contract FailingImplementationTest is IsValidSignatureTestBase {
114114
);
115115
assertEq(result, ERC1271_FAIL_VALUE, "Should reject empty signature");
116116
}
117+
118+
function test_reverts_whenCalledBeforeInitialization() public {
119+
// Deploy a fresh proxy without initializing
120+
address payable uninitProxy = payable(makeAddr("uninitProxy"));
121+
_deployProxy(uninitProxy);
122+
123+
// Try to call isValidSignature
124+
bytes32 hash = keccak256("test message");
125+
bytes memory signature = new bytes(65);
126+
127+
vm.expectRevert(EIP7702Proxy.ProxyNotInitialized.selector);
128+
EIP7702Proxy(uninitProxy).isValidSignature(hash, signature);
129+
}
117130
}
118131

119132
/**

0 commit comments

Comments
 (0)