diff --git a/contracts/test/TestBloctoAccountCloneableWalletV200.sol b/contracts/test/TestBloctoAccountCloneableWalletV200.sol index a6df5fe..dab038d 100644 --- a/contracts/test/TestBloctoAccountCloneableWalletV200.sol +++ b/contracts/test/TestBloctoAccountCloneableWalletV200.sol @@ -7,7 +7,7 @@ import "./TestBloctoAccountV200.sol"; /// @notice This contract represents a complete but non working wallet. contract TestBloctoAccountCloneableWalletV200 is TestBloctoAccountV200 { /// @dev Cconstructor that deploys a NON-FUNCTIONAL version of `TestBloctoAccountV140` - constructor(IEntryPoint anEntryPoint) TestBloctoAccountV200(anEntryPoint) { + constructor(IEntryPoint anEntryPoint, address moduleManager) TestBloctoAccountV200(anEntryPoint, moduleManager) { initialized = true; } } diff --git a/contracts/test/TestBloctoAccountV200.sol b/contracts/test/TestBloctoAccountV200.sol index 1c80252..92298ad 100644 --- a/contracts/test/TestBloctoAccountV200.sol +++ b/contracts/test/TestBloctoAccountV200.sol @@ -27,7 +27,7 @@ contract TestBloctoAccountV200 is UUPSUpgradeable, TokenCallbackHandler, CoreWal * constructor for BloctoAccount * @param anEntryPoint entrypoint address */ - constructor(IEntryPoint anEntryPoint) { + constructor(IEntryPoint anEntryPoint, address moduleManager) CoreWallet(moduleManager) { _entryPoint = anEntryPoint; } diff --git a/contracts/v1.5.x/BloctoAccount.sol b/contracts/v1.5.x/BloctoAccount.sol index a82f36f..3dcef9c 100644 --- a/contracts/v1.5.x/BloctoAccount.sol +++ b/contracts/v1.5.x/BloctoAccount.sol @@ -19,7 +19,7 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas /** * This is the version of this contract. */ - string public constant VERSION = "1.5.3"; + string public constant VERSION = "1.5.4"; /// @notice entrypoint from 4337 official IEntryPoint private immutable _entryPoint; @@ -30,8 +30,9 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas /** * constructor for BloctoAccount * @param anEntryPoint entrypoint address + * @param moduleManager module manager address */ - constructor(IEntryPoint anEntryPoint) { + constructor(IEntryPoint anEntryPoint, address moduleManager) CoreWallet(moduleManager) { _entryPoint = anEntryPoint; } diff --git a/contracts/v1.5.x/BloctoAccountCloneableWallet.sol b/contracts/v1.5.x/BloctoAccountCloneableWallet.sol index aca33b0..7bdb18d 100644 --- a/contracts/v1.5.x/BloctoAccountCloneableWallet.sol +++ b/contracts/v1.5.x/BloctoAccountCloneableWallet.sol @@ -8,7 +8,8 @@ import "./BloctoAccount.sol"; contract BloctoAccountCloneableWallet is BloctoAccount { /// @notice constructor that deploys a NON-FUNCTIONAL version of `BloctoAccount` /// @param anEntryPoint entrypoint address - constructor(IEntryPoint anEntryPoint) BloctoAccount(anEntryPoint) { + /// @param moduleManager module manager address + constructor(IEntryPoint anEntryPoint, address moduleManager) BloctoAccount(anEntryPoint, moduleManager) { initialized = true; initializedImplementation = true; } diff --git a/contracts/v1.5.x/BloctoAccountFactory.sol b/contracts/v1.5.x/BloctoAccountFactory.sol index 89b3ce8..8fbf4ce 100644 --- a/contracts/v1.5.x/BloctoAccountFactory.sol +++ b/contracts/v1.5.x/BloctoAccountFactory.sol @@ -3,11 +3,15 @@ pragma solidity 0.8.17; import "./BloctoAccountFactoryV1_5_2.sol"; import "./BloctoAccountFactoryV1_5_3.sol"; +import "./BloctoAccountFactoryV1_5_4.sol"; // BloctoAccountFactory for creating BloctoAccountProxy -contract BloctoAccountFactory is BloctoAccountFactoryV1_5_2, BloctoAccountFactoryV1_5_3 { +contract BloctoAccountFactory is BloctoAccountFactoryV1_5_2, BloctoAccountFactoryV1_5_3, BloctoAccountFactoryV1_5_4 { /// @notice this is the version of this contract. - string public constant VERSION = "1.5.3"; + string public constant VERSION = "1.5.4"; - constructor(address _account_1_5_3) BloctoAccountFactoryV1_5_3(_account_1_5_3) {} + constructor(address _account_1_5_3, address _account_1_5_4) + BloctoAccountFactoryV1_5_3(_account_1_5_3) + BloctoAccountFactoryV1_5_4(_account_1_5_4) + {} } diff --git a/contracts/v1.5.x/BloctoAccountFactoryV1_5_4.sol b/contracts/v1.5.x/BloctoAccountFactoryV1_5_4.sol new file mode 100644 index 0000000..8969d9f --- /dev/null +++ b/contracts/v1.5.x/BloctoAccountFactoryV1_5_4.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.17; + +import "./BloctoAccountFactoryBase.sol"; + +// BloctoAccountFactory for creating BloctoAccountProxy +contract BloctoAccountFactoryV1_5_4 is BloctoAccountFactoryBase { + //---------------------------V1.5.4---------------------------// + address public immutable bloctoAccountImplementation_1_5_4; + + constructor(address _account_1_5_4) { + bloctoAccountImplementation_1_5_4 = _account_1_5_4; + } + + /// @notice create an account, and return its BloctoAccount. note: diretly use _salt to create account + /// @param _authorizedAddress the initial authorized address, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParity the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKey the corresponding mergedKey (using Schnorr merged key) + function createAccount_1_5_4( + address _authorizedAddress, + address _cosigner, + address _recoveryAddress, + bytes32 _salt, + uint8 _mergedKeyIndexWithParity, + bytes32 _mergedKey + ) public onlyCreateAccountRole returns (BloctoAccount ret) { + // to be consistent address + address newProxy = + Create2.deploy(0, _salt, abi.encodePacked(BLOCTO_ACCOUNT_PROXY, abi.encode(address(initImplementation)))); + ret = BloctoAccount(payable(newProxy)); + ret.initImplementation(bloctoAccountImplementation_1_5_4); + ret.init( + _authorizedAddress, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParity, _mergedKey + ); + emit WalletCreated(address(ret), _authorizedAddress, false); + } + + /// @notice create an account with multiple authorized addresses, and return its BloctoAccount. note: diretly use _salt to create account + /// @param _authorizedAddresses the initial authorized addresses, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParitys the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKeys the corresponding mergedKey + function createAccount2_1_5_4( + address[] calldata _authorizedAddresses, + address _cosigner, + address _recoveryAddress, + bytes32 _salt, + uint8[] calldata _mergedKeyIndexWithParitys, + bytes32[] calldata _mergedKeys + ) public onlyCreateAccountRole returns (BloctoAccount ret) { + // to be consistent address + address newProxy = + Create2.deploy(0, _salt, abi.encodePacked(BLOCTO_ACCOUNT_PROXY, abi.encode(address(initImplementation)))); + ret = BloctoAccount(payable(newProxy)); + ret.initImplementation(bloctoAccountImplementation_1_5_4); + ret.init2( + _authorizedAddresses, uint256(uint160(_cosigner)), _recoveryAddress, _mergedKeyIndexWithParitys, _mergedKeys + ); + // emit event only with _authorizedAddresses[0] + emit WalletCreated(address(ret), _authorizedAddresses[0], true); + } + + /// @notice create an account and run first transaction, it combine from createAccount_1_5_4() of this and invoke2() from CoreWallet + /// @dev why Invoke2Data struct? Because 'CompilerError: Stack too deep.' problem, we cannot directly input (_nonce, _data, _signature) + /// @param _authorizedAddress the initial authorized address, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParity the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKey the corresponding mergedKey (using Schnorr merged key) + /// @param _invoke2Data the invoke2 data {nonce, data, signature} + function createAccountWithInvoke2_1_5_4( + // same input as createAccount_1_5_4() of this contract + address _authorizedAddress, + address _cosigner, + address _recoveryAddress, + bytes32 _salt, + uint8 _mergedKeyIndexWithParity, + bytes32 _mergedKey, + // same input as invoke2() of CoreWallet.sol + Invoke2Data calldata _invoke2Data + ) external onlyCreateAccountRole returns (BloctoAccount ret) { + ret = createAccount_1_5_4( + _authorizedAddress, _cosigner, _recoveryAddress, _salt, _mergedKeyIndexWithParity, _mergedKey + ); + ret.invoke2(_invoke2Data.nonce, _invoke2Data.data, _invoke2Data.signature); + } + + /// @notice create an account with multiple devices and run first transaction, it combine from createAccount2_1_5_4() of this and invoke2() from CoreWallet + /// @dev why Invoke2Data struct? Because 'CompilerError: Stack too deep.' problem, we cannot directly input (_nonce, _data, _signature) + /// @param _authorizedAddresses the initial authorized addresses, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParitys the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKeys the corresponding mergedKey + /// @param _invoke2Data the invoke2 data {nonce, data, signature} + function createAccount2WithInvoke2_1_5_4( + // same input as createAccount2_1_5_4() of this contract + address[] calldata _authorizedAddresses, + address _cosigner, + address _recoveryAddress, + bytes32 _salt, + uint8[] calldata _mergedKeyIndexWithParitys, + bytes32[] calldata _mergedKeys, + // same input as invoke2() of CoreWallet.sol + Invoke2Data calldata _invoke2Data + ) external onlyCreateAccountRole returns (BloctoAccount ret) { + ret = createAccount2_1_5_4( + _authorizedAddresses, _cosigner, _recoveryAddress, _salt, _mergedKeyIndexWithParitys, _mergedKeys + ); + ret.invoke2(_invoke2Data.nonce, _invoke2Data.data, _invoke2Data.signature); + } + + /// @notice simulate for creating an account and run first transaction, it combine from createAccount_1_5_1() of this and invoke2() from CoreWallet + /// @param _authorizedAddress the initial authorized address, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParity the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKey the corresponding mergedKey (using Schnorr merged key) + /// @param _invoke2Data the invoke2 data {nonce, data, signature} + function simulateCreateAccountWithInvoke2_1_5_4( + // same input as createAccount_1_5_1() of this contract + address _authorizedAddress, + address _cosigner, + address _recoveryAddress, + bytes32 _salt, + uint8 _mergedKeyIndexWithParity, + bytes32 _mergedKey, + // same input as invoke2() of CoreWallet.sol + Invoke2Data calldata _invoke2Data + ) external onlyCreateAccountRole returns (BloctoAccount ret) { + ret = createAccount_1_5_4( + _authorizedAddress, _cosigner, _recoveryAddress, _salt, _mergedKeyIndexWithParity, _mergedKey + ); + // always revert + try ret.simulateInvoke2(_invoke2Data.nonce, _invoke2Data.data, _invoke2Data.signature) {} + catch (bytes memory reason) { + // NOTE: this ExecutionResult from CoreWallet.sol + // success bytes(100), bytes4 selector from keccak256("ExecutionResult(bool)") 0x2a6b3136 + btyes32 (bool, 0x01) + bytes32 (uint256) + bytes32 (uint256) + if ( + reason.length == 100 && uint8(reason[35]) == 1 + && bytes4(reason) == bytes4(keccak256("ExecutionResult(bool,uint256,uint256)")) + ) { + revert CreateAccountWithInvokeResult(true, gasleft()); + } + } + + revert CreateAccountWithInvokeResult(false, gasleft()); + } + + /// @notice simulate for creating an account with multiple devices and run first transaction, it combine from createAccount2_1_5_1() of this and invoke2() from CoreWallet + /// @param _authorizedAddresses the initial authorized addresses, must not be zero! + /// @param _cosigner the initial cosigning address for `_authorizedAddress`, can be equal to `_authorizedAddress` + /// @param _recoveryAddress the initial recovery address for the wallet, can be address(0) + /// @param _salt salt for create account (used for address calculation in create2) + /// @param _mergedKeyIndexWithParitys the corresponding index of mergedKeys = authVersion + _mergedIndex + /// @param _mergedKeys the corresponding mergedKey + /// @param _invoke2Data the invoke2 data {nonce, data, signature} + function simulateCreateAccount2WithInvoke2_1_5_4( + // same input as createAccount2_1_5_1() of this contract + address[] calldata _authorizedAddresses, + address _cosigner, + address _recoveryAddress, + bytes32 _salt, + uint8[] calldata _mergedKeyIndexWithParitys, + bytes32[] calldata _mergedKeys, + // same input as invoke2() of CoreWallet.sol + Invoke2Data calldata _invoke2Data + ) external onlyCreateAccountRole returns (BloctoAccount ret) { + ret = createAccount2_1_5_4( + _authorizedAddresses, _cosigner, _recoveryAddress, _salt, _mergedKeyIndexWithParitys, _mergedKeys + ); + // always revert + try ret.simulateInvoke2(_invoke2Data.nonce, _invoke2Data.data, _invoke2Data.signature) {} + catch (bytes memory reason) { + // NOTE: this ExecutionResult from CoreWallet.sol + // success bytes(100), bytes4 selector from keccak256("ExecutionResult(bool)") 0x2a6b3136 + btyes32 (bool, 0x01) + bytes32 (uint256) + bytes32 (uint256) + if ( + reason.length == 100 && uint8(reason[35]) == 1 + && bytes4(reason) == bytes4(keccak256("ExecutionResult(bool,uint256,uint256)")) + ) { + revert CreateAccountWithInvokeResult(true, gasleft()); + } + } + + revert CreateAccountWithInvokeResult(false, gasleft()); + } +} diff --git a/contracts/v1.5.x/CoreWallet.sol b/contracts/v1.5.x/CoreWallet.sol index 8cbc309..f2a5fef 100644 --- a/contracts/v1.5.x/CoreWallet.sol +++ b/contracts/v1.5.x/CoreWallet.sol @@ -48,6 +48,9 @@ contract CoreWallet is IERC1271 { /// @notice This is for point and revert flag b01 when any meta tx fail because of atomicity uint256 internal constant FEE_SUCCESS_META_TXS_FAIL = type(uint256).max - 1; + /// @notice module manager address for verifying module address + address public immutable moduleManager; + /// @notice The pre-shifted authVersion (to get the current authVersion as an integer, /// shift this value right by 160 bits). Starts as `1 << 160` (`AUTH_VERSION_INCREMENTOR`) /// See the comment on the `authorizations` variable for how this is used. @@ -109,7 +112,13 @@ contract CoreWallet is IERC1271 { /// for more information about clone contracts. bool public initialized; - error ExecutionResult(bool targetSuccess, uint256 gasLeft); + error ExecutionResult(bool targetSuccess, uint256 startGas, uint256 gasLeft); + + /// @notice Construct of CoreWallet + /// @param imoduleManager immutable module manager address + constructor(address imoduleManager) { + moduleManager = imoduleManager; + } /// @notice Used to decorate methods that can only be called directly by the recovery address. modifier onlyRecoveryAddress() { @@ -613,6 +622,7 @@ contract CoreWallet is IERC1271 { /// @param _data The data containing the transactions to be invoked; see internalInvoke for details. /// @param _signature Signature byte array associated with `_nonce, _data` function simulateInvoke2(uint256 _nonce, bytes calldata _data, bytes calldata _signature) external { + uint256 startGas = gasleft(); // calculate hash bytes32 operationHash = keccak256(abi.encodePacked(EIP191_PREFIX, EIP191_VERSION_DATA, this, block.chainid, _nonce, _data)); @@ -632,7 +642,7 @@ contract CoreWallet is IERC1271 { internalInvoke(operationHash, _data); // always revert - revert ExecutionResult(true, gasleft()); + revert ExecutionResult(true, startGas, gasleft()); } /// @dev Internal invoke @@ -722,7 +732,11 @@ contract CoreWallet is IERC1271 { // the revert call from assembly. string memory invalidLengthMessage = "data field too short"; string memory callFailed = "call failed"; - + string memory unverifiedModule = "unverified module"; + // assembly cannot access immutable variables directly, so we need to load the moduleManager address into memory + address moduleManagerAddr = moduleManager; + // each meta tx success variable + bool success; // At an absolute minimum, the data field must be at least 85 bytes // @@ -743,6 +757,12 @@ contract CoreWallet is IERC1271 { // 52 = to(20) + value(32) let len := mload(add(memPtr, 52)) + // delete call to blocto modules + // shift 12 byters and check if moduleAddr is zero + let moduleAddr := shr(96, len) + // only keep 12 bytes + len := and(len, 0xFFFFFFFFFFFFFFFFFFFFFFFF) + // Compute a pointer to the end of the current operation // 84 = to(20) + value(32) + size(32) let opEnd := add(len, add(memPtr, 84)) @@ -755,11 +775,27 @@ contract CoreWallet is IERC1271 { // The computed end of this operation goes past the end of the data buffer. Not good! revert(add(invalidLengthMessage, 32), mload(invalidLengthMessage)) } - // NOTE: Code that is compatible with solidity-coverage - // switch gt(opEnd, endPtr) - // case 1 { - // revert(add(invalidLengthMessage, 32), mload(invalidLengthMessage)) - // } + + switch moduleAddr + case 0 { + success := call(gas(), shr(96, mload(memPtr)), mload(add(memPtr, 20)), add(memPtr, 84), len, 0, 0) + } + default { + // step 1. is this module verified ? + let isVerifiedAddr := mload(0x40) + // signature of isVerified(address) + let selector := shl(224, 0xb9209e33) + mstore(isVerifiedAddr, selector) + mstore(add(isVerifiedAddr, 0x04), moduleAddr) + // call moduleManager to check if module is verified + let callSuccess := staticcall(gas(), moduleManagerAddr, isVerifiedAddr, 0x24, 0x00, 0x20) + let isVerified := mload(0x00) + if eq(0, and(callSuccess, isVerified)) { + revert(add(unverifiedModule, 32), mload(unverifiedModule)) + } + // step 2. call module + success := delegatecall(gas(), moduleAddr, memPtr, add(len, 84), 0, 0) + } // This line of code packs in a lot of functionality! // - load the target address from memPtr, the address is only 20-bytes but mload always grabs 32-bytes, @@ -769,7 +805,7 @@ contract CoreWallet is IERC1271 { // - use the previously loaded len field as the size of the call data // - make the call (passing all remaining gas to the child call) // - check the result (0 == reverted) - if eq(0, call(gas(), shr(96, mload(memPtr)), mload(add(memPtr, 20)), add(memPtr, 84), len, 0, 0)) { + if eq(0, success) { switch revertFlag case 1 { revert(add(callFailed, 32), mload(callFailed)) } default { diff --git a/contracts/v1.5.x/ModuleManager.sol b/contracts/v1.5.x/ModuleManager.sol new file mode 100644 index 0000000..4cc4ab2 --- /dev/null +++ b/contracts/v1.5.x/ModuleManager.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract ModuleManager is Ownable { + mapping(address => bool) private verifiedModules; + + /// @dev event to log address addition + event ModuleAdded(address moduleAddress); + + /// @dev Event to log address removal + event ModuleRemoved(address moduleAddress); + + /// @notice constructor setting owner + /// @param admin owner of the contract + constructor(address admin) { + transferOwnership(admin); + } + + /// @dev Function to add an address to the map + /// @param moduleAddress add address to verifiedModules + function addModule(address moduleAddress) external onlyOwner { + require(!verifiedModules[moduleAddress], "address already exists"); + verifiedModules[moduleAddress] = true; + emit ModuleAdded(moduleAddress); + } + + /// @dev Function to remove an address from the map + /// @param moduleAddress address to remove from verifiedModules + function removeModule(address moduleAddress) external onlyOwner { + require(verifiedModules[moduleAddress], "address does not exist"); + verifiedModules[moduleAddress] = false; + emit ModuleRemoved(moduleAddress); + } + + /// @dev Function to check if an address exists in the map + /// @param moduleAddress check address in verifiedModules + function isVerified(address moduleAddress) external view returns (bool) { + return verifiedModules[moduleAddress]; + } +} diff --git a/deploy/1_0_deploy_account_accountFactory.ts b/deploy/1_0_deploy_account_accountFactory.ts index 62ee23e..d5f6909 100644 --- a/deploy/1_0_deploy_account_accountFactory.ts +++ b/deploy/1_0_deploy_account_accountFactory.ts @@ -33,6 +33,8 @@ async function main(): Promise { const accountSalt = hexZeroPad(Buffer.from(BloctoAccountCloneableWalletSalt, 'utf-8'), 32) console.log(`Deploying BloctoAccountCloneableWallet with -> \n\t salt str: ${BloctoAccountCloneableWalletSalt}`) const walletCloneable = await create3Factory.getDeployed(owner.address, accountSalt) + const v154Salt = hexZeroPad(Buffer.from('AccountV154', 'utf-8'), 32) + const walletV154 = await create3Factory.getDeployed(owner.address, v154Salt) if ((await ethers.provider.getCode(walletCloneable)) === '0x') { console.log(`BloctowalletCloneableWallet deploying to: ${walletCloneable}`) @@ -54,7 +56,7 @@ async function main(): Promise { const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactory') const accountFactory = await create3DeployTransparentProxy(BloctoAccountFactory, [walletCloneable, EntryPoint, owner.address], - { initializer: 'initialize', constructorArgs: [walletCloneable], unsafeAllow: ['constructor', 'state-variable-immutable'] }, create3Factory, owner, accountFactorySalt) + { initializer: 'initialize', constructorArgs: [walletCloneable, walletV154], unsafeAllow: ['constructor', 'state-variable-immutable'] }, create3Factory, owner, accountFactorySalt) await accountFactory.deployed() console.log(`BloctoAccountFactory JUST deployed to: ${accountFactory.address}`) diff --git a/deploy/6_deploy_moduleManager.ts b/deploy/6_deploy_moduleManager.ts new file mode 100644 index 0000000..bc811aa --- /dev/null +++ b/deploy/6_deploy_moduleManager.ts @@ -0,0 +1,61 @@ +import hre, { ethers } from 'hardhat' +import { getDeployCode } from '../src/create3Factory' +import { + ModuleManager__factory, + CREATE3Factory__factory +} from '../typechain' +import { hexZeroPad } from '@ethersproject/bytes' + +// NOTE: don't forget to change this according to the backend deploy account +// const Create3FactoryAddress = '0x2f06F83f960ea999536f94df279815F79EeB4054' + +// dev testnet +const Create3FactoryAddress = '0xd6CA621705575c3c23622b0802964a556870953b' + +// Module Manager Salt +const ModuleManagerSalt = 'ModuleManagerV0' + +async function main(): Promise { + // const lockedAmount = ethers.utils.parseEther("1"); + const [owner] = await ethers.getSigners() + console.log('deploy with account: ', owner.address) + + const create3Factory = CREATE3Factory__factory.connect(Create3FactoryAddress, owner) + // -------------------BloctoAccountCloneableWallet------------------------------// + const contractSalt = hexZeroPad(Buffer.from(ModuleManagerSalt, 'utf-8'), 32) + console.log(`Deploying ModuleManager with -> \n\t salt str: ${ModuleManagerSalt}`) + const instance = await create3Factory.getDeployed(owner.address, contractSalt) + + if ((await ethers.provider.getCode(instance)) === '0x') { + console.log(`ModuleManager deploying to: ${instance}`) + const tx = await create3Factory.deploy( + contractSalt, + getDeployCode(new ModuleManager__factory(), [owner.address])) + await tx.wait() + + console.log(`ModuleManager JUST deployed to: ${instance}`) + } else { + console.log(`ModuleManager WAS deployed to: ${instance}`) + } + + // sleep 16 seconds + console.log('sleep 10 seconds for chain sync...') + await new Promise(f => setTimeout(f, 10000)) + + // -------------------Verify------------------------------// + // verify ModuleManager + await hre.run('verify:verify', { + address: instance, + contract: 'contracts/v1.5.x/ModuleManager.sol:ModuleManager', + constructorArguments: [ + owner.address + ] + }) +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/package.json b/package.json index 1872710..55b58fa 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "deploy-verifyingPaymaster": "hardhat run deploy/2_deploy_VerifyingPaymaster.ts", "deploy-upgrade": "hardhat run deploy/upgrade/0_upgrade.ts", "deploy-verify": "hardhat run deploy/4_verify.ts", - "deploy-v1.1.0": "hardhat run deploy/6_deploy_v1.1.0.ts" + "deploy-v1.1.0": "hardhat run deploy/6_deploy_v1.1.0.ts", + "deploy-moduleManager": "hardhat run deploy/6_deploy_moduleManager.ts" }, "devDependencies": { "@account-abstraction/contracts": "^0.6.0", diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index 8cb7eb5..300c181 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -11,7 +11,9 @@ import { TestERC20__factory, BloctoAccountFactory__factory, BloctoAccountFactory, - BloctoAccountFactoryBase + CREATE3Factory__factory, + BloctoAccountFactoryBase, + ModuleManager } from '../typechain' import { EntryPoint } from '@account-abstraction/contracts' @@ -21,7 +23,9 @@ import { createAccount, createAccountV151, createAccountV153, + createAccountV154, deployEntryPoint, + deployModuleManager, ONE_ETH, TWO_ETH, createAuthorizedCosignerRecoverWallet, @@ -34,7 +38,9 @@ import { get151SaltFromAddress, RevertFlag, txAppendData, - logBytes + logBytes, + deployBloctoWalletV153, + deployBloctoWalletV154 } from './testutils' import '@openzeppelin/hardhat-upgrades' import { hexZeroPad, concat } from '@ethersproject/bytes' @@ -42,10 +48,11 @@ import { deployCREATE3Factory, getDeployCode } from '../src/create3Factory' import { create3DeployTransparentProxy } from '../src/deployAccountFactoryWithCreate3' import { zeroAddress } from 'ethereumjs-util' import { parseEther, keccak256 } from 'ethers/lib/utils' +import { mod } from '@nomicfoundation/ethereumjs-evm/dist/opcodes' const ShowGasUsage = false -function randNumber (): number { +function randNumber(): number { return Math.floor(Math.random() * (1000000000000000)) } @@ -61,15 +68,17 @@ describe('BloctoAccount Test', function () { let entryPoint: EntryPoint + let moduleManager: ModuleManager + let create3Factory: CREATE3Factory let testERC20: TestERC20 - const NextVersion = '1.5.3' + const NextVersion = '1.5.4' let account: BloctoAccount - async function testCreateAccount (salt = 0, mergedKeyIndex = 0, ifactory = factory, version = NextVersion): Promise { + async function testCreateAccount(salt = 0, mergedKeyIndex = 0, ifactory = factory, version = NextVersion): Promise { const newSalt = keccak256(concat([ hexZeroPad(BigNumber.from(salt).toHexString(), 32), await cosignerWallet.getAddress(), @@ -113,7 +122,6 @@ describe('BloctoAccount Test', function () { ) break case '1.5.3': - default: console.log('use 1.5.3') retAccount = await createAccountV153( ethersSigner, @@ -126,6 +134,20 @@ describe('BloctoAccount Test', function () { ifactory ) break + case '1.5.4': + default: + console.log('use 1.5.4') + retAccount = await createAccountV154( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + BigNumber.from(salt), + pxIndexWithParity, + px, + ifactory + ) + break } await fund(retAccount) @@ -133,7 +155,7 @@ describe('BloctoAccount Test', function () { } // use authorizedWallet and cosignerWallet to send ERC20 from wallet - async function sendERC20 (iAccount: BloctoAccount, to: string, amount: BigNumber, withChainId: boolean = true): Promise { + async function sendERC20(iAccount: BloctoAccount, to: string, amount: BigNumber, withChainId: boolean = true): Promise { // const authorizeInAccountNonce = (await account.nonces(authorizedWallet.address)).add(1) let authorizeInAccountNonce: BigNumber if (withChainId) { @@ -152,7 +174,7 @@ describe('BloctoAccount Test', function () { return await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, authorizeInAccountNonce, authorizedWallet.address, data) } - async function mint1000testERC20 (target: string, leastAmount: BigNumber, erc20: TestERC20 = testERC20): Promise { + async function mint1000testERC20(target: string, leastAmount: BigNumber, erc20: TestERC20 = testERC20): Promise { const before = await erc20.balanceOf(target) if (before.lt(leastAmount)) { console.log(`mint 1000 ${await erc20.symbol()} to ${target}`) @@ -171,36 +193,31 @@ describe('BloctoAccount Test', function () { await fund(cosignerWallet.address) // 4337 entryPoint = await deployEntryPoint() - + // module manager + moduleManager = await deployModuleManager() // create3 factory create3Factory = await deployCREATE3Factory(ethersSigner) - const accountSalt = hexZeroPad(Buffer.from('BloctoAccount', 'utf-8'), 32) - implementation = await create3Factory.getDeployed(await ethersSigner.getAddress(), accountSalt) - - if ((await ethers.provider.getCode(implementation)) !== '0x') { - console.log(`Using Existed BloctoAccountCloneableWallet (${implementation})!`) - } else { - console.log(`Deploying to BloctoAccountCloneableWallet (${implementation})...`) - const bloctoAccountCloneableWalletDeployTx = await create3Factory.deploy( - accountSalt, - getDeployCode(new BloctoAccountCloneableWallet__factory(), [entryPoint.address]) - ) - await bloctoAccountCloneableWalletDeployTx.wait() - } + const v153 = await deployBloctoWalletV153(ethersSigner, create3Factory, entryPoint.address, moduleManager.address) + const v154 = await deployBloctoWalletV154(ethersSigner, create3Factory, entryPoint.address, moduleManager.address) + const implementation = v153.address // account factory - const BloctoAccountFactoryBase = await ethers.getContractFactory('BloctoAccountFactoryBase') + const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactory') const BloctoAccountFactoryProxySalt = hexZeroPad(Buffer.from('BloctoAccountFactoryProxy_v140', 'utf-8'), 32) const accountFactoryAddress: string = await create3Factory.getDeployed(await create3Factory.signer.getAddress(), BloctoAccountFactoryProxySalt) if ((await ethers.provider.getCode(accountFactoryAddress)) !== '0x') { console.log(`Using Existed BloctoAccountFactory (${accountFactoryAddress})!`) factory = await BloctoAccountFactory__factory.connect(accountFactoryAddress, ethersSigner) } else { - console.log(`Deploying to BloctoAccountFactoryBase (${accountFactoryAddress})...`) - factory = await create3DeployTransparentProxy(BloctoAccountFactoryBase, + console.log(`Deploying to BloctoAccountFactory (${accountFactoryAddress})...`) + console.log('v153: ', v153.address) + console.log('v154: ', v154.address) + console.log('entryPoint: ', entryPoint.address) + factory = await create3DeployTransparentProxy(BloctoAccountFactory, [implementation, entryPoint.address, await ethersSigner.getAddress()], - { initializer: 'initialize' }, create3Factory, ethersSigner, BloctoAccountFactoryProxySalt) + { initializer: 'initialize', constructorArgs: [v153.address, v154.address], unsafeAllow: ['constructor', 'state-variable-immutable'] }, create3Factory, ethersSigner, BloctoAccountFactoryProxySalt) } + console.log('before deploy') // To consider test on REAL chain, separate factory method manipulation const createAccountRole = await factory.CREATE_ACCOUNT_ROLE() if (!(await factory.hasRole(createAccountRole, await ethersSigner.getAddress()))) { @@ -213,7 +230,7 @@ describe('BloctoAccount Test', function () { const factorySetImplementation_1_5_1Tx = await factory.setImplementation_1_5_1(implementation) await factorySetImplementation_1_5_1Tx.wait() } - + console.log('222 sfactory address: ', factory.address) const nowFactoryVersoin = await factory.VERSION() console.log(`Factory version: ${nowFactoryVersoin}`) if (nowFactoryVersoin !== NextVersion) { @@ -266,7 +283,7 @@ describe('BloctoAccount Test', function () { }) it('should not initImplementation with a contract', async () => { - const b = await new BloctoAccount__factory(ethersSigner).deploy(entryPoint.address) + const b = await new BloctoAccount__factory(ethersSigner).deploy(entryPoint.address, moduleManager.address) await expect(b.initImplementation('0x' + 'a'.repeat(40))).to.revertedWith('ERC1967: new implementation is not a contract') }) @@ -652,8 +669,8 @@ describe('BloctoAccount Test', function () { let findWalletCreated = false receipt.events?.forEach((event) => { if (event.event === 'WalletCreated' && - event.args?.authorizedAddress === authorizedEOA.address && - event.args?.wallet === predictAddr151) { + event.args?.authorizedAddress === authorizedEOA.address && + event.args?.wallet === predictAddr151) { findWalletCreated = true } }) @@ -703,8 +720,8 @@ describe('BloctoAccount Test', function () { let findWalletCreated = false receipt.events?.forEach((event) => { if (event.event === 'WalletCreated' && - event.args?.authorizedAddress === authorizedEOA.address && - event.args?.wallet === predictAddr151) { + event.args?.authorizedAddress === authorizedEOA.address && + event.args?.wallet === predictAddr151) { findWalletCreated = true } }) @@ -754,8 +771,8 @@ describe('BloctoAccount Test', function () { let findWalletCreated = false receipt.events?.forEach((event) => { if (event.event === 'WalletCreated' && - event.args?.authorizedAddress === authorizedEOA.address && - event.args?.wallet === predictAddr151) { + event.args?.authorizedAddress === authorizedEOA.address && + event.args?.wallet === predictAddr151) { findWalletCreated = true } }) @@ -833,7 +850,7 @@ describe('BloctoAccount Test', function () { expect(await account2_1_5_1.VERSION()).to.equal(NextVersion) if (cosignerWallet2.address === '0x4eF791438972d2D41FF4BF7911E0F7372971eFcA' && - recoverWallet2.address === '0xFEC60025526f37BEB6134631322E98e48794d8fb') { + recoverWallet2.address === '0xFEC60025526f37BEB6134631322E98e48794d8fb') { const initImplementation = await factory.initImplementation() // local account if (factory.address === '0x591E00821444155a7076cd7254747d05D1374267') { @@ -863,14 +880,14 @@ describe('BloctoAccount Test', function () { const erc20Receiver = createTmpAccount(8) const testFee = parseEther('10') - function fee10DAI (revertFlag: RevertFlag): Uint8Array { + function fee10DAI(revertFlag: RevertFlag): Uint8Array { // const revertFlag = isRevert ? RevertFlag.Revert : RevertFlag.NoRevert return txData(revertFlag, dai.address, BigNumber.from(0), testERC20.interface.encodeFunctionData('transfer', [feeReceiver.address, testFee])) } // tx 1: send 10 DAI to feeReceiver, tx 2: send 1 ERC20 to erc20Receiver - async function feeWithSendERC20 (revertFlag: RevertFlag): Promise<[BigNumber, Uint8Array, string]> { + async function feeWithSendERC20(revertFlag: RevertFlag): Promise<[BigNumber, Uint8Array, string]> { const feeData = fee10DAI(revertFlag) const erc20TransferData = txAppendData(feeData, testERC20.address, BigNumber.from(0), @@ -883,7 +900,7 @@ describe('BloctoAccount Test', function () { } // tx 1: send 10 DAI to feeReceiver, tx 2: send 1 ERC20 to erc20Receiver - async function feeWithSendERC20AndNativeToken (revertFlag: RevertFlag): Promise<[BigNumber, Uint8Array, string]> { + async function feeWithSendERC20AndNativeToken(revertFlag: RevertFlag): Promise<[BigNumber, Uint8Array, string]> { const feeData = fee10DAI(revertFlag) const tx12Data = txAppendData(feeData, testERC20.address, BigNumber.from(0), @@ -897,7 +914,7 @@ describe('BloctoAccount Test', function () { return [newNonce, tx123Data, sign] } - async function clearOutBalance (erc20: TestERC20, targetAccount: BloctoAccount): Promise { + async function clearOutBalance(erc20: TestERC20, targetAccount: BloctoAccount): Promise { const balance = await erc20.balanceOf(targetAccount.address) if (balance.gt(0)) { console.log(`${targetAccount.address} clear out balance(${balance.toString()}) of ${await erc20.symbol()}`) @@ -1201,7 +1218,7 @@ describe('BloctoAccount Test', function () { const sign = await signForInovke2(predictAddr151, newNonce, erc20TransferData, authorizedEOA, cosignerEOA) console.log('simulating createAccountWithInvoke2...') - const errorArgs = await factory.callStatic.simulateCreateAccountWithInvoke2_1_5_3( + const errorArgs = await factory.callStatic.simulateCreateAccountWithInvoke2_1_5_4( await authorizedEOA.getAddress(), await cosignerEOA.getAddress(), await recoverEOA.getAddress(), @@ -1209,13 +1226,13 @@ describe('BloctoAccount Test', function () { pxIndexWithParity, px, { nonce: newNonce, data: erc20TransferData, signature: sign }, - { gasLimit: 1e6 } + { gasLimit: 8e6 } ).catch(e => e.errorArgs) // targetSuccess should be true expect(errorArgs.targetSuccess).to.be.true expect(errorArgs.gasLeft).to.gt(0) // use around 2e5 gas - expect(errorArgs.gasLeft).to.lt(8e5) + expect(errorArgs.gasLeft).to.lt(8e6) // the account should NOT be created expect(await ethers.provider.getCode(predictAddr151)).to.equal('0x') }) @@ -1241,7 +1258,7 @@ describe('BloctoAccount Test', function () { const sign = await signForInovke2(predictAddr151, newNonce, erc20TransferData, authorizedEOA, cosignerEOA) console.log('simulating createAccount2WithInvoke2...') - const errorArgs = await factory.callStatic.simulateCreateAccount2WithInvoke2_1_5_3( + const errorArgs = await factory.callStatic.simulateCreateAccount2WithInvoke2_1_5_4( [authorizedEOA.address, authorizedEOA2.address], cosignerEOA.address, recoverEOA.address, newSalt, diff --git a/test/testutils.ts b/test/testutils.ts index 4adf993..89f2fce 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -11,9 +11,15 @@ import { IERC20, BloctoAccount, BloctoAccount__factory, - BloctoAccountFactory + BloctoAccountCloneableWallet__factory, + BloctoAccountCloneableWallet, + BloctoAccountFactory, + ModuleManager, + CREATE3Factory } from '../typechain' +import { getDeployCode } from '../src/create3Factory' + import { EntryPoint, EntryPoint__factory } from '@account-abstraction/contracts' import { Bytes, BytesLike, hexZeroPad, concat, Signature } from '@ethersproject/bytes' @@ -45,7 +51,7 @@ export enum RevertFlag { PointWithRevert = 3, // b11 } -export function tonumber (x: any): number { +export function tonumber(x: any): number { try { return parseFloat(x.toString()) } catch (e: any) { @@ -55,7 +61,7 @@ export function tonumber (x: any): number { } // just throw 1eth from account[0] to the given address (or contract instance) -export async function fund (contractOrAddress: string | Contract, amountEth = '0.05'): Promise { +export async function fund(contractOrAddress: string | Contract, amountEth = '0.05'): Promise { let address: string if (typeof contractOrAddress === 'string') { address = contractOrAddress @@ -71,44 +77,44 @@ export async function fund (contractOrAddress: string | Contract, amountEth = '0 await tx.wait() } -export async function getBalance (address: string): Promise { +export async function getBalance(address: string): Promise { const balance = await ethers.provider.getBalance(address) return parseInt(balance.toString()) } -export async function getTokenBalance (token: IERC20, address: string): Promise { +export async function getTokenBalance(token: IERC20, address: string): Promise { const balance = await token.balanceOf(address) return parseInt(balance.toString()) } let counter = 0 // Math.floor(Math.random() * 5000) -export function createTmpAccount (idx: number = 1): Wallet { +export function createTmpAccount(idx: number = 1): Wallet { const envName = 'TMP_KEY_' + String(idx) const privateKey = typeof (process.env[envName]) !== 'undefined' ? process.env[envName] : keccak256(Buffer.from(arrayify(BigNumber.from(++counter)))) return new Wallet(privateKey as string, ethers.provider) } // create non-random account, so gas calculations are deterministic -export function createAuthorizedCosignerRecoverWallet (): [Wallet, Wallet, Wallet] { +export function createAuthorizedCosignerRecoverWallet(): [Wallet, Wallet, Wallet] { return [createTmpAccount(1), createTmpAccount(2), createTmpAccount(3)] } -export function createAuthorizedCosignerRecoverWallet2 (): [Wallet, Wallet, Wallet] { +export function createAuthorizedCosignerRecoverWallet2(): [Wallet, Wallet, Wallet] { return [createTmpAccount(4), createTmpAccount(5), createTmpAccount(6)] } -export function createAddress (): string { +export function createAddress(): string { return createTmpAccount().address } -export function callDataCost (data: string): number { +export function callDataCost(data: string): number { return ethers.utils.arrayify(data) .map(x => x === 0 ? 4 : 16) .reduce((sum, x) => sum + x) } -export async function calcGasUsage (rcpt: ContractReceipt, entryPoint: EntryPoint, beneficiaryAddress?: string): Promise<{ actualGasCost: BigNumberish }> { +export async function calcGasUsage(rcpt: ContractReceipt, entryPoint: EntryPoint, beneficiaryAddress?: string): Promise<{ actualGasCost: BigNumberish }> { const actualGas = await rcpt.gasUsed const logs = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(), rcpt.blockHash) const { actualGasCost, actualGasUsed } = logs[0].args @@ -123,7 +129,7 @@ export async function calcGasUsage (rcpt: ContractReceipt, entryPoint: EntryPoin } // helper function to create the initCode to deploy the account, using our account factory. -export function getAccountInitCode (factory: BloctoAccountFactory, authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, salt, pxIndexWithParity, px): BytesLike { +export function getAccountInitCode(factory: BloctoAccountFactory, authorizedAddress: string, cosignerAddress: string, recoveryAddress: string, salt, pxIndexWithParity, px): BytesLike { return hexConcat([ factory.address, factory.interface.encodeFunctionData('createAccountLegacy', [authorizedAddress, cosignerAddress, recoveryAddress, BigNumber.from(salt)]) @@ -131,7 +137,7 @@ export function getAccountInitCode (factory: BloctoAccountFactory, authorizedAdd } // helper function to create the initCode to deploy the account, using our account factory. -export function getAccountInitCode2 (factory: BloctoAccountFactory, authorizedAddresses: BytesLike, cosignerAddress: string, recoveryAddress: string, salt = 0): BytesLike { +export function getAccountInitCode2(factory: BloctoAccountFactory, authorizedAddresses: BytesLike, cosignerAddress: string, recoveryAddress: string, salt = 0): BytesLike { return hexConcat([ factory.address, factory.interface.encodeFunctionData('createAccount2Legacy', [authorizedAddresses, cosignerAddress, recoveryAddress, BigNumber.from(salt)]) @@ -155,7 +161,7 @@ const panicCodes: { [key: number]: string } = { // - stack trace goes back to method (or catch) line, not inner provider // - attempt to parse revert data (needed for geth) // use with ".catch(rethrow())", so that current source file/line is meaningful. -export function rethrow (): (e: Error) => void { +export function rethrow(): (e: Error) => void { const callerStack = new Error().stack!.replace(/Error.*\n.*at.*\n/, '').replace(/.*at.* \(internal[\s\S]*/, '') if (arguments[0] != null) { @@ -179,7 +185,7 @@ export function rethrow (): (e: Error) => void { } } -export function decodeRevertReason (data: string, nullIfNoMatch = true): string | null { +export function decodeRevertReason(data: string, nullIfNoMatch = true): string | null { const methodSig = data.slice(0, 10) const dataParams = '0x' + data.slice(10) @@ -203,7 +209,7 @@ export function decodeRevertReason (data: string, nullIfNoMatch = true): string // basic geth support // - by default, has a single account. our code needs more. -export async function checkForGeth (): Promise { +export async function checkForGeth(): Promise { // @ts-ignore const provider = ethers.provider._hardhatProvider @@ -227,7 +233,7 @@ export async function checkForGeth (): Promise { // { '0': "a", '1': 20, first: "a", second: 20 } // becomes: // { first: "a", second: "20" } -export function objdump (obj: { [key: string]: any }): any { +export function objdump(obj: { [key: string]: any }): any { return Object.keys(obj) .filter(key => key.match(/^[\d_]/) == null) .reduce((set, key) => ({ @@ -239,7 +245,7 @@ export function objdump (obj: { [key: string]: any }): any { * process exception of ValidationResult * usage: entryPoint.simulationResult(..).catch(simulationResultCatch) */ -export function simulationResultCatch (e: any): any { +export function simulationResultCatch(e: any): any { if (e.errorName !== 'ValidationResult') { throw e } @@ -250,14 +256,14 @@ export function simulationResultCatch (e: any): any { * process exception of ValidationResultWithAggregation * usage: entryPoint.simulationResult(..).catch(simulationResultWithAggregation) */ -export function simulationResultWithAggregationCatch (e: any): any { +export function simulationResultWithAggregationCatch(e: any): any { if (e.errorName !== 'ValidationResultWithAggregation') { throw e } return e.errorArgs } -export async function deployEntryPoint (provider = ethers.provider): Promise { +export async function deployEntryPoint(provider = ethers.provider): Promise { // console.log('in deployEntryPoint') // console.log('await ethers.provider.getCode(EntryPointV060):', await ethers.provider.getCode(EntryPointV060)) let addr = EntryPointV060 @@ -273,13 +279,52 @@ export async function deployEntryPoint (provider = ethers.provider): Promise { +export async function deployModuleManager(): Promise { + const instance = await ethers.getContractFactory('contracts/v1.1.0/CloneableWallet.sol:CloneableWallet') + return await instance.deploy() as ModuleManager +} + +export async function deployBloctoWalletV153(ethersSigner: Signer, create3Factory: CREATE3Factory, entryPointAddr: string, moduleManagerAddr: string): Promise { + const accountSalt = hexZeroPad(Buffer.from('BloctoAccountV153', 'utf-8'), 32) + const implementation = await create3Factory.getDeployed(await ethersSigner.getAddress(), accountSalt) + + if ((await ethers.provider.getCode(implementation)) !== '0x') { + console.log(`Using Existed BloctoAccountCloneableWallet (${implementation})!`) + } else { + console.log(`Deploying to BloctoAccountCloneableWallet (${implementation})...`) + const bloctoAccountCloneableWalletDeployTx = await create3Factory.deploy( + accountSalt, + getDeployCode(new BloctoAccountCloneableWallet__factory(), [entryPointAddr, moduleManagerAddr]) + ) + await bloctoAccountCloneableWalletDeployTx.wait() + } + return await BloctoAccountCloneableWallet__factory.connect(implementation, ethersSigner) +} + +export async function deployBloctoWalletV154(ethersSigner: Signer, create3Factory: CREATE3Factory, entryPointAddr: string, moduleManagerAddr: string): Promise { + const accountSalt = hexZeroPad(Buffer.from('BloctoAccountV154', 'utf-8'), 32) + const implementation = await create3Factory.getDeployed(await ethersSigner.getAddress(), accountSalt) + + if ((await ethers.provider.getCode(implementation)) !== '0x') { + console.log(`Using Existed BloctoAccountCloneableWallet (${implementation})!`) + } else { + console.log(`Deploying to BloctoAccountCloneableWallet (${implementation})...`) + const bloctoAccountCloneableWalletDeployTx = await create3Factory.deploy( + accountSalt, + getDeployCode(new BloctoAccountCloneableWallet__factory(), [entryPointAddr, moduleManagerAddr]) + ) + await bloctoAccountCloneableWalletDeployTx.wait() + } + return await BloctoAccountCloneableWallet__factory.connect(implementation, ethersSigner) +} + +export async function isDeployed(addr: string): Promise { const code = await ethers.provider.getCode(addr) return code.length > 2 } // Deploys an implementation and a proxy pointing to this implementation -export async function createAccount ( +export async function createAccount( ethersSigner: Signer, authorizedAddresses: string, cosignerAddresses: string, @@ -302,7 +347,7 @@ export async function createAccount ( } // Deploys an implementation and a proxy pointing to this implementation -export async function createAccountV151 ( +export async function createAccountV151( ethersSigner: Signer, authorizedAddresses: string, cosignerAddresses: string, @@ -328,7 +373,7 @@ export async function createAccountV151 ( } // deploy account with 1.5.3 -export async function createAccountV153 ( +export async function createAccountV153( ethersSigner: Signer, authorizedAddresses: string, cosignerAddresses: string, @@ -353,8 +398,34 @@ export async function createAccountV153 ( return account } +// deploy account with 1.5.4 +export async function createAccountV154( + ethersSigner: Signer, + authorizedAddresses: string, + cosignerAddresses: string, + recoverAddresses: string, + salt: BigNumber, + mergedKeyIndexWithParity: number, + mergedKey: string, + accountFactory: BloctoAccountFactory +): Promise { + const newSalt = keccak256(concat([ + ethers.utils.hexZeroPad(salt.toHexString(), 32), + cosignerAddresses, recoverAddresses + ])) + const accountAddress = await accountFactory.getAddress_1_5_1(newSalt) + const tx = await accountFactory.createAccount_1_5_4(authorizedAddresses, cosignerAddresses, recoverAddresses, newSalt, mergedKeyIndexWithParity, mergedKey) + const receipt = await tx.wait() + if (ShowCreateAccountGas) { + console.log('createAccount_1_5_4 gasUsed: ', receipt.gasUsed) + } + + const account = BloctoAccount__factory.connect(accountAddress, ethersSigner) + return account +} + // helper function to create the setEntryPointCode to set the account entryPoint address -export function getSetEntryPointCode (account: BloctoAccount, entryPointAddress: string): BytesLike { +export function getSetEntryPointCode(account: BloctoAccount, entryPointAddress: string): BytesLike { return hexConcat([ account.address, account.interface.encodeFunctionData('setEntryPoint', [entryPointAddress]) @@ -405,7 +476,7 @@ export const txAppendData = (preData: Uint8Array, to: string, amount: BigNumber, } export const EIP191V0MessagePrefix = '\x19\x00' -export function hashMessageEIP191V0 (chainId: number, address: string, message: Bytes | string): string { +export function hashMessageEIP191V0(chainId: number, address: string, message: Bytes | string): string { address = address.replace('0x', '') const chainIdStr = ethers.utils.hexZeroPad(ethers.utils.hexlify(chainId), 32) @@ -418,7 +489,7 @@ export function hashMessageEIP191V0 (chainId: number, address: string, message: ])) } -export function hashMessageEIP191V0WithoutChainId (address: string, message: Bytes | string): string { +export function hashMessageEIP191V0WithoutChainId(address: string, message: Bytes | string): string { address = address.replace('0x', '') return keccak256(concat([ @@ -428,7 +499,7 @@ export function hashMessageEIP191V0WithoutChainId (address: string, message: Byt ])) } -export async function signMessage (signerWallet: Wallet, accountAddress: string, nonce: BigNumber, data: Uint8Array, addrForData: string = signerWallet.address): Promise { +export async function signMessage(signerWallet: Wallet, accountAddress: string, nonce: BigNumber, data: Uint8Array, addrForData: string = signerWallet.address): Promise { const nonceBytesLike = hexZeroPad(nonce.toHexString(), 32) const dataForHash = concat([ @@ -440,7 +511,7 @@ export async function signMessage (signerWallet: Wallet, accountAddress: string, return sign } -export async function signMessageWithoutChainId (signerWallet: Wallet, accountAddress: string, nonce: BigNumber, data: Uint8Array): Promise { +export async function signMessageWithoutChainId(signerWallet: Wallet, accountAddress: string, nonce: BigNumber, data: Uint8Array): Promise { const nonceBytesLike = hexZeroPad(nonce.toHexString(), 32) const dataForHash = concat([ @@ -452,7 +523,7 @@ export async function signMessageWithoutChainId (signerWallet: Wallet, accountAd return sign } -export async function signForInovke2 (accountAddr: string, nonce: BigNumber, data: Uint8Array, signer: Wallet, cosigner: Wallet, useSchnorr = false, mergedKeyIndex = 0): Promise { +export async function signForInovke2(accountAddr: string, nonce: BigNumber, data: Uint8Array, signer: Wallet, cosigner: Wallet, useSchnorr = false, mergedKeyIndex = 0): Promise { const nonceBytesLike = hexZeroPad(nonce.toHexString(), 32) const dataForHash = concat([ @@ -496,11 +567,11 @@ export async function signForInovke2 (accountAddr: string, nonce: BigNumber, dat return signature } -export function logBytes (uint8: Uint8Array): string { +export function logBytes(uint8: Uint8Array): string { return Buffer.from(uint8).toString('hex') + '(' + uint8.length.toString() + ')' } -export function getMergedKey (wallet1: Wallet, wallet2: Wallet, mergedKeyIndex: number): [px: string, pxIndexWithParity: number] { +export function getMergedKey(wallet1: Wallet, wallet2: Wallet, mergedKeyIndex: number): [px: string, pxIndexWithParity: number] { mergedKeyIndex = 128 + (mergedKeyIndex << 1) const signerOne = new DefaultSigner(wallet1) const signerTwo = new DefaultSigner(wallet2) @@ -512,23 +583,23 @@ export function getMergedKey (wallet1: Wallet, wallet2: Wallet, mergedKeyIndex: return [px, pxIndexWithParity] } -function padWithZeroes (hexString: string, targetLength: number): string { +function padWithZeroes(hexString: string, targetLength: number): string { if (hexString !== '' && !/^[a-f0-9]+$/iu.test(hexString)) { throw new Error( - `Expected an unprefixed hex string. Received: ${hexString}` + `Expected an unprefixed hex string. Received: ${hexString}` ) } if (targetLength < 0) { throw new Error( - `Expected a non-negative integer target length. Received: ${targetLength}` + `Expected a non-negative integer target length. Received: ${targetLength}` ) } return String.prototype.padStart.call(hexString, targetLength, '0') } -function concatSig (v: Buffer, r: Buffer, s: Buffer): string { +function concatSig(v: Buffer, r: Buffer, s: Buffer): string { const rSig = fromSigned(r) const sSig = fromSigned(s) const vSig = bufferToInt(v) @@ -538,13 +609,13 @@ function concatSig (v: Buffer, r: Buffer, s: Buffer): string { return addHexPrefix(rStr.concat(sStr, vStr)) } -export function sign2Str (signer: Wallet, data: string): string { +export function sign2Str(signer: Wallet, data: string): string { const sig = signer._signingKey().signDigest(data) return concatSig(toBuffer(sig.v), toBuffer(sig.r), toBuffer(sig.s)) } -export function get151SaltFromAddress ( +export function get151SaltFromAddress( salt: number, cosignerAddresses: string, recoverAddresses: string): string {