Skip to content

Commit

Permalink
♻️ isolate the two factory scenarios
Browse files Browse the repository at this point in the history
This commit move the multi-step deployment scenario to a specific contract
that inherits from the original one (the one-step deployment scenario).
  • Loading branch information
qd-qd committed Jan 11, 2024
1 parent 7a636b0 commit f6c539b
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 45 deletions.
8 changes: 4 additions & 4 deletions script/AccountFactory/CreateAccount.s.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// SPDX-License-Identifier: APACHE-2.0
pragma solidity >=0.8.19 <0.9.0;

import { AccountFactory } from "src/AccountFactory.sol";
import { AccountFactoryMultiSteps } from "src/AccountFactoryMultiSteps.sol";
import { BaseScript } from "../Base.s.sol";

/// @title Create an Account using an already deployed AccountFactory
/// @dev If you need to deploy an AccountFactory, use the Deploy script in this directory
/// @title Create an Account using an already deployed AccountFactoryMultiSteps
/// @dev If you need to deploy an AccountFactoryMultiSteps, use the Deploy script in this directory
contract CreateAccount is BaseScript {
function run(address factoryAddress, bytes32 loginHash) public broadcast returns (address) {
AccountFactory factory = AccountFactory(factoryAddress);
AccountFactoryMultiSteps factory = AccountFactoryMultiSteps(factoryAddress);
return factory.createAccount(loginHash);
}
}
Expand Down
File renamed without changes.
39 changes: 39 additions & 0 deletions script/AccountFactory/DeployFactoryMultiSteps.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: APACHE-2.0
pragma solidity >=0.8.19 <0.9.0;

import { AccountFactoryMultiSteps } from "src/AccountFactoryMultiSteps.sol";
import { BaseScript } from "../Base.s.sol";

/// @title Deploy an AccountFactoryMultiSteps
/// @dev You can pass environment variables to this script to tailor the deployment.
/// Do not deploy this script in production without changing the default values!
contract AccountFactoryMultiStepsDeploy is BaseScript {
// This is currently the universal address of the 4337 entrypoint
address internal constant ENTRYPOINT = 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789;
// This is the first account exposed by Anvil
address internal constant NAME_SERVICE_OWNER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;

function run() public broadcast returns (AccountFactoryMultiSteps) {
address entrypoint = vm.envOr("ENTRYPOINT", ENTRYPOINT);
address webAuthnVerifier = vm.envOr("WEBAUTHN_VERIFIER", address(0));
address nameServiceOwner = vm.envOr("NAME_SERVICE_OWNER", NAME_SERVICE_OWNER);

return new AccountFactoryMultiSteps(entrypoint, webAuthnVerifier, nameServiceOwner);
}
}

