Skip to content

Commit

Permalink
✨ EIP7702Proxy (#1342)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vectorized authored Feb 5, 2025
1 parent 827d103 commit b904efc
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The Solidity smart contracts are located in the `src` directory.

```ml
accounts
├─ EIP7702Proxy — "Relay proxy for EIP7702 delegations"
├─ ERC1271 — "ERC1271 mixin with nested EIP-712 approach"
├─ ERC4337 — "Simple ERC4337 account implementation"
├─ ERC4337Factory — "Simple ERC4337 account factory implementation"
Expand Down
53 changes: 53 additions & 0 deletions docs/accounts/eip7702proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# EIP7702Proxy

Relay proxy for EIP7702 delegations.


<b>Note:</b>

This relay proxy is useful for upgradeable EIP7702 accounts
without the need for redelegation.

EOA -> EIP7702Proxy (relay) -> EIP7702 account implementation.

This relay proxy also allows for correctly revealing the
"Read as Proxy" and "Write as Proxy" tabs on Etherscan.

This proxy can only be used by a EIP7702 authority.
If any regular contract uses this proxy, it will not work.



<!-- customintro:start --><!-- customintro:end -->

## Immutables

### __self

```solidity
uint256 internal immutable __self = uint256(uint160(address(this)))
```

For allowing the differentiation of the EOA and the proxy itself.

## Storage

### _ERC1967_IMPLEMENTATION_SLOT

```solidity
bytes32 internal constant _ERC1967_IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
```

The ERC-1967 storage slot for the implementation in the proxy.
`uint256(keccak256("eip1967.proxy.implementation")) - 1`.

### _ERC1967_ADMIN_SLOT

```solidity
bytes32 internal constant _ERC1967_ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
```

The ERC-1967 storage slot for the admin in the proxy.
`uint256(keccak256("eip1967.proxy.admin")) - 1`.
1 change: 1 addition & 0 deletions src/Milady.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./accounts/EIP7702Proxy.sol";
import "./accounts/ERC1271.sol";
import "./accounts/ERC4337.sol";
import "./accounts/ERC4337Factory.sol";
Expand Down
117 changes: 117 additions & 0 deletions src/accounts/EIP7702Proxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @notice Relay proxy for EIP7702 delegations.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/EIP7702Proxy.sol)
///
/// @dev Note: This relay proxy is useful for upgradeable EIP7702 accounts
/// without the need for redelegation.
///
/// EOA -> EIP7702Proxy (relay) -> EIP7702 account implementation.
///
/// This relay proxy also allows for correctly revealing the
/// "Read as Proxy" and "Write as Proxy" tabs on Etherscan.
///
/// This proxy can only be used by a EIP7702 authority.
/// If any regular contract uses this proxy, it will not work.
contract EIP7702Proxy {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* IMMUTABLES */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev For allowing the differentiation of the EOA and the proxy itself.
uint256 internal immutable __self = uint256(uint160(address(this)));

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev The ERC-1967 storage slot for the implementation in the proxy.
/// `uint256(keccak256("eip1967.proxy.implementation")) - 1`.
bytes32 internal constant _ERC1967_IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

/// @dev The ERC-1967 storage slot for the admin in the proxy.
/// `uint256(keccak256("eip1967.proxy.admin")) - 1`.
bytes32 internal constant _ERC1967_ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTRUCTOR */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

constructor(address initialImplementation, address initialAdmin) payable {
assembly {
sstore(_ERC1967_IMPLEMENTATION_SLOT, shr(96, shl(96, initialImplementation)))
sstore(_ERC1967_ADMIN_SLOT, shr(96, shl(96, initialAdmin)))
}
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* FALLBACK */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

fallback() external payable virtual {
uint256 s = __self;
assembly {
// Workflow for calling on the proxy itself.
// We cannot put these functions in the public ABI as this proxy must
// fully forward all the calldata from EOAs pointing to this proxy.
if eq(address(), s) {
let fnSel := shr(224, calldataload(0x00))
// `implementation()`.
if eq(0x5c60da1b, fnSel) {
mstore(0x00, sload(_ERC1967_IMPLEMENTATION_SLOT))
return(0x00, 0x20)
}
let admin := sload(_ERC1967_ADMIN_SLOT)
// `admin()`.
if eq(0xf851a440, fnSel) {
mstore(0x00, admin)
return(0x00, 0x20)
}
// Admin workflow.
if eq(caller(), admin) {
let addr := shr(96, shl(96, calldataload(0x04)))
// `changeAdmin(address)`.
if eq(0x8f283970, fnSel) {
sstore(_ERC1967_ADMIN_SLOT, addr)
mstore(0x00, 1)
return(0x00, 0x20) // Store and return `true`.
}
// `upgrade(address)`.
if eq(0x0900f010, fnSel) {
sstore(_ERC1967_IMPLEMENTATION_SLOT, addr)
mstore(0x00, 1)
return(0x00, 0x20) // Store and return `true`.
}
// For minimalism, we shall skip events and calldata bounds checks.
// We don't need to forward any data to the new implementation.
// This "proxy" is actually close to an upgradeable beacon.
}
revert(returndatasize(), 0x00)
}
// Workflow for the EIP7702 authority (i.e. the EOA).
// Copy the delegation from the EIP7702 bytecode.
extcodecopy(address(), 0x20, 0x00, 0x20) // Out-of-bounds bytes copied are zero.
mstore(0x00, 0x5c60da1b) // `implementation()`.
// Require that the bytecode is less than 24 bytes and begins with the expected prefix.
if iszero(
and( // Any dirty upper 96 bits of the target address is ignored in `staticcall`.
staticcall(gas(), mload(0x17), 0x1c, 0x04, 0x00, 0x20),
and(eq(0xef0100, shr(232, mload(0x20))), lt(extcodesize(address()), 24))
)
) { revert(returndatasize(), 0x00) }
// As the authority's storage may be polluted by previous delegations,
// we should always fetch the latest implementation from the proxy.
let implementation := mload(0x00)
calldatacopy(0x00, 0x00, calldatasize()) // Forward calldata into the delegatecall.
if iszero(delegatecall(gas(), implementation, 0x00, calldatasize(), 0x00, 0x00)) {
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
}
}
102 changes: 102 additions & 0 deletions test/EIP7702Proxy.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./utils/SoladyTest.sol";
import {EIP7702Proxy} from "../src/accounts/EIP7702Proxy.sol";

interface IEIP7702ProxyWithAdminABI {
function implementation() external view returns (address);
function admin() external view returns (address);
function changeAdmin(address) external returns (bool);
function upgrade(address) external returns (bool);
function bad() external;
}

contract EIP7702ProxyTest is SoladyTest {
error CustomError(uint256 currentValue);

uint256 public value;

bytes32 internal constant _ERC1967_IMPLEMENTATION_SLOT =
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

bytes32 internal constant _ERC1967_ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

function setValue(uint256 value_) public {
value = value_;
}

function revertWithError() public view {
revert CustomError(value);
}

function _checkBehavesLikeProxy(address instance) internal {
assertTrue(instance != address(0));

uint256 v = _random();
uint256 thisValue = this.value();
if (thisValue == v) {
v ^= 1;
}
EIP7702ProxyTest(instance).setValue(v);
assertEq(v, EIP7702ProxyTest(instance).value());
// assertEq(thisValue, this.value());
// vm.expectRevert(abi.encodeWithSelector(CustomError.selector, v));
// EIP7702ProxyTest(instance).revertWithError();
}

function testEIP7702Proxy(bytes32) public {
address admin = _randomUniqueHashedAddress();
IEIP7702ProxyWithAdminABI eip7702Proxy =
IEIP7702ProxyWithAdminABI(address(new EIP7702Proxy(address(this), admin)));
assertEq(eip7702Proxy.admin(), admin);
assertEq(eip7702Proxy.implementation(), address(this));

if (_randomChance(32)) {
address newAdmin = _randomUniqueHashedAddress();
vm.startPrank(admin);
eip7702Proxy.changeAdmin(newAdmin);
assertEq(eip7702Proxy.admin(), newAdmin);
vm.stopPrank();
admin = newAdmin;

vm.startPrank(_randomUniqueHashedAddress());
vm.expectRevert();
eip7702Proxy.changeAdmin(newAdmin);
vm.stopPrank();
}

if (_randomChance(32)) {
address newImplementation = _randomUniqueHashedAddress();
vm.startPrank(admin);
eip7702Proxy.upgrade(newImplementation);
assertEq(eip7702Proxy.implementation(), newImplementation);
eip7702Proxy.upgrade(address(this));
assertEq(eip7702Proxy.implementation(), address(this));
vm.stopPrank();
}

if (_randomChance(32)) {
vm.startPrank(admin);
vm.expectRevert();
eip7702Proxy.bad();
vm.stopPrank();
}

address authority = _randomUniqueHashedAddress();
vm.etch(authority, abi.encodePacked(hex"ef0100", address(eip7702Proxy)));

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

emit LogAddress("authority", authority);
emit LogAddress("proxy", address(eip7702Proxy));
emit LogAddress("address(this)", address(this));

_checkBehavesLikeProxy(authority);
}
}

0 comments on commit b904efc

Please sign in to comment.