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: add account factory #105

Merged
merged 7 commits into from
Jul 23, 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
84 changes: 84 additions & 0 deletions src/account/AccountFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import {IEntryPoint} from "@eth-infinitism/account-abstraction/interfaces/IEntryPoint.sol";

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";

import {UpgradeableModularAccount} from "../account/UpgradeableModularAccount.sol";
import {ValidationConfigLib} from "../helpers/ValidationConfigLib.sol";

contract AccountFactory is Ownable {
howydev marked this conversation as resolved.
Show resolved Hide resolved
UpgradeableModularAccount public immutable ACCOUNT_IMPL;
bytes32 private immutable _PROXY_BYTECODE_HASH;
uint32 public constant UNSTAKE_DELAY = 1 weeks;
IEntryPoint public immutable ENTRY_POINT;
address public immutable SINGLE_SIGNER_VALIDATION;

constructor(IEntryPoint _entryPoint, UpgradeableModularAccount _accountImpl, address _singleSignerValidation)
Ownable(msg.sender)
{
ENTRY_POINT = _entryPoint;
_PROXY_BYTECODE_HASH =
keccak256(abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(address(_accountImpl), "")));
ACCOUNT_IMPL = _accountImpl;
SINGLE_SIGNER_VALIDATION = _singleSignerValidation;
}

/**
* Create an account, and return its address.
* Returns the address even if the account is already deployed.
* Note that during user operation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after
* account creation
*/
function createAccount(address owner, uint256 salt, uint32 entityId)
external
returns (UpgradeableModularAccount)
{
bytes32 combinedSalt = getSalt(owner, salt, entityId);
address addr = Create2.computeAddress(combinedSalt, _PROXY_BYTECODE_HASH);

// short circuit if exists
if (addr.code.length == 0) {
bytes memory pluginInstallData = abi.encode(entityId, owner);
// not necessary to check return addr since next call will fail if so
new ERC1967Proxy{salt: combinedSalt}(address(ACCOUNT_IMPL), "");
// point proxy to actual implementation and init plugins
UpgradeableModularAccount(payable(addr)).initializeWithValidation(
ValidationConfigLib.pack(SINGLE_SIGNER_VALIDATION, entityId, true, true),
new bytes4[](0),
pluginInstallData,
"",
""
);
}

return UpgradeableModularAccount(payable(addr));
}

function addStake() external payable onlyOwner {
ENTRY_POINT.addStake{value: msg.value}(UNSTAKE_DELAY);
}

function unlockStake() external onlyOwner {
ENTRY_POINT.unlockStake();
}

function withdrawStake(address payable withdrawAddress) external onlyOwner {
ENTRY_POINT.withdrawStake(withdrawAddress);
}

/**
* Calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(address owner, uint256 salt, uint32 entityId) external view returns (address) {
return Create2.computeAddress(getSalt(owner, salt, entityId), _PROXY_BYTECODE_HASH);
}

function getSalt(address owner, uint256 salt, uint32 entityId) public pure returns (bytes32) {
return keccak256(abi.encodePacked(owner, salt, entityId));
}
}
43 changes: 43 additions & 0 deletions test/account/AccountFactory.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import {AccountFactory} from "../../src/account/AccountFactory.sol";
import {UpgradeableModularAccount} from "../../src/account/UpgradeableModularAccount.sol";
import {AccountTestBase} from "../utils/AccountTestBase.sol";

contract AccountFactoryTest is AccountTestBase {
AccountFactory internal _factory;
UpgradeableModularAccount internal _account;

function setUp() public {
_account = new UpgradeableModularAccount(entryPoint);
_factory = new AccountFactory(entryPoint, _account, address(singleSignerValidation));
}

function test_createAccount() public {
UpgradeableModularAccount account = _factory.createAccount(address(this), 100, 0);

assertEq(address(account.entryPoint()), address(entryPoint));
}

function test_createAccountAndGetAddress() public {
UpgradeableModularAccount account = _factory.createAccount(address(this), 100, 0);

assertEq(address(account), address(_factory.createAccount(address(this), 100, 0)));
}

function test_multipleDeploy() public {
UpgradeableModularAccount account = _factory.createAccount(address(this), 100, 0);

uint256 startGas = gasleft();

UpgradeableModularAccount account2 = _factory.createAccount(address(this), 100, 0);

// Assert that the 2nd deployment call cost less than 1 sstore
// Implies that no deployment was done on the second calls
assertLe(startGas - 22_000, gasleft());

// Assert the return addresses are the same
assertEq(address(account), address(account2));
}
}
Loading