Skip to content

Commit e02d844

Browse files
committed
initial prototype for custom impl slot
1 parent 9c0c9e6 commit e02d844

File tree

2 files changed

+156
-17
lines changed

2 files changed

+156
-17
lines changed

src/CustomUpgradeable.sol

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import {StorageSlot} from "openzeppelin-contracts/contracts/utils/StorageSlot.sol";
5+
import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol";
6+
7+
/**
8+
* @dev Custom upgradeability mechanism similar to UUPS but using a custom storage slot
9+
* that is computed during construction to avoid collisions in EIP-7702 context.
10+
*
11+
* The key difference from standard UUPS is the use of a deployment-specific
12+
* storage slot instead of the standard ERC-1967 slot, making it safe to use
13+
* in EIP-7702 where multiple delegates might share storage space.
14+
*/
15+
abstract contract CustomUpgradeable {
16+
/// @notice Emitted when the implementation is upgraded
17+
event Upgraded(address indexed implementation);
18+
19+
/// @notice Emitted when a call is made from an unauthorized context (direct calls or invalid delegatecalls)
20+
error UnauthorizedCallContext();
21+
22+
/// @dev For checking if the context is a delegate call
23+
address private immutable __self = address(this);
24+
25+
/// @dev The storage slot used for the implementation
26+
bytes32 private immutable IMPLEMENTATION_SLOT;
27+
28+
/**
29+
* @dev Initializes the implementation slot using implementation address and initializer
30+
* to ensure uniqueness across different deployments.
31+
*
32+
* The storage slot is computed by hashing:
33+
* - A namespace string ("eip7702.proxy.implementation")
34+
* - The initial implementation address
35+
* - The initializer function selector
36+
*
37+
* This ensures unique slots for different implementations of the proxy,
38+
* preventing storage collisions in EIP-7702 context.
39+
*
40+
* @param implementation The initial implementation address
41+
* @param initializer The function selector used for initialization
42+
*/
43+
constructor(address implementation, bytes4 initializer) {
44+
IMPLEMENTATION_SLOT = keccak256(
45+
abi.encode(
46+
"eip7702.proxy.implementation",
47+
implementation,
48+
initializer
49+
)
50+
);
51+
}
52+
53+
/**
54+
* @dev Upgrade the implementation and optionally execute a function.
55+
* Must be implemented by the proxy to define upgrade authorization rules.
56+
*
57+
* @param newImplementation Address of the new implementation
58+
* @param data Optional function call data to execute after upgrade
59+
* @param signature The signature authorizing the upgrade
60+
*/
61+
function upgradeToAndCall(
62+
address newImplementation,
63+
bytes memory data,
64+
bytes calldata signature
65+
) public payable virtual;
66+
67+
/**
68+
* @dev Returns the current implementation address from the custom storage slot
69+
* computed during construction.
70+
*/
71+
function _getImplementation() internal view returns (address) {
72+
return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value;
73+
}
74+
75+
/**
76+
* @dev Updates the implementation address in the custom storage slot.
77+
* This function should only be called as part of an upgrade process.
78+
*/
79+
function _setImplementation(address newImplementation) internal {
80+
StorageSlot
81+
.getAddressSlot(IMPLEMENTATION_SLOT)
82+
.value = newImplementation;
83+
}
84+
85+
/**
86+
* @dev Ensures the call is through a proxy by checking:
87+
* 1. We are in a delegatecall context (address(this) != __self)
88+
* 2. The caller is the current implementation (_getImplementation() == __self)
89+
*
90+
* This prevents direct calls to the proxy and ensures upgrades are properly
91+
* authorized through the implementation contract.
92+
*/
93+
modifier onlyProxy() {
94+
if (address(this) == __self || _getImplementation() != __self) {
95+
revert UnauthorizedCallContext();
96+
}
97+
_;
98+
}
99+
}

src/EIP7702Proxy.sol

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,58 @@
22
pragma solidity ^0.8.23;
33

44
import {Proxy} from "openzeppelin-contracts/contracts/proxy/Proxy.sol";
5-
import {ERC1967Utils} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol";
65
import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
76
import {StorageSlot} from "openzeppelin-contracts/contracts/utils/StorageSlot.sol";
7+
import {CustomUpgradeable} from "./CustomUpgradeable.sol";
8+
import {Address} from "openzeppelin-contracts/contracts/utils/Address.sol";
89

