Skip to content

Commit

Permalink
feat: use solady's minimal ERC-1967 proxy and UUPSUpgradeable
Browse files Browse the repository at this point in the history
  • Loading branch information
jaypaik committed Mar 11, 2024
1 parent a79c6dc commit 1d1c48d
Show file tree
Hide file tree
Showing 7 changed files with 1,404 additions and 67 deletions.
1,210 changes: 1,210 additions & 0 deletions ext/solady/LibClone.sol

Large diffs are not rendered by default.

142 changes: 142 additions & 0 deletions ext/solady/UUPSUpgradeable.sol
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)
}
}
_;
}
}
44 changes: 18 additions & 26 deletions src/LightAccountFactory.sol
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.
Expand All @@ -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.
Expand All @@ -25,35 +23,29 @@ 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) =
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) {
return keccak256(abi.encode(owner, salt));
}
}
68 changes: 30 additions & 38 deletions src/MultiOwnerLightAccountFactory.sol
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.
Expand All @@ -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`.
Expand All @@ -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));
}

/// @dev `owners` must be in strictly ascending order and not include the 0 address. Its length must not be empty
/// and not exceed `_MAX_OWNERS_ON_CREATION`.
/// @param owners Array of owner addresses.
Expand Down
3 changes: 2 additions & 1 deletion src/common/BaseLightAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
pragma solidity ^0.8.23;

import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {BaseAccount} from "account-abstraction/core/BaseAccount.sol";
import {SIG_VALIDATION_FAILED} from "account-abstraction/core/Helpers.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol";
import {TokenCallbackHandler} from "account-abstraction/samples/callback/TokenCallbackHandler.sol";

import {UUPSUpgradeable} from "../../ext/solady/UUPSUpgradeable.sol";

abstract contract BaseLightAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, IERC1271 {
bytes4 internal constant _1271_MAGIC_VALUE = bytes4(keccak256("isValidSignature(bytes32,bytes)")); // 0x1626ba7e
IEntryPoint internal immutable _ENTRY_POINT;
Expand Down
2 changes: 1 addition & 1 deletion test/LightAccount.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ contract LightAccountTest is Test {
bytes32(uint256(uint160(0x0000000071727De22E5E9d8BAf0edAc6f37da032)))
)
),
0x9eee97e8649714c9c53e786aa29791f862bb415d04ca7ed402f18eaea04d5a14
0x67451f36b4201f5ba07b252318016021456072e6ac603ba84431630cfc7ecac3
);
}

Expand Down
2 changes: 1 addition & 1 deletion test/MultiOwnerLightAccount.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ contract MultiOwnerLightAccountTest is Test {
bytes32(uint256(uint160(0x0000000071727De22E5E9d8BAf0edAc6f37da032)))
)
),
0x746d34207385a35c9fc4c54fd6169ecf343bfb8fd037d3c4544b651cabca73e1
0x4c8a83770489550019a83bd0af91c7577ea978131b78945f87c63d1ec36036e0
);
}

Expand Down

0 comments on commit 1d1c48d

Please sign in to comment.