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: stakable factories #47

Merged
merged 1 commit into from
Mar 22, 2024
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
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);
Copy link
Contributor

Choose a reason for hiding this comment

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

Hypothetically, we could add a withdraw function to withdraw from the EntryPoint's deposit for this factory, but that should never happen unless we make a mistake. So I'm ok leaving that out.

}

/// @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
Loading