/*
ℹ️ HOW TO USE THIS SCRIPT USING A LEDGER:
forge script AccountFactoryMultiStepsDeploy --rpc-url <RPC_URL> --ledger --sender <ACCOUNT_ADDRESS> [--broadcast]
ℹ️ HOW TO USE THIS SCRIPT WITH AN ARBITRARY PRIVATE KEY (NOT RECOMMENDED):
PRIVATE_KEY=<PRIVATE_KEY> forge script AccountFactoryMultiStepsDeploy --rpc-url <RPC_URL> [--broadcast]
ℹ️ HOW TO USE THIS SCRIPT ON ANVIL IN DEFAULT MODE:
forge script AccountFactoryMultiStepsDeploy --rpc-url http://127.0.0.1:8545 --broadcast --sender \
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --mnemonics "test test test test test test test test test test test junk"
*/
34 changes: 3 additions & 31 deletions src/AccountFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,15 @@ import { Account } from "./Account.sol";
/// @notice This contract is a 4337-compliant factory for smart-accounts. It is in charge of deploying an account
/// implementation during its construction, then deploying proxies for the users. The proxies are deployed
/// using the CREATE2 opcode and they use the implementation contract deployed on construction as a
/// reference. For the 1-step onboarding purpose, the factory can also be in charge of setting the first
/// signer of the account, leading to a fully-setup account for the user.
/// reference. Once the account has been deployed by the factory, the factory is also in charge of setting
/// the first signer of the account, leading to a fully-setup account for the user.
/// @dev The name service signature is only used to set the first-signer to the account. It is a EIP-191 message
/// signed by the nameServiceOwner. The message is the keccak256 hash of the login of the account.
contract AccountFactory {
address public immutable accountImplementation;
address public immutable nameServiceOwner;

event AccountCreatedAndInit(bytes32 loginHash, address account, bytes credId, uint256 pubKeyX, uint256 pubKeyY);
event AccountCreated(bytes32 loginHash, address account);

error InvalidNameServiceSignature(bytes32 loginHash, bytes nameServiceSignature);

Expand Down Expand Up @@ -111,33 +110,6 @@ contract AccountFactory {
return address(account);
}

/// @notice This is the multi-steps scenario. This function either deploys an account or returns the address of
/// an existing account based on the parameter given. In any case this function set the first signer.
/// @param loginHash The keccak256 hash of the login of the account
/// @return The address of the account (either deployed or not)
function createAccount(bytes32 loginHash) external returns (address) {
// check if the account is already deployed and return prematurely if it is
address alreadyDeployedAddress = _checkAccountExistence(loginHash);
if (alreadyDeployedAddress != address(0)) {
return alreadyDeployedAddress;
}

// deploy the proxy for the user. During the deployment call, the
// initialize function in the implementation contract is called
// using the `delegatecall` opcode
Account account = Account(
payable(
new ERC1967Proxy{ salt: loginHash }(
address(accountImplementation), abi.encodeCall(Account.initialize, (loginHash))
)
)
);

emit AccountCreated(loginHash, address(account));

return address(account);
}

/// @notice This utility function returns the address of the account that would be deployed
/// @dev This is the under the hood formula used by the CREATE2 opcode
/// @param loginHash The keccak256 hash of the login of the account
Expand Down Expand Up @@ -170,7 +142,7 @@ contract AccountFactory {
}

// NOTE:
// - Both creation methods defined in this contract follow the EIP-4337 recommandations.
// - The creation method defined in this contract follow the EIP-4337 recommandations.
// That's why the methods return the address of the already deployed account if it exists.
// https://eips.ethereum.org/EIPS/eip-4337#first-time-account-creation
//
Expand Down
58 changes: 58 additions & 0 deletions src/AccountFactoryMultiSteps.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.20 <0.9.0;

import { AccountFactory, Account, ERC1967Proxy } from "./AccountFactory.sol";

/// @title Multi-seps 4337-compliant Account Factory
/// @notice This contract inherits from the AccountFactory contract and adds a multi-steps scenario.
/// @custom:experimental This is an experimental contract.
contract AccountFactoryMultiSteps is AccountFactory {
event AccountCreated(bytes32 loginHash, address account);

/// @notice Deploy the implementation of the account and store its address in the storage of the factory. This
/// implementation will be used as the implementation reference
/// for all the proxies deployed by this factory.
/// @param entryPoint The unique address of the entrypoint (EIP-4337 related)
/// @param webAuthnVerifier The address of the crypto library that will be used by
/// the account to verify the WebAuthn signature of the signer(s)
/// @param _nameServiceOwner The address used to verify the signature of the name service.
/// This address is stored in the storage of this contract that validate future signatures
/// @dev The account deployed here is expected to be proxied later, its own storage won't be used.
/// All the arguments passed to the constructor function are used to set immutable variables.
/// As a valid signature from the nameServiceOwner is required to set the first signer of the account,
/// there is no need to make the account inoperable. No one will be able to use it.
constructor(
address entryPoint,
address webAuthnVerifier,
address _nameServiceOwner
)
AccountFactory(entryPoint, webAuthnVerifier, _nameServiceOwner)
{ }

/// @notice This is the multi-steps scenario. This function either deploys an account or returns the address of
/// an existing account based on the parameter given. In any case this function set the first signer.
/// @param loginHash The keccak256 hash of the login of the account
/// @return The address of the account (either deployed or not)
function createAccount(bytes32 loginHash) external returns (address) {
// check if the account is already deployed and return prematurely if it is
address alreadyDeployedAddress = _checkAccountExistence(loginHash);
if (alreadyDeployedAddress != address(0)) {
return alreadyDeployedAddress;
}

// deploy the proxy for the user. During the deployment call, the
// initialize function in the implementation contract is called
// using the `delegatecall` opcode
Account account = Account(
payable(
new ERC1967Proxy{ salt: loginHash }(
address(accountImplementation), abi.encodeCall(Account.initialize, (loginHash))
)
)
);

emit AccountCreated(loginHash, address(account));

return address(account);
}
}
13 changes: 8 additions & 5 deletions test/integration/AccountFactory/deterministicDeployments.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity >=0.8.20 <0.9.0;

import { AccountFactory } from "src/AccountFactory.sol";
import { AccountFactoryMultiSteps } from "src/AccountFactoryMultiSteps.sol";
import { BaseTest } from "test/BaseTest.sol";

/// @notice The role of this test is to ensure our both deterministic deployment
Expand All @@ -17,18 +18,20 @@ contract AccountFactoryDeterministicDeployment is BaseTest {
hex"a784b0b917cd6e19a4b6ebfb5d93a217ea76c37ff6d98d5f3aa18015e7220543a95d215a50381c";

AccountFactory private factory;
AccountFactoryMultiSteps private factoryMultiSteps;

function setUp() external {
factory = new AccountFactory(address(0), address(0), SIGNER);
factoryMultiSteps = new AccountFactoryMultiSteps(address(0), address(0), SIGNER);
}

function test_WhenUsingTheCreateAccountFlow() external {
// it should deploy the account to the same address calculated by getAddress

assertEq(factory.getAddress(LOGIN_HASH), factory.createAccount(LOGIN_HASH));
assertEq(factoryMultiSteps.getAddress(LOGIN_HASH), factoryMultiSteps.createAccount(LOGIN_HASH));
}

function test_WhenUsingTheCreateAccountAndIntFlow() external {
function test_WhenUsingTheCreateAccountAndInitFlow() external {
// it should deploy the account to the same address calculated by getAddress

assertEq(
Expand All @@ -43,13 +46,13 @@ contract AccountFactoryDeterministicDeployment is BaseTest {

// deploy the account using `createAndInitAccount`
address createAccountAndInitAddress =
factory.createAndInitAccount(uint256(0), uint256(0), LOGIN_HASH, hex"", PRE_FORGED_SIGNATURE);
factoryMultiSteps.createAndInitAccount(uint256(0), uint256(0), LOGIN_HASH, hex"", PRE_FORGED_SIGNATURE);

// revert to the state of the EVM before deploying the first account
// revert to the state of the EVM before deploying the first account -- resetting the deployed account
vm.revertTo(snapshot);

// deploy the account using `createAccount`
address createAccountAddress = factory.createAccount(LOGIN_HASH);
address createAccountAddress = factoryMultiSteps.createAccount(LOGIN_HASH);

// ensure both flows deployed the account to the same address
assertEq(createAccountAddress, createAccountAndInitAddress);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
AccountFactoryDeterministicDeployment
├── when using the createAccount flow
│ └── it should deploy the account to the same address calculated by getAddress
├── when using the createAccountAndInt flow
├── when using the createAccountAndInit flow
│ └── it should deploy the account to the same address calculated by getAddress
└── when using both flows with the same parameters
└── it should deploy the account to the same address
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.20 <0.9.0;

import { AccountFactory } from "src/AccountFactory.sol";
import { AccountFactoryMultiSteps } from "src/AccountFactoryMultiSteps.sol";
import { BaseTest } from "test/BaseTest.sol";

contract AccountFactory__CreateAccount is BaseTest {
AccountFactory internal factory;
contract AccountFactoryMultiSteps__CreateAccount is BaseTest {
AccountFactoryMultiSteps internal factory;
address internal implementation;

// copy here the event definition from the contract
// @dev: once we bump to 0.8.21, import the event from the contract
event AccountCreated(bytes32 loginHash, address account);

function setUp() external {
factory = new AccountFactory(address(0), address(0), address(0));
factory = new AccountFactoryMultiSteps(address(0), address(0), address(0));
implementation = factory.accountImplementation();
}

Expand Down
File renamed without changes.

0 comments on commit f6c539b

Please sign in to comment.