910
/// @title EIP7702Proxy
1011
/// @notice Proxy contract designed for EIP-7702 smart accounts
11-
/// @dev Implements ERC-1967 with an initial implementation address and guarded initializer function
12+
/// @dev Implements a custom upgradeable proxy pattern with a unique storage slot
13+
/// to avoid collisions with other delegates in an EIP-7702 context.
1214
/// @author Coinbase (https://github.com/base-org/eip-7702-proxy)
13-
contract EIP7702Proxy is Proxy {
14-
/// @notice ERC1271 interface constants
15+
contract EIP7702Proxy is Proxy, CustomUpgradeable {
16+
/// @notice ERC1271 interface constants for signature validation
1517
bytes4 internal constant ERC1271_MAGIC_VALUE = 0x1626ba7e;
1618
bytes4 internal constant ERC1271_FAIL_VALUE = 0xffffffff;
1719

18-
/// @notice Address of this proxy contract delegate
20+
/// @notice The address of this proxy contract
1921
address immutable proxy;
2022

21-
/// @notice Initial implementation address set during construction
23+
/// @notice The initial implementation address
2224
address immutable initialImplementation;
2325

24-
/// @notice Function selector on the implementation that is guarded from direct calls
26+
/// @notice The function selector that is protected from direct calls
2527
bytes4 immutable guardedInitializer;
2628

27-
/// @dev Storage slot with the initialized flag, calculated via ERC-7201
29+
/// @dev Storage slot for the initialized flag, calculated via ERC-7201
2830
bytes32 internal constant INITIALIZED_SLOT =
2931
keccak256(
3032
abi.encode(uint256(keccak256("EIP7702Proxy.initialized")) - 1)
3133
) & ~bytes32(uint256(0xff));
3234

33-
/// @notice Emitted when the initialization signature is invalid
35+
/// @notice Emitted when a signature fails validation during initialization
3436
error InvalidSignature();
3537

36-
/// @notice Emitted when the `guardedInitializer` is called
38+
/// @notice Emitted when attempting to call the guarded initializer directly
3739
error InvalidInitializer();
3840

39-
/// @notice Emitted when trying to delegate before initialization
41+
/// @notice Emitted when trying to use the proxy before initialization
4042
error ProxyNotInitialized();
4143

42-
/// @notice Emitted when constructor arguments are zero
44+
/// @notice Emitted when constructor receives zero address or selector
4345
error ZeroValueConstructorArguments();
4446

47+
/// @notice Emitted when an operation is not authorized
48+
error Unauthorized();
49+
4550
/// @notice Initializes the proxy with an initial implementation and guarded initializer
4651
/// @param implementation The initial implementation address
4752
/// @param initializer The selector of the `guardedInitializer` function
48-
constructor(address implementation, bytes4 initializer) {
53+
constructor(
54+
address implementation,
55+
bytes4 initializer
56+
) CustomUpgradeable(implementation, initializer) {
4957
if (implementation == address(0))
5058
revert ZeroValueConstructorArguments();
5159
if (initializer == bytes4(0)) revert ZeroValueConstructorArguments();
@@ -81,11 +89,19 @@ contract EIP7702Proxy is Proxy {
8189
// Set initialized flag before upgrading
8290
StorageSlot.getBooleanSlot(INITIALIZED_SLOT).value = true;
8391

84-
// Set the ERC-1967 implementation slot, emit Upgraded event, call the initializer
85-
ERC1967Utils.upgradeToAndCall(
86-
initialImplementation,
92+
// Update to use our custom implementation storage instead of ERC1967Utils
93+
_setImplementation(initialImplementation);
94+
95+
(bool success, ) = initialImplementation.delegatecall(
8796
abi.encodePacked(guardedInitializer, args)
8897
);
98+
if (!success) {
99+
assembly {
100+
let ptr := mload(0x40)
101+
returndatacopy(ptr, 0, returndatasize())
102+
revert(ptr, returndatasize())
103+
}
104+
}
89105
}
90106

91107
/// @notice Handles ERC-1271 signature validation by enforcing a final `ecrecover` check if signatures fail `isValidSignature` check
@@ -142,6 +158,30 @@ contract EIP7702Proxy is Proxy {
142158
}
143159

144160
function _implementation() internal view override returns (address) {
145-
return ERC1967Utils.getImplementation();
161+
return _getImplementation();
146162
}
163+
164+
function upgradeToAndCall(
165+
address newImplementation,
166+
bytes memory data,
167+
bytes calldata signature
168+
) public payable override {
169+
// Create hash of upgrade data
170+
bytes32 hash = keccak256(abi.encode(proxy, newImplementation, data));
171+
172+
// Use our existing isValidSignature logic to validate the upgrade
173+
if (this.isValidSignature(hash, signature) != ERC1271_MAGIC_VALUE) {
174+
revert Unauthorized();
175+
}
176+
177+
// If signature valid, proceed with upgrade
178+
_setImplementation(newImplementation);
179+
emit Upgraded(newImplementation);
180+
181+
if (data.length > 0) {
182+
Address.functionDelegateCall(newImplementation, data);
183+
}
184+
}
185+
186+
receive() external payable {}
147187
}

0 commit comments

Comments
 (0)