Skip to content

Commit

Permalink
✨ Improve EIP7702Proxy and add LibEIP7702 (#1355)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vectorized authored Feb 18, 2025
1 parent 29b5dbe commit fe8ca07
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 24 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ accounts
├─ ERC6551 — "Simple ERC6551 account implementation"
├─ ERC6551Proxy — "Relay proxy for upgradeable ERC6551 accounts"
├─ ERC7821 — "Minimal batch executor mixin"
├─ LibEIP7702 — "Library for EIP7702 operations"
├─ LibERC6551 — "Library for interacting with ERC6551 accounts"
├─ LibERC7579 — "Library for handling ERC7579 mode and execution data"
├─ Receiver — "Receiver mixin for ETH and safe-transferred ERC721 and ERC1155 tokens"
Expand Down
4 changes: 2 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ auto_detect_solc = false
optimizer = true
optimizer_runs = 1_000
gas_limit = 100_000_000 # ETH is 30M, but we use a higher value.
skip = ["*/*Transient*", "*/ext/ithaca/*", "*/ext/zksync/*"]
skip = ["*/*7702*", "*/*Transient*", "*/ext/ithaca/*", "*/ext/zksync/*"]
fs_permissions = [{ access = "read", path = "./test/data"}]
remappings = [
"forge-std=test/utils/forge-std/"
]

[profile.pre_global_structs]
skip = ["*/g/*", "*/*Transient*", "*/ext/ithaca/*", "*/ext/zksync/*"]
skip = ["*/g/*", "*/*7702*", "*/*Transient*", "*/ext/ithaca/*", "*/ext/zksync/*"]

[profile.post_cancun]
evm_version = "cancun"
Expand Down
1 change: 0 additions & 1 deletion src/Milady.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// 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
57 changes: 42 additions & 15 deletions src/accounts/EIP7702Proxy.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
pragma solidity ^0.8.24;

/// @notice Relay proxy for EIP7702 delegations.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/EIP7702Proxy.sol)
Expand Down Expand Up @@ -33,6 +33,11 @@ contract EIP7702Proxy {
bytes32 internal constant _ERC1967_ADMIN_SLOT =
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

/// @dev The transient storage slot for requesting the proxy to initialize the implementation.
/// `uint256(keccak256("eip7702.proxy.delegation.initialization.request")) - 1`.
bytes32 internal constant _EIP7702_PROXY_DELEGATION_INITIALIZATION_REQUEST_SLOT =
0x94e11c6e41e7fb92cb8bb65e13fdfbd4eba8b831292a1a220f7915c78c7c078f;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTRUCTOR */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand Down Expand Up @@ -63,8 +68,8 @@ contract EIP7702Proxy {
return(calldatasize(), 0x20)
}
let fnSel := shr(224, calldataload(0x00))
// `implementation()`.
if eq(0x5c60da1b, fnSel) {
// `implementation()` or `eip7702ProxyImplementation()`.
if or(eq(0x5c60da1b, fnSel), eq(0x7dae87cb, fnSel)) {
if staticcall(gas(), address(), calldatasize(), 0x00, 0x00, 0x20) {
return(0x00, returndatasize())
}
Expand All @@ -77,7 +82,7 @@ contract EIP7702Proxy {
}
// Admin workflow.
if eq(caller(), admin) {
let addr := shr(96, shl(96, calldataload(0x04)))
let addr := shr(96, calldataload(0x10))
// `changeAdmin(address)`.
if eq(0x8f283970, fnSel) {
sstore(_ERC1967_ADMIN_SLOT, addr)
Expand All @@ -97,17 +102,39 @@ contract EIP7702Proxy {
revert(returndatasize(), 0x00)
}
// Workflow for the EIP7702 authority (i.e. the EOA).
// As the authority's storage may be polluted by previous delegations,
// we should always fetch the latest implementation from the proxy.
calldatacopy(0x00, 0x00, calldatasize()) // Forward calldata into the delegatecall.
if iszero(
and( // The arguments of `and` are evaluated from right to left.
delegatecall(
gas(), mload(calldatasize()), 0x00, calldatasize(), calldatasize(), 0x00
),
staticcall(gas(), s, calldatasize(), 0x00, calldatasize(), 0x20)
)
) {
let impl := sload(_ERC1967_IMPLEMENTATION_SLOT) // The preferred implementation on the EOA.
calldatacopy(0x00, 0x00, calldatasize()) // Copy the calldata for the delegatecall.
// If the EOA's implementation, perform the initialization workflow.
if iszero(shl(96, impl)) {
if iszero(
and( // The arguments of `and` are evaluated from right to left.
delegatecall(
gas(), mload(calldatasize()), 0x00, calldatasize(), calldatasize(), 0x00
),
// Fetch the implementation from the proxy.
staticcall(gas(), s, calldatasize(), 0x00, calldatasize(), 0x20)
)
) {
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
// Because we cannot reliably and efficiently tell if the call is made
// via staticcall or call, we shall ask the delegation to make a proxy delegation
// initialization request to signal that we should initialize the storage slot with
// the actual implementation. This also gives flexibility on whether to let the
// proxy auto-upgrade, or let the authority manually upgrade (via 7702 or passkey).
// A non-zero value in the transient storage denotes a initialization request.
if tload(_EIP7702_PROXY_DELEGATION_INITIALIZATION_REQUEST_SLOT) {
let implSlot := _ERC1967_IMPLEMENTATION_SLOT
// The `implementation` is still at `calldatasize()` in memory.
sstore(implSlot, or(shl(160, shr(160, sload(implSlot))), mload(calldatasize())))
tstore(_EIP7702_PROXY_DELEGATION_INITIALIZATION_REQUEST_SLOT, 0) // Clear.
}
returndatacopy(0x00, 0x00, returndatasize())
return(0x00, returndatasize())
}
// Otherwise, just delegatecall and bubble up the results without initialization.
if iszero(delegatecall(gas(), impl, 0x00, calldatasize(), calldatasize(), 0x00)) {
returndatacopy(0x00, 0x00, returndatasize())
revert(0x00, returndatasize())
}
Expand Down
147 changes: 147 additions & 0 deletions src/accounts/LibEIP7702.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @notice Library for EIP7702 operations.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/LibEIP7702.sol)
library LibEIP7702 {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CUSTOM ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev The proxy query has failed.
error ProxyQueryFailed();

/// @dev Failed to change the proxy admin.
error ChangeProxyAdminFailed();

/// @dev Failed to upgrade the proxy.
error UpgradeProxyFailed();

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

/// @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 transient storage slot for requesting the proxy to initialize the implementation.
/// `uint256(keccak256("eip7702.proxy.delegation.initialization.request")) - 1`.
bytes32 internal constant _EIP7702_PROXY_DELEGATION_INITIALIZATION_REQUEST_SLOT =
0x94e11c6e41e7fb92cb8bb65e13fdfbd4eba8b831292a1a220f7915c78c7c078f;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* AUTHORITY OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev Returns the delegation of the account.
/// If the account is not an EIP7702 authority, the `delegation` will be `address(0)`.
function delegation(address account) internal view returns (address result) {
/// @solidity memory-safe-assembly
assembly {
extcodecopy(account, 0x00, 0x00, 0x20)
// Note: Checking that it starts with hex"ef01" is the most general and futureproof.
// 7702 bytecode is `abi.encodePacked(hex"ef01", uint8(version), address(delegation))`.
result := mul(shr(96, mload(0x03)), eq(0xef01, shr(240, mload(0x00))))
}
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* PROXY OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev Returns the implementation of the proxy.
/// Assumes that the proxy is a proper EIP7702Proxy, if it exists.
function proxyImplementation(address proxy) internal view returns (address result) {
/// @solidity memory-safe-assembly
assembly {
// Although `implementation()` is supported, we'll use a less common
// function selector to avoid accidental collision with other delegations.
mstore(0x00, 0x7dae87cb) // `eip7702ProxyImplementation()`.
let t := staticcall(gas(), proxy, 0x1c, 0x04, 0x00, 0x20)
if iszero(and(gt(returndatasize(), 0x1f), t)) {
mstore(0x00, 0x26ec9b6a) // `ProxyQueryFailed()`.
revert(0x1c, 0x04)
}
result := mload(0x00)
}
}

/// @dev Returns the admin of the proxy.
/// Assumes that the proxy is a proper EIP7702Proxy, if it exists.
function proxyAdmin(address proxy) internal view returns (address result) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, 0xf851a440) // `admin()`.
let t := staticcall(gas(), proxy, 0x1c, 0x04, 0x00, 0x20)
if iszero(and(gt(returndatasize(), 0x1f), t)) {
mstore(0x00, 0x26ec9b6a) // `ProxyQueryFailed()`.
revert(0x1c, 0x04)
}
result := mload(0x00)
}
}

/// @dev Changes the admin on the proxy. The caller must be the admin.
/// Assumes that the proxy is a proper EIP7702Proxy, if it exists.
function changeProxyAdmin(address proxy, address newAdmin) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, 0x8f283970) // `changeAdmin(address)`.
mstore(0x20, shr(96, shl(96, newAdmin)))
if iszero(and(eq(mload(0x00), 1), call(gas(), proxy, 0, 0x1c, 0x24, 0x00, 0x20))) {
mstore(0x00, 0xc502e37e) // `ChangeProxyAdminFailed()`.
revert(0x1c, 0x04)
}
}
}

/// @dev Changes the implementation on the proxy. The caller must be the admin.
/// Assumes that the proxy is a proper EIP7702Proxy, if it exists.
function upgradeProxy(address proxy, address newImplementation) internal {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, 0x0900f010) // `upgrade(address)`.
mstore(0x20, shr(96, shl(96, newImplementation)))
if iszero(and(eq(mload(0x00), 1), call(gas(), proxy, 0, 0x1c, 0x24, 0x00, 0x20))) {
mstore(0x00, 0xc6edd882) // `UpgradeProxyFailed()`.
revert(0x1c, 0x04)
}
}
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* PROXY DELEGATION OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev Upgrades the implementation.
/// The new implementation will NOT be active until the next UserOp or transaction.
/// To "auto-upgrade" to the latest implementation on the proxy, pass in `address(0)` to reset
/// the implementation slot. This causes the proxy to use the latest default implementation,
/// which may be optionally reinitialized via `requestProxyDelegationInitialization()`.
/// This function is intended to be used on the authority of an EIP7702Proxy delegation.
/// The most intended usage pattern is to wrap this in an access-gated admin function.
function upgradeProxyDelegation(address newImplementation) internal {
/// @solidity memory-safe-assembly
assembly {
let s := ERC1967_IMPLEMENTATION_SLOT
mstore(0x00, sload(s))
mstore(0x0c, shl(96, newImplementation))
sstore(s, mload(0x00))
}
}

/// @dev Requests the implementation to be initialized to the latest implementation on the proxy.
/// This function is intended to be used on the authority of an EIP7702Proxy delegation.
/// The most intended usage pattern is to place it at the end of an `execute` function.
function requestProxyDelegationInitialization() internal {
/// @solidity memory-safe-assembly
assembly {
if iszero(shl(96, sload(ERC1967_IMPLEMENTATION_SLOT))) {
// Use a dedicated transient storage slot for better Swiss-cheese-model safety.
tstore(_EIP7702_PROXY_DELEGATION_INITIALIZATION_REQUEST_SLOT, address())
}
}
}
}
Loading

0 comments on commit fe8ca07

Please sign in to comment.