From 54cd9bdf4ceba9232823484ca95c556cb4f4f2d9 Mon Sep 17 00:00:00 2001 From: qd-qd Date: Tue, 2 Apr 2024 19:12:20 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20make=20account=20upgreadable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the UUPS upgradeability pattern for the Account contract. This commit also reorg account functions and rename the `VERSION` function to `version` to be more consistent with the rest of system. --- script/Account/01_AccountDeploy.s.sol | 2 +- .../01_FactoryDeployImplementation.s.sol | 2 +- src/v1/Account/SmartAccount.sol | 258 +++++++-------- test/unit/v1/Account/upgradeable.t.sol | 293 ++++++++++++++++++ test/unit/v1/Account/upgradeable.tree | 10 + test/unit/v1/Account/versionning.t.sol | 2 +- 6 files changed, 440 insertions(+), 127 deletions(-) create mode 100644 test/unit/v1/Account/upgradeable.t.sol create mode 100644 test/unit/v1/Account/upgradeable.tree diff --git a/script/Account/01_AccountDeploy.s.sol b/script/Account/01_AccountDeploy.s.sol index 0daa0fe..8eb4819 100644 --- a/script/Account/01_AccountDeploy.s.sol +++ b/script/Account/01_AccountDeploy.s.sol @@ -46,7 +46,7 @@ contract SmartAccountDeploy is BaseScript { SmartAccount account = new SmartAccount(entryPointAddress, verifier); // 3. Check the version of the account factory is the expected one - require(Metadata.VERSION == account.VERSION(), "Version mismatch"); + require(Metadata.VERSION == account.version(), "Version mismatch"); return account; } } diff --git a/script/AccountFactory/01_FactoryDeployImplementation.s.sol b/script/AccountFactory/01_FactoryDeployImplementation.s.sol index b9dc9e1..e73b265 100644 --- a/script/AccountFactory/01_FactoryDeployImplementation.s.sol +++ b/script/AccountFactory/01_FactoryDeployImplementation.s.sol @@ -16,7 +16,7 @@ contract FactoryDeployImplementation is BaseScript { require(address(accountImplementation).code.length > 0, "Account not deployed"); // 2. Check the version of the account is the expected one - require(Metadata.VERSION == SmartAccount(accountImplementation).VERSION(), "Version mismatch"); + require(Metadata.VERSION == SmartAccount(accountImplementation).version(), "Version mismatch"); // 3. Confirm the account implementation address with the user string memory prompt = string( diff --git a/src/v1/Account/SmartAccount.sol b/src/v1/Account/SmartAccount.sol index 9723480..2d36688 100644 --- a/src/v1/Account/SmartAccount.sol +++ b/src/v1/Account/SmartAccount.sol @@ -5,6 +5,7 @@ import { IEntryPoint } from "@eth-infinitism/interfaces/IEntryPoint.sol"; import { UserOperation } from "@eth-infinitism/interfaces/UserOperation.sol"; import { BaseAccount } from "@eth-infinitism/core/BaseAccount.sol"; import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol"; import { IWebAuthn256r1 } from "@webauthn/IWebAuthn256r1.sol"; import { UV_FLAG_MASK } from "@webauthn/utils.sol"; import { SignerVaultWebAuthnP256R1 } from "src/utils/SignerVaultWebAuthnP256R1.sol"; @@ -14,34 +15,23 @@ import { Metadata } from "src/v1/Metadata.sol"; import { SmartAccountTokensSupport } from "src/v1/Account/SmartAccountTokensSupport.sol"; import { SmartAccountEIP1271 } from "src/v1/Account/SmartAccountEIP1271.sol"; -/** - * TODO: - * - Take a look to proxy's versions - * - New nonce serie per entrypoint? In that case, first addFirstSigner - */ -contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, SmartAccountEIP1271 { - // ============================== - // ========= METADATA =========== - // ============================== - - uint256 public constant VERSION = Metadata.VERSION; - - // ============================== - // ========= CONSTANTS ========== - // ============================== +contract SmartAccount is Initializable, UUPSUpgradeable, BaseAccount, SmartAccountTokensSupport, SmartAccountEIP1271 { + // ====================================== + // ============= CONSTANTS ============== + // ====================================== address public immutable webAuthnVerifierAddress; address internal immutable entryPointAddress; - // ============================== - // =========== STATE ============ - // ============================== + // ====================================== + // =============== STATE ================ + // ====================================== address internal factoryAddress; - // ============================== - // ======= EVENTS/ERRORS ======== - // ============================== + // ====================================== + // =========== EVENTS/ERRORS ============ + // ====================================== /// @notice Emitted every time a signer is added to the account /// @dev The credIdHash is indexed to allow off-chain services to track account with same signer authorized @@ -65,9 +55,9 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, /// @dev `values` can be of length 0 if no value is passed to the calls error IncorrectExecutionBatchParameters(); - // ============================== - // ======= CONSTRUCTION ========= - // ============================== + // ====================================== + // ============ CONSTRUCTION ============= + // ====================================== /// @dev Do not store any state in this function as the contract will be proxified, only immutable variables /// @param _entryPoint The address of the 4337 entrypoint used by this implementation @@ -99,9 +89,9 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, _addWebAuthnSigner(credIdHash, pubX, pubY, credId); } - // ============================== - // ========= MODIFIER =========== - // ============================== + // ====================================== + // ============= MODIFIER =============== + // ====================================== /// @notice This modifier ensure the caller is the 4337 entrypoint stored modifier onlyEntrypoint() { @@ -119,30 +109,9 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, _; } - // ============================== - // ======== FUNCTIONS =========== - // ============================== - - /// @notice Allow the contract to receive native tokens - // solhint-disable-next-line no-empty-blocks - receive() external payable { } - - /// @notice Return the entrypoint used by this implementation - function entryPoint() public view override returns (IEntryPoint) { - return IEntryPoint(entryPointAddress); - } - - /// @notice Return the factory that initialized this contract - /// @return The address of the factory - function factory() external view returns (address) { - return factoryAddress; - } - - /// @notice Return the webauthn verifier used by this contract - /// @return The address of the webauthn verifier - function webAuthnVerifier() external view returns (address) { - return webAuthnVerifierAddress; - } + // ====================================== + // ========= INTERNAL FUNCTIONS ========= + // ====================================== /// @notice Used internally to get the webauthn verifier /// @return The 256r1 webauthn verifier @@ -150,59 +119,6 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, return IWebAuthn256r1(webAuthnVerifierAddress); } - /// @notice Extract the signer from the authenticatorData - /// @dev This function is free to be called (!!) - /// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer - /// @return credId The credential ID, uniquely identifying the signer. - /// @return credIdHash The hash of the credential ID, uniquely identifying the signer. - /// @return pubkeyX The X coordinate of the signer's public key. - /// @return pubkeyY The Y coordinate of the signer's public key. - function extractSignerFromAuthData(bytes calldata authenticatorData) - public - pure - virtual - returns (bytes memory credId, bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY) - { - (credId, credIdHash, pubkeyX, pubkeyY) = SignerVaultWebAuthnP256R1.extractSignerFromAuthData(authenticatorData); - } - - /// @notice Remove an existing Webauthn p256r1. - /// @dev This function can only be called by the account itself. The whole 4337 workflow must be respected - /// @param credIdHash The hash of the credential ID associated to the signer - function removeWebAuthnP256R1Signer(bytes32 credIdHash) external virtual onlySelf { - // 1. get the current public key stored - (uint256 pubkeyX, uint256 pubkeyY) = SignerVaultWebAuthnP256R1.pubkey(credIdHash); - - // 2. remove the signer from the vault - SignerVaultWebAuthnP256R1.remove(credIdHash); - - // 3. emit the event with the removed signer - emit SignerRemoved(Signature.Type.WEBAUTHN_P256R1, credIdHash, pubkeyX, pubkeyY); - } - - /// @notice Add a Webauthn p256r1 new signer to the account - /// @dev This function can only be called by the account itself. The whole 4337 workflow must be respected - /// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer - function addWebAuthnP256R1Signer(bytes calldata authenticatorData) - external - virtual - onlySelf - returns (bytes32, uint256, uint256, bytes memory) - { - // 1. verify the UV is set in the authenticatorData - if ((authenticatorData[32] & UV_FLAG_MASK) == 0) revert InvalidSignerAddition(); - - // 2. extract the signer from the authenticatorData - (bytes memory credId, bytes32 credIdHash, uint256 pubX, uint256 pubY) = - extractSignerFromAuthData(authenticatorData); - - // 3. set the signer in the vault - _addWebAuthnSigner(credIdHash, pubX, pubY, credId); - - // 4. return the signer - return (credIdHash, pubX, pubY, credId); - } - /// @notice Set a new Webauthn p256r1 new signer and emit the expected event. This function /// can not override an existing signer, use `remnoveWebAuthnP256R1Signer` for this /// @param credIdHash The hash of the credential ID associated to the signer @@ -224,21 +140,6 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, emit SignerAdded(Signature.Type.WEBAUTHN_P256R1, credId, credIdHash, pubkeyX, pubkeyY); } - /// @notice Return a signer stored in the account using its credIdHash. When storing a signer, the credId - /// is hashed using keccak256 because its length is unpredictable. - /// @dev This function is free to be called (!!) - /// @param _credIdHash The hash of the credential ID, uniquely identifying the signer. - /// @return credIdHash The hash of the credential ID, uniquely identifying the signer. - /// @return pubkeyX The X coordinate of the signer's public key. - /// @return pubkeyY The Y coordinate of the signer's public key. - function getSigner(bytes32 _credIdHash) - external - view - returns (bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY) - { - (credIdHash, pubkeyX, pubkeyY) = SignerVaultWebAuthnP256R1.get(_credIdHash); - } - /// @notice The validation of the creation signature /// @dev This creation signature is the signature that can only be used once during account creation (nonce == 0). /// This signature is different from the ones the account will use for validation for the rest of its lifetime. @@ -298,7 +199,7 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, // the signature. The expected challenge is constructed on-chain using the data from the userOp and // the environment (entrypoint address, chainid, this contract address)... /// @param userOp The user operation to validate - function _validateWebAuthnP256R1Signature(UserOperation calldata userOp) internal returns (uint256) { + function _validateWebAuthnP256R1Signature(UserOperation calldata userOp) internal virtual returns (uint256) { // 1. decode the signature ( /*identifier*/ , bytes memory authData, bytes memory clientData, uint256 r, uint256 s, bytes32 credIdHash) = abi.decode(userOp.signature, (bytes1, bytes, bytes, uint256, uint256, bytes32)); @@ -334,6 +235,7 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, bytes32 // userOpHash ) internal + virtual override returns (uint256 validationData) { @@ -352,14 +254,12 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, return Signature.State.FAILURE; } - // *********** EXECUTE ***********// - /// @notice Execute a transaction /// @dev Revert if the call fails /// @param target The address of the contract to call /// @param value The value to pass in this call /// @param data The calldata to pass in this call (selector + encoded arguments) - function _call(address target, uint256 value, bytes calldata data) internal { + function _call(address target, uint256 value, bytes calldata data) internal virtual { (bool success, bytes memory result) = target.call{ value: value }(data); if (!success) { assembly { @@ -368,12 +268,121 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, } } + // ====================================== + // ======= EXTERNAL FREE FUNCTIONS ====== + // ====================================== + + /// @notice Allow the contract to receive native tokens + // solhint-disable-next-line no-empty-blocks + receive() external payable { } + + function version() external pure virtual returns (uint256) { + return Metadata.VERSION; + } + + /// @notice Return the entrypoint used by this implementation + function entryPoint() public view override returns (IEntryPoint) { + return IEntryPoint(entryPointAddress); + } + + /// @notice Return the factory that initialized this contract + /// @return The address of the factory + function factory() external view returns (address) { + return factoryAddress; + } + + /// @notice Return the webauthn verifier used by this contract + /// @return The address of the webauthn verifier + function webAuthnVerifier() external view returns (address) { + return webAuthnVerifierAddress; + } + + /// @notice Extract the signer from the authenticatorData + /// @dev This function is free to be called (!!) + /// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer + /// @return credId The credential ID, uniquely identifying the signer. + /// @return credIdHash The hash of the credential ID, uniquely identifying the signer. + /// @return pubkeyX The X coordinate of the signer's public key. + /// @return pubkeyY The Y coordinate of the signer's public key. + function extractSignerFromAuthData(bytes calldata authenticatorData) + public + pure + virtual + returns (bytes memory credId, bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY) + { + (credId, credIdHash, pubkeyX, pubkeyY) = SignerVaultWebAuthnP256R1.extractSignerFromAuthData(authenticatorData); + } + + /// @notice Return a signer stored in the account using its credIdHash. When storing a signer, the credId + /// is hashed using keccak256 because its length is unpredictable. + /// @dev This function is free to be called (!!) + /// @param _credIdHash The hash of the credential ID, uniquely identifying the signer. + /// @return credIdHash The hash of the credential ID, uniquely identifying the signer. + /// @return pubkeyX The X coordinate of the signer's public key. + /// @return pubkeyY The Y coordinate of the signer's public key. + function getSigner(bytes32 _credIdHash) + external + view + virtual + returns (bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY) + { + (credIdHash, pubkeyX, pubkeyY) = SignerVaultWebAuthnP256R1.get(_credIdHash); + } + + // ====================================== + // ===== EXTERNAL ITSELF FUNCTIONS ====== + // ====================================== + + /// @notice Remove an existing Webauthn p256r1. + /// @dev This function can only be called by the account itself. The whole 4337 workflow must be respected + /// @param credIdHash The hash of the credential ID associated to the signer + function removeWebAuthnP256R1Signer(bytes32 credIdHash) external virtual onlySelf { + // 1. get the current public key stored + (uint256 pubkeyX, uint256 pubkeyY) = SignerVaultWebAuthnP256R1.pubkey(credIdHash); + + // 2. remove the signer from the vault + SignerVaultWebAuthnP256R1.remove(credIdHash); + + // 3. emit the event with the removed signer + emit SignerRemoved(Signature.Type.WEBAUTHN_P256R1, credIdHash, pubkeyX, pubkeyY); + } + + /// @notice Add a Webauthn p256r1 new signer to the account + /// @dev This function can only be called by the account itself. The whole 4337 workflow must be respected + /// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer + function addWebAuthnP256R1Signer(bytes calldata authenticatorData) + external + virtual + onlySelf + returns (bytes32, uint256, uint256, bytes memory) + { + // 1. verify the UV is set in the authenticatorData + if ((authenticatorData[32] & UV_FLAG_MASK) == 0) revert InvalidSignerAddition(); + + // 2. extract the signer from the authenticatorData + (bytes memory credId, bytes32 credIdHash, uint256 pubX, uint256 pubY) = + extractSignerFromAuthData(authenticatorData); + + // 3. set the signer in the vault + _addWebAuthnSigner(credIdHash, pubX, pubY, credId); + + // 4. return the signer + return (credIdHash, pubX, pubY, credId); + } + + /// @notice authorize account upgrade to a new implementation if the caller is the account itself + function _authorizeUpgrade(address) internal virtual override onlySelf { } + + // ====================================== + // === EXTERNAL ENTRYPOINT FUNCTIONS ==== + // ====================================== + /// @notice Execute a transaction if called by the entrypoint /// @dev Revert if the call fails /// @param target The address of the contract to call /// @param value The value to pass in this call /// @param data The calldata to pass in this call (selector + encoded arguments) - function execute(address target, uint256 value, bytes calldata data) external onlyEntrypoint { + function execute(address target, uint256 value, bytes calldata data) external virtual onlyEntrypoint { _call(target, value, data); } @@ -388,6 +397,7 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport, bytes[] calldata datas ) external + virtual onlyEntrypoint { // 1. check the length of the parameters is correct. Note that `values` can be of length 0 if no value is passed diff --git a/test/unit/v1/Account/upgradeable.t.sol b/test/unit/v1/Account/upgradeable.t.sol new file mode 100644 index 0000000..8c8e003 --- /dev/null +++ b/test/unit/v1/Account/upgradeable.t.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: APACHE-2.0 +pragma solidity >=0.8.20 <0.9.0; + +import { BaseTest } from "test/BaseTest/BaseTest.sol"; +import { SmartAccount, Initializable, UUPSUpgradeable, Metadata } from "src/v1/Account/SmartAccount.sol"; +import { AccountFactory } from "src/v1/AccountFactory.sol"; + +contract SmartAccount__Upgradeable is BaseTest { + address internal entryPoint; + address internal verifier; + + AccountFactory internal factory; + SmartAccount internal account; + + function setUp() external setUpCreateFixture { + // 1. set the entrypoint and the verifier + entryPoint = makeAddr("entrypoint"); + verifier = makeAddr("verifier"); + + // 2. deploy an implementation of the account + address accountImplementation = address(new SmartAccount(entryPoint, verifier)); + + // 3. deploy the implementation of the factory and one instance + address factoryImplementation = address(deployFactoryImplementation(payable(accountImplementation))); + factory = deployFactoryInstance(factoryImplementation, makeAddr("proxy-owner"), SMOOTH_SIGNER.addr); + + // 4. get the address of the future account + address accountFutureAddress = factory.getAddress(createFixtures.response.authData); + + // 5. craft a valid deployment signature + bytes memory signature = craftDeploymentSignature(createFixtures.response.authData, accountFutureAddress); + + // 6. deploy an account instance and set the first signer + account = SmartAccount(payable(factory.createAndInitAccount(createFixtures.response.authData, signature))); + } + + function test_CanBeUpgradedWithoutData() external { + // it can be upgraded to another implementation + + // 1. deploy a new implementation of the account + address newEntryPoint = makeAddr("new-entrypoint"); + SmartAccountV2 newAccountImplementation = new SmartAccountV2(newEntryPoint, makeAddr("new-verifier")); + + // 2. fetch the entrypoint of the current account + address currentEntryPoint = address(account.entryPoint()); + + // 3. upgrade the account to the new implementation + vm.prank(currentEntryPoint); + account.execute( + address(account), + 0, + abi.encodeWithSelector(UUPSUpgradeable.upgradeToAndCall.selector, address(newAccountImplementation), "") + ); + + // 4. make sure the account has been upgraded + assertEq(address(account.entryPoint()), newEntryPoint); + } + + function test_CanBeUpgradedWithData() external { + // it can be upgraded to another implementation + + // 1. deploy a new implementation of the account + address newEntryPoint = makeAddr("new-entrypoint"); + SmartAccountV2 newAccountImplementation = new SmartAccountV2(newEntryPoint, makeAddr("new-verifier")); + + // 2. fetch the entrypoint of the current account + address currentEntryPoint = address(account.entryPoint()); + + // 3. tell the VM to expect a call + vm.expectCall( + address(newAccountImplementation), + abi.encodeWithSelector(SmartAccountV2.correctInitialize.selector, address(66)), + 1 + ); + + // 4. upgrade the account to the new implementation + vm.prank(currentEntryPoint); + account.execute( + address(account), + 0, + abi.encodeWithSelector( + UUPSUpgradeable.upgradeToAndCall.selector, + address(newAccountImplementation), + abi.encodeWithSelector(SmartAccountV2.correctInitialize.selector, address(66)) + ) + ); + + // 5. make sure the account has been upgraded + assertEq(address(account.entryPoint()), newEntryPoint); + } + + function test_RevertIfNotInitiatedByItself(address caller) external { + // it revert if not initiated by itself + + // 1. make sure the fuzzed address is different from the entrypoint + vm.assume(caller != entryPoint); + + // 2. deploy a new implementation of the account + SmartAccount newAccountImplementation = new SmartAccount(makeAddr("new-entrypoint"), makeAddr("new-verifier")); + + // 3. tell the VM to expect a revert + vm.expectRevert("account: not from EntryPoint"); + + // 4. try to upgrade the account to the new implementation -- must revert + vm.prank(caller); + account.execute( + address(account), + 0, + abi.encodeWithSelector(UUPSUpgradeable.upgradeToAndCall.selector, address(newAccountImplementation), "") + ); + } + + function test_RevertIfReinitializerIsEqualOrBelow() external { + // it revert if reinitializer is equal or below + + // 1. deploy a new implementation of the account + address newEntryPoint = makeAddr("new-entrypoint"); + SmartAccountV2 newAccountImplementation = new SmartAccountV2(newEntryPoint, makeAddr("new-verifier")); + + // 2. fetch the entrypoint of the current account + address currentEntryPoint = address(account.entryPoint()); + + // 3. tell the VM to expect a call + vm.expectCall( + address(newAccountImplementation), + abi.encodeWithSelector(SmartAccountV2.incorrectInitialize.selector, address(12)), + 1 + ); + + // 4. tell the VM to expect a revert + vm.expectRevert(Initializable.InvalidInitialization.selector); + + // 5. upgrade the account to the new implementation + vm.prank(currentEntryPoint); + account.execute( + address(account), + 0, + abi.encodeWithSelector( + UUPSUpgradeable.upgradeToAndCall.selector, + address(newAccountImplementation), + abi.encodeWithSelector(SmartAccountV2.incorrectInitialize.selector, address(12)) + ) + ); + + // 6. make sure the account has not been upgraded + assertEq(address(account.entryPoint()), currentEntryPoint); + } + + function test_MaintainsTheSignersStored() external { + // it maintains the signers stored + + // 1. fetch the current stored signer + (bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY) = + account.getSigner(keccak256(createFixtures.signer.credId)); + assertNotEq(credIdHash, bytes32(0)); + assertNotEq(pubkeyX, 0); + assertNotEq(pubkeyY, 0); + + // 2. deploy a new implementation of the account + + SmartAccountV2 newAccountImplementation = + new SmartAccountV2(makeAddr("new-entrypoint"), makeAddr("new-verifier")); + + // 3. upgrade the account to the new implementation + vm.prank(entryPoint); + account.execute( + address(account), + 0, + abi.encodeWithSelector(UUPSUpgradeable.upgradeToAndCall.selector, address(newAccountImplementation), "") + ); + + // 4. fetch the stored signer from the new implementation + (bytes32 newCredIdHash, uint256 newPubkeyX, uint256 newPubkeyY) = + account.getSigner(keccak256(createFixtures.signer.credId)); + + // 5. make sure the stored signer has not changed + assertEq(newCredIdHash, credIdHash); + assertEq(newPubkeyX, pubkeyX); + assertEq(newPubkeyY, pubkeyY); + } + + function test_MaintainsTheFactoryAddressStored() external { + // it maintains the factory address stored + + // 1. fetch the current factory + address currentFactory = account.factory(); + assertNotEq(currentFactory, address(0)); + + // 2. deploy a new implementation of the account + SmartAccountV2 newAccountImplementation = + new SmartAccountV2(makeAddr("new-entrypoint"), makeAddr("new-verifier")); + + // 3. upgrade the account to the new implementation + vm.prank(entryPoint); + account.execute( + address(account), + 0, + abi.encodeWithSelector(UUPSUpgradeable.upgradeToAndCall.selector, address(newAccountImplementation), "") + ); + + // 4. make sure the stored factory has not changed + assertEq(currentFactory, account.factory()); + } + + function test_CanUpdateTheWebauthnVerifier() external { + // it can update the webauthn verifier + + // 1. deploy a new implementation of the account + address newVerifier = makeAddr("new-verifier"); + SmartAccountV2 newAccountImplementation = new SmartAccountV2(makeAddr("new-entrypoint"), newVerifier); + + // 2. upgrade the account to the new implementation + vm.prank(entryPoint); + account.execute( + address(account), + 0, + abi.encodeWithSelector(UUPSUpgradeable.upgradeToAndCall.selector, address(newAccountImplementation), "") + ); + + // 3. make sure the account has been upgraded + assertEq(SmartAccountV2(payable(address(account))).exposed_webauthn256R1Verifier(), newVerifier); + } + + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint64 version); + + function test_EmitAnEvent() external { + // it emits an event + + // 1. deploy a new implementation of the account + SmartAccountV2 newAccountImplementation = + new SmartAccountV2(makeAddr("new-entrypoint"), makeAddr("new-verifier")); + + // 2. we tell the VM to expect an event + vm.expectEmit(true, false, false, true, address(account)); + emit Initialized(2); + + // 3. upgrade the account to the new implementation + vm.prank(address(account.entryPoint())); + account.execute( + address(account), + 0, + abi.encodeWithSelector( + UUPSUpgradeable.upgradeToAndCall.selector, + address(newAccountImplementation), + abi.encodeWithSelector(SmartAccountV2.correctInitialize.selector, address(66)) + ) + ); + } + + function test_CanUpdateTheVersion() external { + // it can update the version + + // 1. fetch the current version + uint256 currentVersion = account.version(); + + // 2. deploy a new implementation of the account + SmartAccountV2 newAccountImplementation = + new SmartAccountV2(makeAddr("new-entrypoint"), makeAddr("new-verifier")); + + // 3. upgrade the account to the new implementation + vm.prank(entryPoint); + account.execute( + address(account), + 0, + abi.encodeWithSelector(UUPSUpgradeable.upgradeToAndCall.selector, address(newAccountImplementation), "") + ); + + // 4. make sure the version of the account has been updated + assert(currentVersion < account.version()); + } +} + +contract SmartAccountV2 is SmartAccount { + constructor(address _entrypoint, address _verifier) SmartAccount(_entrypoint, _verifier) { } + + // expected to work as expected as the version is higher than the curret one + function correctInitialize(address) external reinitializer(2) { } + + // expected to revert as the version is equal to the current one + function incorrectInitialize() external reinitializer(1) { } + + function exposed_webauthn256R1Verifier() external view returns (address) { + return address(webauthn256R1Verifier()); + } + + // increase the version by 1_000_000 + function version() external pure virtual override returns (uint256) { + return Metadata.VERSION + 1_000_000; + } +} diff --git a/test/unit/v1/Account/upgradeable.tree b/test/unit/v1/Account/upgradeable.tree new file mode 100644 index 0000000..b4c764e --- /dev/null +++ b/test/unit/v1/Account/upgradeable.tree @@ -0,0 +1,10 @@ +SmartAccount__Upgradeable +├── it can be upgraded with data +├── it can be upgraded without data +├── it revert if not initiated by itself +├── it revert if reinitializer is equal or below +├── it maintains the signers stored +├── it maintains the factory address stored +├── it can update the webauthn verifier +├── it emit an event +└── it can update the version diff --git a/test/unit/v1/Account/versionning.t.sol b/test/unit/v1/Account/versionning.t.sol index 12f71bc..5b41604 100644 --- a/test/unit/v1/Account/versionning.t.sol +++ b/test/unit/v1/Account/versionning.t.sol @@ -15,6 +15,6 @@ contract SmartAccount__Versionning is BaseTest { function test_AllowVersionFetching() external { // it allow version fetching - assertEq(account.VERSION(), Metadata.VERSION); + assertEq(account.version(), Metadata.VERSION); } } From 487574fe62afd3712cbfd09e4e64b61d6f5f410d Mon Sep 17 00:00:00 2001 From: qd-qd Date: Tue, 2 Apr 2024 19:27:15 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9A=9A=20rename=20account=20deploymen?= =?UTF-8?q?t=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...1_AccountDeploy.s.sol => 01_AccountDeployImplementation.s.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename script/Account/{01_AccountDeploy.s.sol => 01_AccountDeployImplementation.s.sol} (100%) diff --git a/script/Account/01_AccountDeploy.s.sol b/script/Account/01_AccountDeployImplementation.s.sol similarity index 100% rename from script/Account/01_AccountDeploy.s.sol rename to script/Account/01_AccountDeployImplementation.s.sol