Skip to content
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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;

Copy link
Contributor

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

/// @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)
}
}
_;
}
}
48 changes: 22 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,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) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning (bool alreadyDeployed, address accountAddress) is a very nice DevEx from solady :)

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)
}
}
}
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));
Copy link
Contributor

Choose a reason for hiding this comment

The 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`.
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)))
)
),
0x8b2f633eba37bf3a83c6f8e7a66b5816e425717597e3a687777e977ddbc589c0
0xa677af8a16ab66d988856a88ea647c45da368a588b036126ce3e21645f7891ca
);
}

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)))
)
),
0x48a98b19a2130ad0b8bf369ce1f6205eb3dbf67aedf5b412d00a1686b87d3e4f
0x7016bedb9dcfa8db776c0f17f2118c3dbc61c14351f845f2014befa061e973dd
);
}

Expand Down
Loading