diff --git a/script/AccountFactory/CreateAccount.s.sol b/script/AccountFactory/CreateAccount.s.sol index 1a99802..0d96f48 100644 --- a/script/AccountFactory/CreateAccount.s.sol +++ b/script/AccountFactory/CreateAccount.s.sol @@ -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); } } diff --git a/script/AccountFactory/Deploy.s.sol b/script/AccountFactory/DeployFactory.s.sol similarity index 100% rename from script/AccountFactory/Deploy.s.sol rename to script/AccountFactory/DeployFactory.s.sol diff --git a/script/AccountFactory/DeployFactoryMultiSteps.s.sol b/script/AccountFactory/DeployFactoryMultiSteps.s.sol new file mode 100644 index 0000000..bdded15 --- /dev/null +++ b/script/AccountFactory/DeployFactoryMultiSteps.s.sol @@ -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 --ledger --sender [--broadcast] + + + ℹ️ HOW TO USE THIS SCRIPT WITH AN ARBITRARY PRIVATE KEY (NOT RECOMMENDED): + PRIVATE_KEY= forge script AccountFactoryMultiStepsDeploy --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" + +*/ diff --git a/src/AccountFactory.sol b/src/AccountFactory.sol index 4012ef3..67aee81 100644 --- a/src/AccountFactory.sol +++ b/src/AccountFactory.sol @@ -12,8 +12,8 @@ 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 { @@ -21,7 +21,6 @@ contract AccountFactory { 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); @@ -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 @@ -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 // diff --git a/src/AccountFactoryMultiSteps.sol b/src/AccountFactoryMultiSteps.sol new file mode 100644 index 0000000..d02134e --- /dev/null +++ b/src/AccountFactoryMultiSteps.sol @@ -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); + } +} diff --git a/test/integration/AccountFactory/deterministicDeployments.t.sol b/test/integration/AccountFactory/deterministicDeployments.t.sol index 91cf309..1a70b85 100644 --- a/test/integration/AccountFactory/deterministicDeployments.t.sol +++ b/test/integration/AccountFactory/deterministicDeployments.t.sol @@ -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 @@ -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( @@ -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); diff --git a/test/integration/AccountFactory/deterministicDeployments.tree b/test/integration/AccountFactory/deterministicDeployments.tree index a3bad89..d9f05f9 100644 --- a/test/integration/AccountFactory/deterministicDeployments.tree +++ b/test/integration/AccountFactory/deterministicDeployments.tree @@ -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 diff --git a/test/unit/AccountFactory/createAccount.t.sol b/test/unit/AccountFactoryMultiSteps/createAccount.t.sol similarity index 92% rename from test/unit/AccountFactory/createAccount.t.sol rename to test/unit/AccountFactoryMultiSteps/createAccount.t.sol index e911135..0e18b0f 100644 --- a/test/unit/AccountFactory/createAccount.t.sol +++ b/test/unit/AccountFactoryMultiSteps/createAccount.t.sol @@ -1,11 +1,11 @@ // 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 @@ -13,7 +13,7 @@ contract AccountFactory__CreateAccount is BaseTest { 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(); } diff --git a/test/unit/AccountFactory/createAccount.tree b/test/unit/AccountFactoryMultiSteps/createAccount.tree similarity index 100% rename from test/unit/AccountFactory/createAccount.tree rename to test/unit/AccountFactoryMultiSteps/createAccount.tree