Skip to content

Commit 9b861ab

Browse files
authored
⚡️ Optimize EIP7702Proxy for zero address default admin (#1368)
1 parent 17c333d commit 9b861ab

2 files changed

Lines changed: 108 additions & 33 deletions

File tree

src/accounts/EIP7702Proxy.sol

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ contract EIP7702Proxy {
1919
/// @dev For allowing the differentiation of the EOA and the proxy itself.
2020
uint256 internal immutable __self = uint256(uint160(address(this)));
2121

22+
/// @dev The default implementation. Provided for optimization.
23+
/// Set if the `initialAdmin == address(0) && initialImplementation != address(0)`.
24+
uint256 internal immutable _defaultImplementation;
25+
2226
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
2327
/* STORAGE */
2428
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -45,11 +49,18 @@ contract EIP7702Proxy {
4549
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
4650

4751
constructor(address initialImplementation, address initialAdmin) payable {
52+
uint256 defaultImplementation;
4853
/// @solidity memory-safe-assembly
4954
assembly {
50-
sstore(_ERC1967_IMPLEMENTATION_SLOT, shr(96, shl(96, initialImplementation)))
51-
sstore(_ERC1967_ADMIN_SLOT, shr(96, shl(96, initialAdmin)))
55+
let implementation := shr(96, shl(96, initialImplementation))
56+
let admin := shr(96, shl(96, initialAdmin))
57+
// We will store the implementation in the storage regardless,
58+
// to aid proxy detection on block explorers.
59+
sstore(_ERC1967_IMPLEMENTATION_SLOT, implementation)
60+
sstore(_ERC1967_ADMIN_SLOT, admin)
61+
defaultImplementation := mul(lt(admin, iszero(iszero(implementation))), implementation)
5262
}
63+
_defaultImplementation = defaultImplementation;
5364
}
5465

5566
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
@@ -58,6 +69,7 @@ contract EIP7702Proxy {
5869

5970
fallback() external payable virtual {
6071
uint256 s = __self;
72+
uint256 defaultImplementation = _defaultImplementation;
6173
/// @solidity memory-safe-assembly
6274
assembly {
6375
mstore(0x40, returndatasize()) // Optimization trick to change `6040608052` into `3d604052`.
@@ -106,35 +118,41 @@ contract EIP7702Proxy {
106118
// Workflow for the EIP7702 authority (i.e. the EOA).
107119
let impl := sload(_ERC1967_IMPLEMENTATION_SLOT) // The preferred implementation on the EOA.
108120
calldatacopy(0x00, 0x00, calldatasize()) // Copy the calldata for the delegatecall.
109-
// If the preferred implementation is `address(0)`, perform the initialization workflow.
121+
// If the preferred implementation is `address(0)`.
110122
if iszero(shl(96, impl)) {
111-
if iszero(
112-
and( // The arguments of `and` are evaluated from right to left.
113-
delegatecall(
114-
gas(), mload(calldatasize()), 0x00, calldatasize(), calldatasize(), 0x00
115-
),
116-
// Fetch the implementation from the proxy.
117-
staticcall(gas(), s, calldatasize(), 0x00, calldatasize(), 0x20)
118-
)
119-
) {
123+
// If `defaultImplementation` is `address(0)`, perform the initialization workflow.
124+
if iszero(defaultImplementation) {
125+
if iszero(
126+
and( // The arguments of `and` are evaluated from right to left.
127+
delegatecall(
128+
gas(), mload(calldatasize()), 0x00, calldatasize(), 0x00, 0x00
129+
),
130+
// Fetch the implementation from the proxy.
131+
staticcall(gas(), s, calldatasize(), 0x00, calldatasize(), 0x20)
132+
)
133+
) {
134+
returndatacopy(0x00, 0x00, returndatasize())
135+
revert(0x00, returndatasize())
136+
}
137+
// Because we cannot reliably and efficiently tell if the call is made
138+
// via staticcall or call, we shall ask the delegation to make a proxy delegation
139+
// initialization request to signal that we should initialize the storage slot with
140+
// the actual implementation. This also gives flexibility on whether to let the
141+
// proxy auto-upgrade, or let the authority manually upgrade (via 7702 or passkey).
142+
// A non-zero value in the transient storage denotes a initialization request.
143+
if tload(_EIP7702_PROXY_DELEGATION_INITIALIZATION_REQUEST_SLOT) {
144+
let implSlot := _ERC1967_IMPLEMENTATION_SLOT
145+
// The `implementation` is still at `calldatasize()` in memory.
146+
// Preserve the upper 96 bits when updating in case they are used for some stuff.
147+
sstore(
148+
implSlot, or(shl(160, shr(160, sload(implSlot))), mload(calldatasize()))
149+
)
150+
tstore(_EIP7702_PROXY_DELEGATION_INITIALIZATION_REQUEST_SLOT, 0) // Clear.
151+
}
120152
returndatacopy(0x00, 0x00, returndatasize())
121-
revert(0x00, returndatasize())
153+
return(0x00, returndatasize())
122154
}
123-
// Because we cannot reliably and efficiently tell if the call is made
124-
// via staticcall or call, we shall ask the delegation to make a proxy delegation
125-
// initialization request to signal that we should initialize the storage slot with
126-
// the actual implementation. This also gives flexibility on whether to let the
127-
// proxy auto-upgrade, or let the authority manually upgrade (via 7702 or passkey).
128-
// A non-zero value in the transient storage denotes a initialization request.
129-
if tload(_EIP7702_PROXY_DELEGATION_INITIALIZATION_REQUEST_SLOT) {
130-
let implSlot := _ERC1967_IMPLEMENTATION_SLOT
131-
// The `implementation` is still at `calldatasize()` in memory.
132-
// Preserve the upper 96 bits when updating in case they are used for some stuff.
133-
sstore(implSlot, or(shl(160, shr(160, sload(implSlot))), mload(calldatasize())))
134-
tstore(_EIP7702_PROXY_DELEGATION_INITIALIZATION_REQUEST_SLOT, 0) // Clear.
135-
}
136-
returndatacopy(0x00, 0x00, returndatasize())
137-
return(0x00, returndatasize())
155+
impl := defaultImplementation
138156
}
139157
// Otherwise, just delegatecall and bubble up the results without initialization.
140158
if iszero(delegatecall(gas(), impl, 0x00, calldatasize(), calldatasize(), 0x00)) {

test/EIP7702Proxy.t.sol

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ contract Implementation2 {
2424
value = value_;
2525
LibEIP7702.requestProxyDelegationInitialization();
2626
}
27+
28+
function upgradeProxyDelegation(address newImplementation) public {
29+
LibEIP7702.upgradeProxyDelegation(newImplementation);
30+
}
2731
}
2832

2933
contract EIP7702ProxyTest is SoladyTest {
@@ -59,7 +63,7 @@ contract EIP7702ProxyTest is SoladyTest {
5963

6064
assertEq(EIP7702ProxyTest(instance).version(), 1);
6165

62-
uint256 v = _random();
66+
uint256 v = _randomUniform();
6367
uint256 thisValue = this.value();
6468
if (thisValue == v) {
6569
v ^= 1;
@@ -72,6 +76,10 @@ contract EIP7702ProxyTest is SoladyTest {
7276
EIP7702ProxyTest(instance).revertWithError();
7377
}
7478

79+
function upgradeProxyDelegation(address newImplementation) public {
80+
LibEIP7702.upgradeProxyDelegation(newImplementation);
81+
}
82+
7583
function testEIP7702Proxy(bytes32, bool f) public {
7684
vm.pauseGasMetering();
7785

@@ -121,6 +129,8 @@ contract EIP7702ProxyTest is SoladyTest {
121129
vm.stopPrank();
122130
}
123131

132+
// Generate some random value that has the lower 160 bits zeroized,
133+
// to test if the proxy can handle dirty bits.
124134
uint256 r = (_random() >> 160) << 160;
125135
vm.store(address(this), _ERC1967_IMPLEMENTATION_SLOT, bytes32(r));
126136

@@ -142,20 +152,20 @@ contract EIP7702ProxyTest is SoladyTest {
142152
emit LogAddress("proxy", address(eip7702Proxy));
143153
emit LogAddress("address(this)", address(this));
144154

145-
vm.resumeGasMetering();
146-
147155
// Runtime REVM detection.
148156
// If this check fails, then we are not ready to test it in CI.
149157
// The exact length is 23 at the time of writing as of the EIP7702 spec,
150158
// but we give our heuristic some leeway.
151159
if (authority.code.length > 0x20) return;
152160

153-
if (!f) assertEq(LibEIP7702.delegation(authority), address(eip7702Proxy));
161+
vm.resumeGasMetering();
154162

155163
_checkBehavesLikeProxy(authority);
156164

157165
vm.pauseGasMetering();
158166

167+
if (!f) assertEq(LibEIP7702.delegation(authority), address(eip7702Proxy));
168+
159169
// Check that upgrading the proxy won't cause the authority's implementation to change.
160170
if (!f && _randomChance(2)) {
161171
vm.startPrank(admin);
@@ -164,6 +174,12 @@ contract EIP7702ProxyTest is SoladyTest {
164174

165175
_checkBehavesLikeProxy(authority);
166176

177+
if (!f && _randomChance(2)) {
178+
EIP7702ProxyTest(authority).upgradeProxyDelegation(address(new Implementation2()));
179+
assertEq(Implementation2(authority).version(), 2);
180+
Implementation2(authority).upgradeProxyDelegation(address(this));
181+
}
182+
167183
if (!f && _randomChance(2) && (r >> 160) > 0) {
168184
vm.startPrank(admin);
169185
eip7702Proxy.upgrade(address(new Implementation2()));
@@ -187,6 +203,47 @@ contract EIP7702ProxyTest is SoladyTest {
187203
}
188204

189205
function testEIP7702Proxy() public {
190-
this.testEIP7702Proxy(0, true);
206+
testEIP7702Proxy(0, true);
207+
}
208+
209+
function testEIP7702ProxyWithDefaultImplementation(bytes32, bool f) public {
210+
vm.pauseGasMetering();
211+
212+
IEIP7702ProxyWithAdminABI eip7702Proxy =
213+
IEIP7702ProxyWithAdminABI(address(new EIP7702Proxy(address(this), address(0))));
214+
215+
assertEq(eip7702Proxy.admin(), address(0));
216+
assertEq(LibEIP7702.proxyAdmin(address(eip7702Proxy)), address(0));
217+
assertEq(eip7702Proxy.implementation(), address(this));
218+
assertEq(LibEIP7702.proxyImplementation(address(eip7702Proxy)), address(this));
219+
220+
address authority = _randomUniqueHashedAddress();
221+
assertEq(LibEIP7702.delegation(authority), address(0));
222+
vm.etch(authority, abi.encodePacked(hex"ef0100", address(eip7702Proxy)));
223+
224+
// Generate some random value that has the lower 160 bits zeroized,
225+
// to test if the proxy can handle dirty bits.
226+
uint256 r = (_random() >> 160) << 160;
227+
vm.store(authority, _ERC1967_IMPLEMENTATION_SLOT, bytes32(r));
228+
229+
if (authority.code.length > 0x20) return;
230+
231+
vm.resumeGasMetering();
232+
233+
_checkBehavesLikeProxy(authority);
234+
235+
vm.pauseGasMetering();
236+
237+
if (!f && _randomChance(2)) {
238+
EIP7702ProxyTest(authority).upgradeProxyDelegation(address(new Implementation2()));
239+
assertEq(Implementation2(authority).version(), 2);
240+
Implementation2(authority).upgradeProxyDelegation(address(this));
241+
}
242+
243+
vm.resumeGasMetering();
244+
}
245+
246+
function testEIP7702ProxyWithDefaultImplementation() public {
247+
testEIP7702ProxyWithDefaultImplementation(0, true);
191248
}
192249
}

0 commit comments

Comments
 (0)