Skip to content

Commit

Permalink
feat: stakable factories (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaypaik authored Mar 22, 2024
1 parent 8727f68 commit 93f46a2
Show file tree
Hide file tree
Showing 10 changed files with 487 additions and 143 deletions.
372 changes: 258 additions & 114 deletions .gasestimates.md

Large diffs are not rendered by default.

40 changes: 37 additions & 3 deletions .storagelayout.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,49 @@ Generated via `bash utils/inspect.sh`.

---

`forge inspect --pretty src/CustomSlotInitializable.sol:CustomSlotInitializable storage-layout`
`forge inspect --pretty src/LightAccount.sol:LightAccount storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|------|------|------|--------|-------|----------|

`forge inspect --pretty src/LightAccount.sol:LightAccount storage-layout`
`forge inspect --pretty src/LightAccountFactory.sol:LightAccountFactory storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|---------------|---------|------|--------|-------|-------------------------------------------------|
| _owner | address | 0 | 0 | 20 | src/LightAccountFactory.sol:LightAccountFactory |
| _pendingOwner | address | 1 | 0 | 20 | src/LightAccountFactory.sol:LightAccountFactory |

`forge inspect --pretty src/MultiOwnerLightAccount.sol:MultiOwnerLightAccount storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|------|------|------|--------|-------|----------|

`forge inspect --pretty src/LightAccountFactory.sol:LightAccountFactory storage-layout`
`forge inspect --pretty src/MultiOwnerLightAccountFactory.sol:MultiOwnerLightAccountFactory storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|---------------|---------|------|--------|-------|---------------------------------------------------------------------|
| _owner | address | 0 | 0 | 20 | src/MultiOwnerLightAccountFactory.sol:MultiOwnerLightAccountFactory |
| _pendingOwner | address | 1 | 0 | 20 | src/MultiOwnerLightAccountFactory.sol:MultiOwnerLightAccountFactory |

`forge inspect --pretty src/common/BaseLightAccount.sol:BaseLightAccount storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|------|------|------|--------|-------|----------|

`forge inspect --pretty src/common/BaseLightAccountFactory.sol:BaseLightAccountFactory storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|---------------|---------|------|--------|-------|----------------------------------------------------------------|
| _owner | address | 0 | 0 | 20 | src/common/BaseLightAccountFactory.sol:BaseLightAccountFactory |
| _pendingOwner | address | 1 | 0 | 20 | src/common/BaseLightAccountFactory.sol:BaseLightAccountFactory |

`forge inspect --pretty src/common/CustomSlotInitializable.sol:CustomSlotInitializable storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|------|------|------|--------|-------|----------|

`forge inspect --pretty src/common/ERC1271.sol:ERC1271 storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|------|------|------|--------|-------|----------|

`forge inspect --pretty src/external/solady/EIP712.sol:EIP712 storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|------|------|------|--------|-------|----------|

`forge inspect --pretty src/external/solady/UUPSUpgradeable.sol:UUPSUpgradeable storage-layout`
| Name | Type | Slot | Offset | Bytes | Contract |
|------|------|------|--------|-------|----------|

5 changes: 3 additions & 2 deletions script/Deploy_LightAccountFactory.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ contract Deploy_LightAccountFactory is Script {
console.log("******** Deploy ...... *********");
console.log("********************************");

// TODO: Use environment variable for factory owner.
LightAccountFactory factory = new LightAccountFactory{
salt: 0x4e59b44847b379578588920ca78fbf26c0b4956c5528f3e2f146000008fabf77
}(entryPoint);
}(msg.sender, entryPoint);

// Deployed address check
if (address(factory) != 0x00004EC70002a32400f8ae005A26081065620D20) {
Expand All @@ -52,7 +53,7 @@ contract Deploy_LightAccountFactory is Script {
console.logAddress(address(factory));

console.log("Implementation address:");
console.logAddress(address(factory.accountImplementation()));
console.logAddress(address(factory.ACCOUNT_IMPLEMENTATION()));
vm.stopBroadcast();
}
}
15 changes: 9 additions & 6 deletions src/LightAccountFactory.sol
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";

import {BaseLightAccountFactory} from "./common/BaseLightAccountFactory.sol";
import {LibClone} from "./external/solady/LibClone.sol";
import {LightAccount} from "./LightAccount.sol";

/// @title A factory contract for LightAccount.
/// @dev A UserOperations "initCode" holds the address of the factory, and a method call (`createAccount`). The
/// factory's `createAccount` returns the target account address even if it is already installed. This way,
/// `entryPoint.getSenderAddress()` can be called either before or after the account is created.
contract LightAccountFactory {
LightAccount public immutable accountImplementation;
contract LightAccountFactory is BaseLightAccountFactory {
LightAccount public immutable ACCOUNT_IMPLEMENTATION;

constructor(IEntryPoint entryPoint) {
accountImplementation = new LightAccount(entryPoint);
constructor(address owner, IEntryPoint entryPoint) Ownable(owner) {
ACCOUNT_IMPLEMENTATION = new LightAccount(entryPoint);
ENTRY_POINT = entryPoint;
}

/// @notice Create an account, and return its address. Returns the address even if the account is already deployed.
Expand All @@ -26,7 +29,7 @@ contract LightAccountFactory {
/// @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));
LibClone.createDeterministicERC1967(address(ACCOUNT_IMPLEMENTATION), _getCombinedSalt(owner, salt));

account = LightAccount(payable(accountAddress));

Expand All @@ -41,7 +44,7 @@ contract LightAccountFactory {
/// @return The address of the account that would be created with `createAccount`.
function getAddress(address owner, uint256 salt) public view returns (address) {
return LibClone.predictDeterministicAddressERC1967(
address(accountImplementation), _getCombinedSalt(owner, salt), address(this)
address(ACCOUNT_IMPLEMENTATION), _getCombinedSalt(owner, salt), address(this)
);
}

Expand Down
17 changes: 10 additions & 7 deletions src/MultiOwnerLightAccountFactory.sol
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";

import {BaseLightAccountFactory} from "./common/BaseLightAccountFactory.sol";
import {LibClone} from "./external/solady/LibClone.sol";
import {MultiOwnerLightAccount} from "./MultiOwnerLightAccount.sol";

/// @title A factory contract for MultiOwnerLightAccount.
/// @dev A UserOperations "initCode" holds the address of the factory, and a method call (`createAccount` or
/// `createAccountSingle`). The factory returns the target account address even if it is already deployed. This way,
/// `entryPoint.getSenderAddress()` can be called either before or after the account is created.
contract MultiOwnerLightAccountFactory {
contract MultiOwnerLightAccountFactory is BaseLightAccountFactory {
uint256 internal constant _MAX_OWNERS_ON_CREATION = 100;
MultiOwnerLightAccount public immutable accountImplementation;
MultiOwnerLightAccount public immutable ACCOUNT_IMPLEMENTATION;

error InvalidOwners();
error OwnersArrayEmpty();
error OwnersLimitExceeded();

constructor(IEntryPoint entryPoint) {
accountImplementation = new MultiOwnerLightAccount(entryPoint);
constructor(address owner, IEntryPoint entryPoint) Ownable(owner) {
ACCOUNT_IMPLEMENTATION = new MultiOwnerLightAccount(entryPoint);
ENTRY_POINT = entryPoint;
}

/// @notice Create an account, and return its address. Returns the address even if the account is already deployed.
Expand All @@ -33,7 +36,7 @@ contract MultiOwnerLightAccountFactory {
_validateOwnersArray(owners);

(bool alreadyDeployed, address accountAddress) =
LibClone.createDeterministicERC1967(address(accountImplementation), _getCombinedSalt(owners, salt));
LibClone.createDeterministicERC1967(address(ACCOUNT_IMPLEMENTATION), _getCombinedSalt(owners, salt));

account = MultiOwnerLightAccount(payable(accountAddress));

Expand All @@ -54,7 +57,7 @@ contract MultiOwnerLightAccountFactory {
_validateOwnersArray(owners);

(bool alreadyDeployed, address accountAddress) =
LibClone.createDeterministicERC1967(address(accountImplementation), _getCombinedSalt(owners, salt));
LibClone.createDeterministicERC1967(address(ACCOUNT_IMPLEMENTATION), _getCombinedSalt(owners, salt));

account = MultiOwnerLightAccount(payable(accountAddress));

Expand All @@ -71,7 +74,7 @@ contract MultiOwnerLightAccountFactory {
_validateOwnersArray(owners);

return LibClone.predictDeterministicAddressERC1967(
address(accountImplementation), _getCombinedSalt(owners, salt), address(this)
address(ACCOUNT_IMPLEMENTATION), _getCombinedSalt(owners, salt), address(this)
);
}

Expand Down
59 changes: 59 additions & 0 deletions src/common/BaseLightAccountFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.23;

import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol";

abstract contract BaseLightAccountFactory is Ownable2Step {
IEntryPoint public immutable ENTRY_POINT;

error InvalidAction();
error TransferFailed();

/// @notice Allow contract to receive native currency.
receive() external payable {}

/// @notice Add stake to an entry point.
/// @dev Only callable by owner.
/// @param unstakeDelay Unstake delay for the stake.
/// @param amount Amount of native currency to stake.
function addStake(uint32 unstakeDelay, uint256 amount) external payable onlyOwner {
ENTRY_POINT.addStake{value: amount}(unstakeDelay);
}

/// @notice Start unlocking stake for an entry point.
/// @dev Only callable by owner.
function unlockStake() external onlyOwner {
ENTRY_POINT.unlockStake();
}

/// @notice Withdraw stake from an entry point.
/// @dev Only callable by owner.
/// @param to Address to send native currency to.
function withdrawStake(address payable to) external onlyOwner {
ENTRY_POINT.withdrawStake(to);
}

/// @notice Withdraw funds from this contract.
/// @dev Can withdraw stuck erc20s or native currency.
/// @param to Address to send erc20s or native currency to.
/// @param token Address of the token to withdraw, 0 address for native currency.
/// @param amount Amount of the token to withdraw in case of rebasing tokens.
function withdraw(address payable to, address token, uint256 amount) external onlyOwner {
if (token == address(0)) {
(bool success,) = to.call{value: address(this).balance}("");
if (!success) {
revert TransferFailed();
}
} else {
SafeERC20.safeTransfer(IERC20(token), to, amount);
}
}

/// @notice Overriding to disable renounce ownership in Ownable.
function renounceOwnership() public view override onlyOwner {
revert InvalidAction();
}
}
8 changes: 4 additions & 4 deletions test/LightAccount.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ contract LightAccountTest is Test {
function setUp() public {
eoaAddress = vm.addr(EOA_PRIVATE_KEY);
entryPoint = new EntryPoint();
LightAccountFactory factory = new LightAccountFactory(entryPoint);
LightAccountFactory factory = new LightAccountFactory(address(this), entryPoint);
account = factory.createAccount(eoaAddress, 1);
vm.deal(address(account), 1 << 128);
lightSwitch = new LightSwitch();
Expand Down Expand Up @@ -173,14 +173,14 @@ contract LightAccountTest is Test {
}

function testInitialize() public {
LightAccountFactory factory = new LightAccountFactory(entryPoint);
LightAccountFactory factory = new LightAccountFactory(address(this), entryPoint);
vm.expectEmit(true, false, false, false);
emit Initialized(0);
account = factory.createAccount(eoaAddress, 1);
}

function testCannotInitializeWithZeroOwner() public {
LightAccountFactory factory = new LightAccountFactory(entryPoint);
LightAccountFactory factory = new LightAccountFactory(address(this), entryPoint);
vm.expectRevert(abi.encodeWithSelector(LightAccount.InvalidOwner.selector, (address(0))));
account = factory.createAccount(address(0), 1);
}
Expand Down Expand Up @@ -487,7 +487,7 @@ contract LightAccountTest is Test {
bytes32(uint256(uint160(0x0000000071727De22E5E9d8BAf0edAc6f37da032)))
)
),
0x6aa8e89cd8a2df2df153f6959aace173a48e4ecedde04f7dc09add425d88a7e2
0x10f16a2b2ab8a4a6c680a35494382da995eea00c40bed1fc5391774da7de7fc9
);
}

Expand Down
54 changes: 52 additions & 2 deletions test/LightAccountFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import "forge-std/Test.sol";

import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";

import {BaseLightAccountFactory} from "../src/common/BaseLightAccountFactory.sol";
import {LightAccount} from "../src/LightAccount.sol";
import {LightAccountFactory} from "../src/LightAccountFactory.sol";

contract LightAccountTest is Test {
contract LightAccountFactoryTest is Test {
using stdStorage for StdStorage;

address public constant OWNER_ADDRESS = address(0x100);
Expand All @@ -17,7 +18,7 @@ contract LightAccountTest is Test {

function setUp() public {
entryPoint = new EntryPoint();
factory = new LightAccountFactory(entryPoint);
factory = new LightAccountFactory(address(this), entryPoint);
}

function testReturnsAddressWhenAccountAlreadyExists() public {
Expand All @@ -33,4 +34,53 @@ contract LightAccountTest is Test {
assertTrue(address(factual).codehash != bytes32(0));
assertEq(counterfactual, address(factual));
}

function testAddStake() public {
assertEq(entryPoint.balanceOf(address(factory)), 0);
vm.deal(address(this), 100 ether);
factory.addStake{value: 10 ether}(10 hours, 10 ether);
assertEq(entryPoint.getDepositInfo(address(factory)).stake, 10 ether);
}

function testUnlockStake() public {
testAddStake();
factory.unlockStake();
assertEq(entryPoint.getDepositInfo(address(factory)).withdrawTime, block.timestamp + 10 hours);
}

function testWithdrawStake() public {
testUnlockStake();
vm.warp(10 hours);
vm.expectRevert("Stake withdrawal is not due");
factory.withdrawStake(payable(address(this)));
assertEq(address(this).balance, 90 ether);
vm.warp(10 hours + 1);
factory.withdrawStake(payable(address(this)));
assertEq(address(this).balance, 100 ether);
}

function testWithdraw() public {
factory.addStake{value: 10 ether}(10 hours, 1 ether);
assertEq(address(factory).balance, 9 ether);
factory.withdraw(payable(address(this)), address(0), 0); // amount = balance if native currency
assertEq(address(factory).balance, 0);
}

function test2StepOwnershipTransfer() public {
address owner1 = address(0x200);
assertEq(factory.owner(), address(this));
factory.transferOwnership(owner1);
assertEq(factory.owner(), address(this));
vm.prank(owner1);
factory.acceptOwnership();
assertEq(factory.owner(), owner1);
}

function testCannotRenounceOwnership() public {
vm.expectRevert(BaseLightAccountFactory.InvalidAction.selector);
factory.renounceOwnership();
}

/// @dev Receive funds from withdraw.
receive() external payable {}
}
8 changes: 4 additions & 4 deletions test/MultiOwnerLightAccount.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ contract MultiOwnerLightAccountTest is Test {
function setUp() public {
eoaAddress = vm.addr(EOA_PRIVATE_KEY);
entryPoint = new EntryPoint();
MultiOwnerLightAccountFactory factory = new MultiOwnerLightAccountFactory(entryPoint);
MultiOwnerLightAccountFactory factory = new MultiOwnerLightAccountFactory(address(this), entryPoint);
account = factory.createAccountSingle(eoaAddress, 1);
vm.deal(address(account), 1 << 128);
lightSwitch = new LightSwitch();
Expand Down Expand Up @@ -223,14 +223,14 @@ contract MultiOwnerLightAccountTest is Test {
}

function testInitialize() public {
MultiOwnerLightAccountFactory factory = new MultiOwnerLightAccountFactory(entryPoint);
MultiOwnerLightAccountFactory factory = new MultiOwnerLightAccountFactory(address(this), entryPoint);
vm.expectEmit(true, false, false, false);
emit Initialized(0);
account = factory.createAccountSingle(eoaAddress, 1);
}

function testCannotInitializeWithZeroOwner() public {
MultiOwnerLightAccountFactory factory = new MultiOwnerLightAccountFactory(entryPoint);
MultiOwnerLightAccountFactory factory = new MultiOwnerLightAccountFactory(address(this), entryPoint);
vm.expectRevert(MultiOwnerLightAccountFactory.InvalidOwners.selector);
account = factory.createAccountSingle(address(0), 1);
}
Expand Down Expand Up @@ -662,7 +662,7 @@ contract MultiOwnerLightAccountTest is Test {
bytes32(uint256(uint160(0x0000000071727De22E5E9d8BAf0edAc6f37da032)))
)
),
0x5f04ff963bc2b9faa9a30beff91afc7cdf84f8a52f369b70060fe0f41b0504df
0x04ef3a9310d1a2d867807831aa0361b20aad25421de58cf37457fcf7f9567b6a
);
}

Expand Down
Loading

0 comments on commit 93f46a2

Please sign in to comment.