-
Notifications
You must be signed in to change notification settings - Fork 31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(v2): use solady's minimal ERC-1967 proxy and UUPSUpgradeable #35
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.4; | ||
|
||
/// @notice UUPS proxy mixin. | ||
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/UUPSUpgradeable.sol) | ||
/// @author Modified from OpenZeppelin | ||
/// (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/utils/UUPSUpgradeable.sol) | ||
/// | ||
/// Note: | ||
/// - This implementation is intended to be used with ERC1967 proxies. | ||
/// See: `LibClone.deployERC1967` and related functions. | ||
/// - This implementation is NOT compatible with legacy OpenZeppelin proxies | ||
/// which do not store the implementation at `_ERC1967_IMPLEMENTATION_SLOT`. | ||
abstract contract UUPSUpgradeable { | ||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* CUSTOM ERRORS */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @dev The upgrade failed. | ||
error UpgradeFailed(); | ||
|
||
/// @dev The call is from an unauthorized call context. | ||
error UnauthorizedCallContext(); | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* IMMUTABLES */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @dev For checking if the context is a delegate call. | ||
uint256 private immutable __self = uint256(uint160(address(this))); | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* EVENTS */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @dev Emitted when the proxy's implementation is upgraded. | ||
event Upgraded(address indexed implementation); | ||
|
||
/// @dev `keccak256(bytes("Upgraded(address)"))`. | ||
uint256 private constant _UPGRADED_EVENT_SIGNATURE = | ||
0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b; | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* 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; | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* UUPS OPERATIONS */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @dev Please override this function to check if `msg.sender` is authorized | ||
/// to upgrade the proxy to `newImplementation`, reverting if not. | ||
/// ``` | ||
/// function _authorizeUpgrade(address) internal override onlyOwner {} | ||
/// ``` | ||
function _authorizeUpgrade(address newImplementation) internal virtual; | ||
|
||
/// @dev Returns the storage slot used by the implementation, | ||
/// as specified in [ERC1822](https://eips.ethereum.org/EIPS/eip-1822). | ||
/// | ||
/// Note: The `notDelegated` modifier prevents accidental upgrades to | ||
/// an implementation that is a proxy contract. | ||
function proxiableUUID() public view virtual notDelegated returns (bytes32) { | ||
// This function must always return `_ERC1967_IMPLEMENTATION_SLOT` to comply with ERC1967. | ||
return _ERC1967_IMPLEMENTATION_SLOT; | ||
} | ||
|
||
/// @dev Upgrades the proxy's implementation to `newImplementation`. | ||
/// Emits a {Upgraded} event. | ||
/// | ||
/// Note: Passing in empty `data` skips the delegatecall to `newImplementation`. | ||
function upgradeToAndCall(address newImplementation, bytes calldata data) | ||
public | ||
payable | ||
virtual | ||
onlyProxy | ||
{ | ||
_authorizeUpgrade(newImplementation); | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
newImplementation := shr(96, shl(96, newImplementation)) // Clears upper 96 bits. | ||
mstore(0x01, 0x52d1902d) // `proxiableUUID()`. | ||
let s := _ERC1967_IMPLEMENTATION_SLOT | ||
// Check if `newImplementation` implements `proxiableUUID` correctly. | ||
if iszero(eq(mload(staticcall(gas(), newImplementation, 0x1d, 0x04, 0x01, 0x20)), s)) { | ||
mstore(0x01, 0x55299b49) // `UpgradeFailed()`. | ||
revert(0x1d, 0x04) | ||
} | ||
// Emit the {Upgraded} event. | ||
log2(codesize(), 0x00, _UPGRADED_EVENT_SIGNATURE, newImplementation) | ||
sstore(s, newImplementation) // Updates the implementation. | ||
|
||
// Perform a delegatecall to `newImplementation` if `data` is non-empty. | ||
if data.length { | ||
// Forwards the `data` to `newImplementation` via delegatecall. | ||
let m := mload(0x40) | ||
calldatacopy(m, data.offset, data.length) | ||
if iszero(delegatecall(gas(), newImplementation, m, data.length, codesize(), 0x00)) | ||
{ | ||
// Bubble up the revert if the call reverts. | ||
returndatacopy(m, 0x00, returndatasize()) | ||
revert(m, returndatasize()) | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// @dev Requires that the execution is performed through a proxy. | ||
modifier onlyProxy() { | ||
uint256 s = __self; | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
// To enable use cases with an immutable default implementation in the bytecode, | ||
// (see: ERC6551Proxy), we don't require that the proxy address must match the | ||
// value stored in the implementation slot, which may not be initialized. | ||
if eq(s, address()) { | ||
mstore(0x00, 0x9f03a026) // `UnauthorizedCallContext()`. | ||
revert(0x1c, 0x04) | ||
} | ||
} | ||
_; | ||
} | ||
|
||
/// @dev Requires that the execution is NOT performed via delegatecall. | ||
/// This is the opposite of `onlyProxy`. | ||
modifier notDelegated() { | ||
uint256 s = __self; | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
if iszero(eq(s, address())) { | ||
mstore(0x00, 0x9f03a026) // `UnauthorizedCallContext()`. | ||
revert(0x1c, 0x04) | ||
} | ||
} | ||
_; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,9 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.23; | ||
|
||
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; | ||
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; | ||
|
||
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; | ||
|
||
import {LibClone} from "../ext/solady/LibClone.sol"; | ||
import {LightAccount} from "./LightAccount.sol"; | ||
|
||
/// @title A factory contract for LightAccount. | ||
|
@@ -15,8 +13,8 @@ import {LightAccount} from "./LightAccount.sol"; | |
contract LightAccountFactory { | ||
LightAccount public immutable accountImplementation; | ||
|
||
constructor(IEntryPoint _entryPoint) { | ||
accountImplementation = new LightAccount(_entryPoint); | ||
constructor(IEntryPoint entryPoint) { | ||
accountImplementation = new LightAccount(entryPoint); | ||
} | ||
|
||
/// @notice Create an account, and return its address. Returns the address even if the account is already deployed. | ||
|
@@ -25,35 +23,33 @@ contract LightAccountFactory { | |
/// creation. | ||
/// @param owner The owner of the account to be created. | ||
/// @param salt A salt, which can be changed to create multiple accounts with the same owner. | ||
/// @return ret The address of either the newly deployed account or an existing account with this owner and salt. | ||
function createAccount(address owner, uint256 salt) public returns (LightAccount ret) { | ||
address addr = getAddress(owner, salt); | ||
uint256 codeSize = addr.code.length; | ||
if (codeSize > 0) { | ||
return LightAccount(payable(addr)); | ||
/// @return account The address of either the newly deployed account or an existing account with this owner and salt. | ||
function createAccount(address owner, uint256 salt) public returns (LightAccount account) { | ||
(bool alreadyDeployed, address accountAddress) = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning |
||
LibClone.createDeterministicERC1967(address(accountImplementation), _getCombinedSalt(owner, salt)); | ||
|
||
account = LightAccount(payable(accountAddress)); | ||
|
||
if (!alreadyDeployed) { | ||
account.initialize(owner); | ||
} | ||
ret = LightAccount( | ||
payable( | ||
new ERC1967Proxy{salt: bytes32(salt)}( | ||
address(accountImplementation), abi.encodeCall(LightAccount.initialize, (owner)) | ||
) | ||
) | ||
); | ||
} | ||
|
||
/// @notice Calculate the counterfactual address of this account as it would be returned by `createAccount`. | ||
/// @param owner The owner of the account to be created. | ||
/// @param salt A salt, which can be changed to create multiple accounts with the same owner. | ||
/// @return The address of the account that would be created with `createAccount`. | ||
function getAddress(address owner, uint256 salt) public view returns (address) { | ||
return Create2.computeAddress( | ||
bytes32(salt), | ||
keccak256( | ||
abi.encodePacked( | ||
type(ERC1967Proxy).creationCode, | ||
abi.encode(address(accountImplementation), abi.encodeCall(LightAccount.initialize, (owner))) | ||
) | ||
) | ||
return LibClone.predictDeterministicAddressERC1967( | ||
address(accountImplementation), _getCombinedSalt(owner, salt), address(this) | ||
); | ||
} | ||
|
||
function _getCombinedSalt(address owner, uint256 salt) internal pure returns (bytes32 combinedSalt) { | ||
assembly ("memory-safe") { | ||
mstore(0x00, owner) | ||
mstore(0x20, salt) | ||
combinedSalt := keccak256(0x00, 0x40) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,9 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity ^0.8.23; | ||
|
||
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; | ||
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; | ||
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; | ||
|
||
import {LibClone} from "../ext/solady/LibClone.sol"; | ||
import {MultiOwnerLightAccount} from "./MultiOwnerLightAccount.sol"; | ||
|
||
/// @title A factory contract for MultiOwnerLightAccount. | ||
|
@@ -29,42 +28,39 @@ contract MultiOwnerLightAccountFactory { | |
/// creation. | ||
/// @param owners The owners of the account to be created. | ||
/// @param salt A salt, which can be changed to create multiple accounts with the same owners. | ||
/// @return ret The address of either the newly deployed account or an existing account with these owners and salt. | ||
function createAccount(address[] calldata owners, uint256 salt) public returns (MultiOwnerLightAccount ret) { | ||
address addr = getAddress(owners, salt); | ||
uint256 codeSize = addr.code.length; | ||
if (codeSize > 0) { | ||
return MultiOwnerLightAccount(payable(addr)); | ||
/// @return account The address of either the newly deployed account or an existing account with these owners and salt. | ||
function createAccount(address[] calldata owners, uint256 salt) public returns (MultiOwnerLightAccount account) { | ||
_validateOwnersArray(owners); | ||
|
||
(bool alreadyDeployed, address accountAddress) = | ||
LibClone.createDeterministicERC1967(address(accountImplementation), _getCombinedSalt(owners, salt)); | ||
|
||
account = MultiOwnerLightAccount(payable(accountAddress)); | ||
|
||
if (!alreadyDeployed) { | ||
account.initialize(owners); | ||
} | ||
ret = MultiOwnerLightAccount( | ||
payable( | ||
new ERC1967Proxy{salt: bytes32(salt)}( | ||
address(accountImplementation), abi.encodeCall(MultiOwnerLightAccount.initialize, (owners)) | ||
) | ||
) | ||
); | ||
} | ||
|
||
/// @notice Create an account, and return its address. Returns the address even if the account is already deployed. | ||
/// @dev This method uses less calldata than `createAccount` when creating accounts with a single initial owner. | ||
/// @param owner The owner of the account to be created. | ||
/// @param salt A salt, which can be changed to create multiple accounts with the same owner. | ||
/// @return ret The address of either the newly deployed account or an existing account with this owner and salt. | ||
function createAccountSingle(address owner, uint256 salt) public returns (MultiOwnerLightAccount ret) { | ||
/// @return account The address of either the newly deployed account or an existing account with this owner and salt. | ||
function createAccountSingle(address owner, uint256 salt) public returns (MultiOwnerLightAccount account) { | ||
address[] memory owners = new address[](1); | ||
owners[0] = owner; | ||
address addr = getAddress(owners, salt); | ||
uint256 codeSize = addr.code.length; | ||
if (codeSize > 0) { | ||
return MultiOwnerLightAccount(payable(addr)); | ||
|
||
_validateOwnersArray(owners); | ||
|
||
(bool alreadyDeployed, address accountAddress) = | ||
LibClone.createDeterministicERC1967(address(accountImplementation), _getCombinedSalt(owners, salt)); | ||
|
||
account = MultiOwnerLightAccount(payable(accountAddress)); | ||
|
||
if (!alreadyDeployed) { | ||
account.initialize(owners); | ||
} | ||
ret = MultiOwnerLightAccount( | ||
payable( | ||
new ERC1967Proxy{salt: bytes32(salt)}( | ||
address(accountImplementation), abi.encodeCall(MultiOwnerLightAccount.initialize, (owners)) | ||
) | ||
) | ||
); | ||
} | ||
|
||
/// @notice Calculate the counterfactual address of this account as it would be returned by `createAccount`. | ||
|
@@ -74,19 +70,15 @@ contract MultiOwnerLightAccountFactory { | |
function getAddress(address[] memory owners, uint256 salt) public view returns (address) { | ||
_validateOwnersArray(owners); | ||
|
||
return Create2.computeAddress( | ||
bytes32(salt), | ||
keccak256( | ||
abi.encodePacked( | ||
type(ERC1967Proxy).creationCode, | ||
abi.encode( | ||
address(accountImplementation), abi.encodeCall(MultiOwnerLightAccount.initialize, (owners)) | ||
) | ||
) | ||
) | ||
return LibClone.predictDeterministicAddressERC1967( | ||
address(accountImplementation), _getCombinedSalt(owners, salt), address(this) | ||
); | ||
} | ||
|
||
function _getCombinedSalt(address[] memory owners, uint256 salt) internal pure returns (bytes32) { | ||
return keccak256(abi.encode(owners, salt)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this one, we can't avoid memory expansion without a large refactor. |
||
} | ||
|
||
/// @dev `owners` must be in strictly ascending order and not include the 0 address. The ordering requirement | ||
/// ensures a canonical counterfactual for a given set of initial owners. Also, its length must not be empty | ||
/// and not exceed `_MAX_OWNERS_ON_CREATION`. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here for the solady commit hash