From 69d633980e40c19ed47d54f64d34bbbb48de2b81 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Thu, 27 Jul 2023 16:33:44 +0800 Subject: [PATCH 1/2] v1.5.0: hardhat test success but hardhat coverage fail --- package.json | 4 ++-- test/bloctoaccount.test.ts | 12 +++++++----- yarn.lock | 17 ++++++++++++----- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 20ecdfa..2eaecf8 100644 --- a/package.json +++ b/package.json @@ -57,10 +57,10 @@ "ethereumjs-wallet": "^1.0.1", "hardhat-deploy": "^0.11.23", "hardhat-deploy-ethers": "^0.3.0-beta.11", - "solidity-coverage": "^0.8.2", + "solidity-coverage": "^0.8.4", "source-map-support": "^0.5.19", "table": "^6.8.0", "typescript": "^4.3.5" }, "license": "GPL-3.0" -} \ No newline at end of file +} diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index 875401b..6b2d46d 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -11,8 +11,8 @@ import { CREATE3Factory, TestBloctoAccountCloneableWalletV200, TestBloctoAccountCloneableWalletV200__factory, - TestERC20, - TestERC20__factory + TestERC20__factory, + BloctoAccountFactory__factory } from '../typechain' import { EntryPoint } from '@account-abstraction/contracts' import { @@ -149,7 +149,7 @@ describe('BloctoAccount Upgrade Test', function () { expect(await accountV140.VERSION()).to.eql(NowVersion) }) - it('should delpoy new cloneble wallet and upgrade factory ', async () => { + it('should delpoy new cloneable wallet and upgrade factory ', async () => { // deploy BloctoAccount next version const accountSalt = hexZeroPad(Buffer.from('BloctoAccount_next_version', 'utf-8'), 32) implementation = await create3Factory.getDeployed(await ethersSigner.getAddress(), accountSalt) @@ -312,9 +312,11 @@ describe('BloctoAccount Upgrade Test', function () { const createAccountWallet = await createTmpAccount() await fund(createAccountWallet.address) // grant account role - await factory.grantRole(await factory.CREATE_ACCOUNT_ROLE(), await createAccountWallet.address) + await factory.grantRole(await factory.CREATE_ACCOUNT_ROLE(), createAccountWallet.address) + expect(await factory.hasRole(await factory.CREATE_ACCOUNT_ROLE(), createAccountWallet.address)).true + // create account with createAccountWallet - const factoryWithCreateAccount = await factory.connect(createAccountWallet) + const factoryWithCreateAccount = BloctoAccountFactory__factory.connect(factory.address, createAccountWallet) const mergedKeyIndex = 0 const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, mergedKeyIndex) diff --git a/yarn.lock b/yarn.lock index 1f4fd34..f5bb4af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1035,6 +1035,13 @@ dependencies: antlr4ts "^0.5.0-alpha.4" +"@solidity-parser/parser@^0.16.0": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.16.1.tgz#f7c8a686974e1536da0105466c4db6727311253c" + integrity sha512-PdhRFNhbTtu3x8Axm0uYpqOy/lODYQK+MlYSgqIsq2L8SFYEHJPHNUiOTAJbDGzNjjr1/n9AcIayxafR/fWmYw== + dependencies: + antlr4ts "^0.5.0-alpha.4" + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -8919,13 +8926,13 @@ solidity-ast@^0.4.15: resolved "https://registry.yarnpkg.com/solidity-ast/-/solidity-ast-0.4.46.tgz#d0745172dced937741d07464043564e35b147c59" integrity sha512-MlPZQfPhjWXqh7YxWcBGDXaPZIfMYCOHYoLEhGDWulNwEPIQQZuB7mA9eP17CU0jY/bGR4avCEUVVpvHtT2gbA== -solidity-coverage@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.8.2.tgz#bc39604ab7ce0a3fa7767b126b44191830c07813" - integrity sha512-cv2bWb7lOXPE9/SSleDO6czkFiMHgP4NXPj+iW9W7iEKLBk7Cj0AGBiNmGX3V1totl9wjPrT0gHmABZKZt65rQ== +solidity-coverage@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/solidity-coverage/-/solidity-coverage-0.8.4.tgz#c57a21979f5e86859c5198de9fbae2d3bc6324a5" + integrity sha512-xeHOfBOjdMF6hWTbt42iH4x+7j1Atmrf5OldDPMxI+i/COdExUxszOswD9qqvcBTaLGiOrrpnh9UZjSpt4rBsg== dependencies: "@ethersproject/abi" "^5.0.9" - "@solidity-parser/parser" "^0.14.1" + "@solidity-parser/parser" "^0.16.0" chalk "^2.4.2" death "^1.1.0" detect-port "^1.3.0" From 4130a354ba32e87c36ba7cc8eb45377f2a07e4f1 Mon Sep 17 00:00:00 2001 From: kbehouse Date: Tue, 1 Aug 2023 16:50:16 +0800 Subject: [PATCH 2/2] v1.5.0: raise test coverage rate funcs and lines 100% --- .solcover.js | 6 +- contracts/BloctoAccount.sol | 2 +- contracts/BloctoAccountFactory.sol | 29 -- contracts/Create3/Bytes32AddressLib.sol | 6 +- contracts/test/TestERC20.sol | 8 + package.json | 2 + test/bloctoaccount.test.ts | 192 +++++++++- test/corewallet.test.ts | 466 ++++++++++++++++++++++++ test/entrypoint/UserOp.ts | 24 +- test/entrypoint/entrypoint.test.ts | 27 +- test/schnorrMultiSign.test.ts | 157 +++++++- test/testutils.ts | 38 +- yarn.lock | 9 +- 13 files changed, 891 insertions(+), 75 deletions(-) create mode 100644 test/corewallet.test.ts diff --git a/.solcover.js b/.solcover.js index 186efea..9f94a5d 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,10 +1,8 @@ module.exports = { skipFiles: [ "test", - "samples/bls/lib", - //solc-coverage fails to compile our Manager module. - "samples/gnosis", - "utils/Exec.sol" + "Paymaster/VerifyingPaymaster.sol", + "TokenCallbackHandler.sol" ], configureYulOptimizer: true, }; diff --git a/contracts/BloctoAccount.sol b/contracts/BloctoAccount.sol index a29a6da..92392eb 100644 --- a/contracts/BloctoAccount.sol +++ b/contracts/BloctoAccount.sol @@ -149,5 +149,5 @@ contract BloctoAccount is UUPSUpgradeable, TokenCallbackHandler, CoreWallet, Bas } /// @dev This empty reserved space for future versions. refer from: https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps - // uint256[50] private __gap; + uint256[50] private __gap; } diff --git a/contracts/BloctoAccountFactory.sol b/contracts/BloctoAccountFactory.sol index a9db936..4071ffa 100644 --- a/contracts/BloctoAccountFactory.sol +++ b/contracts/BloctoAccountFactory.sol @@ -38,9 +38,6 @@ contract BloctoAccountFactory is Initializable, AccessControlUpgradeable { } /// @notice create an account, and return its BloctoAccount. - /// returns the address even if the account is already deployed. - /// Note that during UserOperation execution, this method is called only if the account is not deployed. - /// This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation /// @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) @@ -70,7 +67,6 @@ contract BloctoAccountFactory is Initializable, AccessControlUpgradeable { } /// @notice create an account with multiple authorized addresses, and return its BloctoAccount. - /// returns the address even if the account is already deployed. /// @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) @@ -121,31 +117,6 @@ contract BloctoAccountFactory is Initializable, AccessControlUpgradeable { bloctoAccountImplementation = _bloctoAccountImplementation; } - /// @notice set the entrypoint - /// @param _entrypoint target entrypoint - function setEntrypoint(IEntryPoint _entrypoint) public { - require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not a admin"); - require(address(_entrypoint) != address(0), "Invalid entrypoint address."); - entryPoint = _entrypoint; - } - - /// @notice withdraw value from the deposit - /// @param withdrawAddress target to send to - /// @param amount to withdraw - function withdrawTo(address payable withdrawAddress, uint256 amount) public { - require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not a admin"); - require(address(withdrawAddress) != address(0), "Invalid withdraw address."); - require(amount > 0, "Invalid withdraw amount."); - entryPoint.withdrawTo(withdrawAddress, amount); - } - - /// @notice add stake in etnrypoint for this factory to avoid bundler reject - /// @param unstakeDelaySec - the unstake delay for this factory. Can only be increased. - function addStake(uint32 unstakeDelaySec) external payable { - require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "caller is not a admin"); - entryPoint.addStake{value: msg.value}(unstakeDelaySec); - } - /// @dev This empty reserved space for future versions. refer from: https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps uint256[50] private __gap; } diff --git a/contracts/Create3/Bytes32AddressLib.sol b/contracts/Create3/Bytes32AddressLib.sol index 88ec4b9..de016ff 100644 --- a/contracts/Create3/Bytes32AddressLib.sol +++ b/contracts/Create3/Bytes32AddressLib.sol @@ -9,7 +9,7 @@ library Bytes32AddressLib { return address(uint160(uint256(bytesValue))); } - function fillLast12Bytes(address addressValue) internal pure returns (bytes32) { - return bytes32(bytes20(addressValue)); - } + // function fillLast12Bytes(address addressValue) internal pure returns (bytes32) { + // return bytes32(bytes20(addressValue)); + // } } diff --git a/contracts/test/TestERC20.sol b/contracts/test/TestERC20.sol index 85639cc..5166138 100644 --- a/contracts/test/TestERC20.sol +++ b/contracts/test/TestERC20.sol @@ -24,4 +24,12 @@ contract TestERC20 is ERC20, ERC20Burnable, AccessControl { function decimals() public view virtual override returns (uint8) { return _decimals; } + + function senderBalance() public view returns (uint256) { + return balanceOf(msg.sender); + } + + function payableLookBalance() public payable returns (uint256) { + return balanceOf(msg.sender); + } } diff --git a/package.json b/package.json index 2eaecf8..d9ee4e7 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@account-abstraction/contracts": "^0.6.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.8", "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-waffle": "^2.0.1", "@openzeppelin/hardhat-upgrades": "^1.28.0", @@ -36,6 +37,7 @@ "eslint-plugin-standard": "^5.0.0", "ethereum-waffle": "^3.4.0", "ethers": "^5.4.2", + "ethjs-util": "^0.1.6", "hardhat": "^2.6.6", "hardhat-storage-layout": "^0.1.7", "solhint": "^3.3.7", diff --git a/test/bloctoaccount.test.ts b/test/bloctoaccount.test.ts index 6b2d46d..7e1113f 100644 --- a/test/bloctoaccount.test.ts +++ b/test/bloctoaccount.test.ts @@ -12,26 +12,38 @@ import { TestBloctoAccountCloneableWalletV200, TestBloctoAccountCloneableWalletV200__factory, TestERC20__factory, - BloctoAccountFactory__factory + BloctoAccountFactory__factory, + BloctoAccountFactory } from '../typechain' import { EntryPoint } from '@account-abstraction/contracts' + import { fund, createTmpAccount, createAccount, deployEntryPoint, ONE_ETH, + TWO_ETH, createAuthorizedCosignerRecoverWallet, txData, signMessage, getMergedKey, signMessageWithoutChainId, - TWO_ETH + rethrow + } from './testutils' import '@openzeppelin/hardhat-upgrades' import { hexZeroPad } from '@ethersproject/bytes' import { deployCREATE3Factory, getDeployCode } from '../src/create3Factory' import { create3DeployTransparentProxy } from '../src/deployAccountFactoryWithCreate3' +import { mine } from '@nomicfoundation/hardhat-network-helpers' + +import { fillUserOpDefaults, getUserOpHash, packUserOp, signUserOp, fillAndSignWithCoSigner, fillSignWithEIP191V0 } from './entrypoint/UserOp' +import { UserOperation } from './entrypoint/UserOperation' + +import { hexConcat } from 'ethers/lib/utils' +import { zeroAddress } from 'ethereumjs-util' +import { create } from 'domain' describe('BloctoAccount Upgrade Test', function () { const ethersSigner = ethers.provider.getSigner() @@ -47,12 +59,12 @@ describe('BloctoAccount Upgrade Test', function () { let create3Factory: CREATE3Factory - let testERC20: TettERC20 + let testERC20: TestERC20__factory const NowVersion = '1.4.0' const NextVersion = '1.5.0' - async function testCreateAccount (salt = 0, mergedKeyIndex = 0): Promise { + async function testCreateAccount (salt = 0, mergedKeyIndex = 0, ifactory = factory): Promise { const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, mergedKeyIndex) const account = await createAccount( @@ -63,7 +75,7 @@ describe('BloctoAccount Upgrade Test', function () { BigNumber.from(salt), pxIndexWithParity, px, - factory + ifactory ) await fund(account) @@ -175,7 +187,7 @@ describe('BloctoAccount Upgrade Test', function () { expect(await account.VERSION()).to.eql(NextVersion) }) - describe('wallet function', () => { + describe('wallet functions', () => { const AccountSalt = 123 it('should receive native token', async () => { @@ -290,6 +302,130 @@ describe('BloctoAccount Upgrade Test', function () { }) }) + describe('4337 functions', () => { + let account: BloctoAccount + let factory: BloctoAccountFactory + + before(async () => { + const accountContractSalt = hexZeroPad(Buffer.from('BloctoAccount_test_4337', 'utf-8'), 32) + await create3Factory.deploy( + accountContractSalt, + getDeployCode(new BloctoAccountCloneableWallet__factory(), [entryPoint.address]) + ) + + implementation = await create3Factory.getDeployed(await ethersSigner.getAddress(), accountContractSalt) + expect((await ethers.provider.getCode(implementation))).not.equal('0x') + const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactory') + const create3Salt = hexZeroPad(Buffer.from('AccountFactory_test_4337', 'utf-8'), 32) + factory = await create3DeployTransparentProxy(BloctoAccountFactory, + [implementation, entryPoint.address, await ethersSigner.getAddress()], + { initializer: 'initialize' }, create3Factory, ethersSigner, create3Salt) + await factory.grantRole(await factory.CREATE_ACCOUNT_ROLE(), await ethersSigner.getAddress()) + + account = await testCreateAccount(433700, 0, factory) + }) + + it('should execute transfer ERC20 from entrypoint', async () => { + const receiver = createTmpAccount() + const beneficiaryAddress = createTmpAccount().address + await testERC20.mint(account.address, TWO_ETH) + const erc20Transfer = await testERC20.populateTransaction.transfer(receiver.address, ONE_ETH) + const accountExecFromEntryPoint = await account.populateTransaction.execute(testERC20.address, 0, erc20Transfer.data!) + + const op1 = await fillSignWithEIP191V0({ + callData: accountExecFromEntryPoint.data, + sender: account.address, + callGasLimit: 2e6, + verificationGasLimit: 1e5 + }, authorizedWallet, cosignerWallet, entryPoint, account.address) + + // start test + // test send ERC20 + const beforeRecevive = await testERC20.balanceOf(receiver.address) + await entryPoint.handleOps([op1], beneficiaryAddress).catch((rethrow())).then(async r => r!.wait()) + + expect(await testERC20.balanceOf(receiver.address)).to.equal(beforeRecevive.add(ONE_ETH)) + }) + + it('should revert execute transfer ERC20 from entrypoint', async () => { + const account2 = await testCreateAccount(433702, 0, factory) + const beneficiaryAddress = createTmpAccount().address + // 0xf2fde38a: any non-exist function + const accountExecFromEntryPoint = await account2.populateTransaction.execute(testERC20.address, 0, '0xf2fde38a') + + const op1 = await fillSignWithEIP191V0({ + callData: accountExecFromEntryPoint.data, + sender: account2.address, + callGasLimit: 500, + verificationGasLimit: 1e5 + }, authorizedWallet, cosignerWallet, entryPoint, account.address) + + // start test + await expect(entryPoint.handleOps([op1], beneficiaryAddress)).to.revertedWith('VM Exception while processing transaction: reverted with an unrecognized custom error') + }) + + it('should executeBatch transfer ERC20 from entrypoint', async () => { + const receiver1 = createTmpAccount() + const receiver2 = createTmpAccount() + const beneficiaryAddress = createTmpAccount().address + await testERC20.mint(account.address, TWO_ETH) + const erc20Transfer1 = await testERC20.populateTransaction.transfer(receiver1.address, ONE_ETH) + const erc20Transfer2 = await testERC20.populateTransaction.transfer(receiver2.address, ONE_ETH) + const accountExecFromEntryPoint = await account.populateTransaction.executeBatch( + [testERC20.address, testERC20.address], + [0, 0], [erc20Transfer1.data!, erc20Transfer2.data!]) + + const op1 = await fillSignWithEIP191V0({ + callData: accountExecFromEntryPoint.data, + sender: account.address, + callGasLimit: 2e6, + verificationGasLimit: 1e5 + }, authorizedWallet, cosignerWallet, entryPoint, account.address) + + // start test + // test send ERC20 + const beforeRecevive1 = await testERC20.balanceOf(receiver1.address) + const beforeRecevive2 = await testERC20.balanceOf(receiver2.address) + + await entryPoint.handleOps([op1], beneficiaryAddress).catch((rethrow())).then(async r => r!.wait()) + + expect(await testERC20.balanceOf(receiver1.address)).to.equal(beforeRecevive1.add(ONE_ETH)) + expect(await testERC20.balanceOf(receiver2.address)).to.equal(beforeRecevive2.add(ONE_ETH)) + }) + + it('should deposit & getDeposit by anyone', async () => { + const depositor = createTmpAccount() + await fund(depositor.address, '2') + const accountLinkDepositor = await BloctoAccount__factory.connect(account.address, depositor) + const beforeDeposit = await account.getDeposit() + await accountLinkDepositor.addDeposit({ value: ONE_ETH }) + expect(await account.getDeposit()).to.equal(beforeDeposit.add(ONE_ETH)) + }) + + it('should withdraw deposit', async () => { + const beneficiary = createTmpAccount() + const depositor = createTmpAccount() + await fund(depositor.address, '2') + const accountLinkDepositor = await BloctoAccount__factory.connect(account.address, depositor) + await accountLinkDepositor.addDeposit({ value: ONE_ETH }) + + const withdrawDepositToTx = await account.populateTransaction.withdrawDepositTo(beneficiary.address, ONE_ETH) + const accountExecFromEntryPoint = await account.populateTransaction.execute(account.address, 0, withdrawDepositToTx.data!) + + const op1 = await fillSignWithEIP191V0({ + callData: accountExecFromEntryPoint.data, + sender: account.address, + callGasLimit: 2e6, + verificationGasLimit: 1e5 + }, authorizedWallet, cosignerWallet, entryPoint, account.address) + + const beforeRecevive = await ethers.provider.getBalance(beneficiary.address) + await entryPoint.handleOps([op1], depositor.address).catch((rethrow())).then(async r => r!.wait()) + expect(await ethers.provider.getBalance(beneficiary.address)).to.equal(beforeRecevive.add(ONE_ETH)) + }) + }) + + // for Blocto Account Factory describe('should upgrade factory to different version implementation', () => { const TestSalt = 135341 @@ -332,4 +468,48 @@ describe('BloctoAccount Upgrade Test', function () { ) }) }) + + describe('EOA entrypoint for _call fail test', () => { + let account: BloctoAccount + let factory: BloctoAccountFactory + const entrypointEOA = createTmpAccount() + + before(async () => { + const accountContractSalt = hexZeroPad(Buffer.from('test_call_account', 'utf-8'), 32) + await create3Factory.deploy( + accountContractSalt, + getDeployCode(new BloctoAccountCloneableWallet__factory(), [entrypointEOA.address]) + ) + + implementation = await create3Factory.getDeployed(await ethersSigner.getAddress(), accountContractSalt) + expect((await ethers.provider.getCode(implementation))).not.equal('0x') + const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactory') + const create3Salt = hexZeroPad(Buffer.from('test_call_factory', 'utf-8'), 32) + factory = await create3DeployTransparentProxy(BloctoAccountFactory, + [implementation, entrypointEOA.address, await ethersSigner.getAddress()], + { initializer: 'initialize' }, create3Factory, ethersSigner, create3Salt) + await factory.grantRole(await factory.CREATE_ACCOUNT_ROLE(), await ethersSigner.getAddress()) + + // create account with entrypoint EOA + const mergedKeyIndex = 0 + const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, mergedKeyIndex) + + account = await createAccount( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + BigNumber.from(6346346), + pxIndexWithParity, + px, + factory + ) + }) + + it('should revert for execute non exist function', async () => { + const accountLinkEntrypoint = await BloctoAccount__factory.connect(account.address, entrypointEOA) + await expect(accountLinkEntrypoint.execute(testERC20.address, 0, '0xf2fde38a')) + .to.be.reverted + }) + }) }) diff --git a/test/corewallet.test.ts b/test/corewallet.test.ts new file mode 100644 index 0000000..a1f8b80 --- /dev/null +++ b/test/corewallet.test.ts @@ -0,0 +1,466 @@ +// acknowledgement https://github.com/dapperlabs/dapper-contracts/blob/master/test/wallet.test.js +// update it from js to ts and fit ethers +import { ethers } from 'hardhat' +import { Wallet, BigNumber } from 'ethers' +import { expect } from 'chai' +import { + BloctoAccount, + BloctoAccount__factory, + BloctoAccountCloneableWallet__factory, + CREATE3Factory, + TestERC20__factory +} from '../typechain' +import { EntryPoint } from '@account-abstraction/contracts' +import { + fund, + createTmpAccount, + createAccount, + deployEntryPoint, + ONE_ETH, + createAuthorizedCosignerRecoverWallet, + txData, + signMessage, + getMergedKey, + signMessageWithoutChainId, + TWO_ETH +} from './testutils' +import '@openzeppelin/hardhat-upgrades' +import { hexZeroPad } from '@ethersproject/bytes' +import { deployCREATE3Factory, getDeployCode } from '../src/create3Factory' +import { create3DeployTransparentProxy } from '../src/deployAccountFactoryWithCreate3' + +const ShowLog = false + +function log (...args: any): void { + if (ShowLog) console.log(...args) +} + +describe('BloctoAccount CoreWallet Test', function () { + const ethersSigner = ethers.provider.getSigner() + const WalletSalt = 123456 + + let authorizedWallet: Wallet + let cosignerWallet: Wallet + let recoverWallet: Wallet + + let implementation: string + let factory: BloctoAccountFactory + + let entryPoint: EntryPoint + + let create3Factory: CREATE3Factory + + let testERC20: TettERC20 + + async function testCreateAccount (salt = 0, mergedKeyIndex = 0): Promise { + const [px, pxIndexWithParity] = getMergedKey(authorizedWallet, cosignerWallet, mergedKeyIndex) + + const account = await createAccount( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + BigNumber.from(salt), + pxIndexWithParity, + px, + factory + ) + await fund(account) + + return account + } + + async function invokeWithCosigner (account: BloctoAccount, data: Uint8Array, + authorized: Wallet = authorizedWallet, cosigner: Wallet = cosignerWallet): Promise { + const newNonce: BigNumber = (await account.nonce()).add(1) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosigner) + + const sign = await signMessage(authorized, account.address, newNonce, data) + await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, newNonce, authorized.address, data) + } + + // use authorizedWallet and cosignerWallet to send ERC20 from cosigner + async function sendERC20ByCosigner (account: BloctoAccount, to: string, amount: BigNumber, withChainId: boolean = true, + authorized: Wallet = authorizedWallet, cosigner: Wallet = cosignerWallet): Promise { + // const authorizeInAccountNonce = (await account.nonces(authorizedWallet.address)).add(1) + const authorizeInAccountNonce = (await account.nonce()).add(1) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosigner) + const data = txData(1, testERC20.address, BigNumber.from(0), + testERC20.interface.encodeFunctionData('transfer', [to, amount])) + + const sign = withChainId ? await signMessage(authorized, account.address, authorizeInAccountNonce, data) : await signMessageWithoutChainId(authorized, account.address, authorizeInAccountNonce, data) + await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, authorizeInAccountNonce, authorized.address, data) + } + + // use authorizedWallet and cosignerWallet to send ERC20 from authorized + async function sendERC20ByAuthorized (account: BloctoAccount, to: string, amount: BigNumber, withChainId: boolean = true, + authorized: Wallet = authorizedWallet, cosigner: Wallet = cosignerWallet): Promise { + const authorizeInAccountNonce = (await account.nonce()) + const accountLinkAuthorized = BloctoAccount__factory.connect(account.address, authorized) + const data = txData(1, testERC20.address, BigNumber.from(0), + testERC20.interface.encodeFunctionData('transfer', [to, amount])) + + const sign = withChainId ? await signMessage(cosigner, account.address, authorizeInAccountNonce, data, authorized.address) : await signMessageWithoutChainId(cosigner, account.address, authorizeInAccountNonce, data) + await accountLinkAuthorized.invoke1SignerSends(sign.v, sign.r, sign.s, data) + } + + // use authorizedWallet and cosignerWallet to setDelegate from cosigner + async function setDelegateByCosigner (account: BloctoAccount, interfaceId: string, delegateAddr: string, + authorized: Wallet = authorizedWallet, cosigner: Wallet = cosignerWallet): Promise { + // const authorizeInAccountNonce = (await account.nonces(authorizedWallet.address)).add(1) + const authorizeInAccountNonce = (await account.nonce()).add(1) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosigner) + + const data = txData(1, account.address, BigNumber.from(0), + account.interface.encodeFunctionData('setDelegate', [interfaceId, delegateAddr])) + + const sign = await signMessage(authorized, account.address, authorizeInAccountNonce, data) + await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, authorizeInAccountNonce, authorized.address, data) + } + + before(async function () { + // 3 account + [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + await fund(authorizedWallet.address) + await fund(cosignerWallet.address) + // 4337 + entryPoint = await deployEntryPoint() + + // create3 factory + create3Factory = await deployCREATE3Factory(ethersSigner) + + const accountSalt = hexZeroPad(Buffer.from('BloctoAccount_v140', 'utf-8'), 32) + implementation = await create3Factory.getDeployed(await ethersSigner.getAddress(), accountSalt) + expect((await ethers.provider.getCode(implementation))).to.equal('0x') + + await create3Factory.deploy( + accountSalt, + getDeployCode(new BloctoAccountCloneableWallet__factory(), [entryPoint.address]) + ) + + expect((await ethers.provider.getCode(implementation))).not.equal('0x') + + // account factory + const BloctoAccountFactory = await ethers.getContractFactory('BloctoAccountFactoryV140') + factory = await create3DeployTransparentProxy(BloctoAccountFactory, + [implementation, entryPoint.address, await ethersSigner.getAddress()], + { initializer: 'initialize' }, create3Factory, ethersSigner) + await factory.grantRole(await factory.CREATE_ACCOUNT_ROLE(), await ethersSigner.getAddress()) + + // testERC20 deploy + testERC20 = await new TestERC20__factory(ethersSigner).deploy('TestERC20', 'TST', 18) + }) + + describe('emergency recovery performed (emergencyRecovery)', () => { + let account: BloctoAccount + + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + let curAuthVersion: BigNumber + // let accountLinkCosigner2: BloctoAccount + + before(async function () { + // account for test + account = await testCreateAccount(WalletSalt) + const [px2, pxIndexWithParity2] = getMergedKey(authorizedWallet2, cosignerWallet2, 1) + // must call with recovery address + curAuthVersion = await account.authVersion() + // fund + await fund(recoverWallet.address) + await fund(cosignerWallet2.address) + const accountLinkRecovery = BloctoAccount__factory.connect(account.address, recoverWallet) + const res = await accountLinkRecovery.emergencyRecovery(authorizedWallet2.address, cosignerWallet2.address, pxIndexWithParity2, px2) + const receipt = await res.wait() + // 81313 + log('emergencyRecovery gas used: ', receipt.gasUsed.toString()) + }) + + it('backup key is different (check new authorized & cosigner)', async () => { + expect(authorizedWallet.address).not.equal(authorizedWallet2.address) + expect(cosignerWallet.address).not.equal(cosignerWallet2.address) + }) + it('should be able to perform transactions with backup key (send ERC20)', async () => { + // prepare + const receiveAccount = createTmpAccount() + await testERC20.mint(account.address, TWO_ETH) + + // test send ERC20 + const before = await testERC20.balanceOf(account.address) + const beforeRecevive = await testERC20.balanceOf(receiveAccount.address) + + await sendERC20ByCosigner(account, receiveAccount.address, ONE_ETH, true, authorizedWallet2, cosignerWallet2) + + expect(await testERC20.balanceOf(account.address)).to.equal(before.sub(ONE_ETH)) + expect(await testERC20.balanceOf(receiveAccount.address)).to.equal(beforeRecevive.add(ONE_ETH)) + }) + + it('should not be able to perform transactions with old key', async function () { + const receiveAccount = createTmpAccount() + await expect(sendERC20ByCosigner(account, receiveAccount.address, ONE_ETH, true, authorizedWallet, cosignerWallet)).to.revertedWith('authorized addresses must be equal') + }) + + it('should see that the auth version has incremented', async function () { + const authVersionIncrementor = await account.AUTH_VERSION_INCREMENTOR() + const res = curAuthVersion.add(authVersionIncrementor) + expect(await account.authVersion()).to.equal(res) + }) + + it('should be able to recover gas for previous version', async function () { + // call recover gas + // anyone can call recover gas + const anyAccount = createTmpAccount() + await fund(anyAccount.address) + const accountLinkAny = BloctoAccount__factory.connect(account.address, anyAccount) + const res = await accountLinkAny.recoverGas(1, [authorizedWallet.address]) + log('recoverGas gas used: ', (await res.wait()).gasUsed) + }) + + it('should be able to set a new recovery address', async function () { + const setRecoveryAddressData = txData(1, account.address, BigNumber.from(0), + account.interface.encodeFunctionData('setRecoveryAddress', [recoverWallet2.address])) + + await invokeWithCosigner(account, setRecoveryAddressData, authorizedWallet2, cosignerWallet2) + + expect(await account.recoveryAddress()).to.equal(recoverWallet2.address) + }) + }) + + describe('emergency recovery 2 performed (emergencyRecovery2)', () => { + let account: BloctoAccount + + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + let curAuthVersion: BigNumber + // let accountLinkCosigner2: BloctoAccount + + before(async function () { + account = await testCreateAccount(WalletSalt + 2) + const [px2, pxIndexWithParity2] = getMergedKey(authorizedWallet2, cosignerWallet2, 1) + // must call with recovery address + curAuthVersion = await account.authVersion() + // fund + await fund(recoverWallet.address) + await fund(cosignerWallet2.address) + const accountLinkRecovery = BloctoAccount__factory.connect(account.address, recoverWallet) + const res = await accountLinkRecovery.emergencyRecovery2(authorizedWallet2.address, cosignerWallet2.address, recoverWallet2.address, pxIndexWithParity2, px2) + const receipt = await res.wait() + // 81313 + log('emergencyRecovery gas used: ', receipt.gasUsed.toString()) + }) + + it('backup key is different (check new authorized & cosigner)', async () => { + expect(authorizedWallet.address).not.equal(authorizedWallet2.address) + expect(cosignerWallet.address).not.equal(cosignerWallet2.address) + }) + it('should be able to perform transactions with backup key (send ERC20)', async () => { + // prepare + const receiveAccount = createTmpAccount() + await testERC20.mint(account.address, TWO_ETH) + + // test send ERC20 + const before = await testERC20.balanceOf(account.address) + const beforeRecevive = await testERC20.balanceOf(receiveAccount.address) + + await sendERC20ByCosigner(account, receiveAccount.address, ONE_ETH, true, authorizedWallet2, cosignerWallet2) + + expect(await testERC20.balanceOf(account.address)).to.equal(before.sub(ONE_ETH)) + expect(await testERC20.balanceOf(receiveAccount.address)).to.equal(beforeRecevive.add(ONE_ETH)) + }) + + it('should not be able to perform transactions with old key', async function () { + const receiveAccount = createTmpAccount() + await expect(sendERC20ByCosigner(account, receiveAccount.address, ONE_ETH, true, authorizedWallet, cosignerWallet)).to.revertedWith('authorized addresses must be equal') + }) + + it('should see that the auth version has incremented', async function () { + const authVersionIncrementor = await account.AUTH_VERSION_INCREMENTOR() + const res = curAuthVersion.add(authVersionIncrementor) + expect(await account.authVersion()).to.equal(res) + }) + + it('should be able to recover gas for previous version', async function () { + // call recover gas + // anyone can call recover gas + const anyAccount = createTmpAccount() + await fund(anyAccount.address) + const accountLinkAny = BloctoAccount__factory.connect(account.address, anyAccount) + const res = await accountLinkAny.recoverGas(1, [authorizedWallet.address]) + log('recoverGas gas used: ', (await res.wait()).gasUsed) + }) + + it('should be able to set a new recovery address', async function () { + const setRecoveryAddressData = txData(1, account.address, BigNumber.from(0), + account.interface.encodeFunctionData('setRecoveryAddress', [recoverWallet2.address])) + + await invokeWithCosigner(account, setRecoveryAddressData, authorizedWallet2, cosignerWallet2) + + expect(await account.recoveryAddress()).to.equal(recoverWallet2.address) + }) + }) + + describe('authorized wallet send tx', () => { + const BloctoAccountSalt = 224230 + let account: BloctoAccount + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + + before(async function () { + const [px2, pxIndexWithParity2] = getMergedKey(authorizedWallet2, cosignerWallet2, 1) + account = await createAccount( + ethersSigner, + await authorizedWallet2.getAddress(), + await cosignerWallet2.getAddress(), + await recoverWallet2.getAddress(), + BigNumber.from(BloctoAccountSalt), + pxIndexWithParity2, + px2, + factory + ) + + // fund + await fund(account) + await fund(authorizedWallet2.address) + await fund(cosignerWallet2.address) + }) + + it('should be able to perform transactions with authorized key (send ERC20)', async () => { + // prepare + const receiveAccount = createTmpAccount() + await testERC20.mint(account.address, TWO_ETH) + + // test send ERC20 + const before = await testERC20.balanceOf(account.address) + const beforeRecevive = await testERC20.balanceOf(receiveAccount.address) + + await sendERC20ByAuthorized(account, receiveAccount.address, ONE_ETH, true, authorizedWallet2, cosignerWallet2) + + expect(await testERC20.balanceOf(account.address)).to.equal(before.sub(ONE_ETH)) + expect(await testERC20.balanceOf(receiveAccount.address)).to.equal(beforeRecevive.add(ONE_ETH)) + }) + + describe('isValidSignature test', () => { + it('shoule return 0 for wrong authorized signature', async () => { + const sig = '0x' + '2'.repeat(130) + expect(await account.isValidSignature('0x' + '1'.repeat(64), sig)).to.equal('0x00000000') + }) + it('shoule return 0 for wrong authorized with cosigner signature', async () => { + const sig = '0x' + '2'.repeat(260) + expect(await account.isValidSignature('0x' + '1'.repeat(64), sig)).to.equal('0x00000000') + }) + + it('shoule return 0 for wrong signature length', async () => { + const sig = '0x' + '2'.repeat(360) + expect(await account.isValidSignature('0x' + '1'.repeat(64), sig)).to.equal('0x00000000') + }) + + it('shoule return 0 for none zero authorized but zero for cosigner', async () => { + const fakeHash = '0x' + '1'.repeat(64) + const signWithAuthorized = await authorizedWallet.signMessage(ethers.utils.arrayify(fakeHash)) + + const sig = signWithAuthorized + '1'.repeat(130) + expect(await account.isValidSignature(fakeHash, sig)).to.equal('0x00000000') + }) + }) + }) + + describe('authorized wallet same as cosigner wallet send tx', () => { + const BloctoAccountSalt = 224230 + let account: BloctoAccount + const [authorizedWallet2, , recoverWallet2] = createAuthorizedCosignerRecoverWallet() + + before(async function () { + const [px2, pxIndexWithParity2] = getMergedKey(authorizedWallet2, authorizedWallet2, 1) + account = await createAccount( + ethersSigner, + await authorizedWallet2.getAddress(), + await authorizedWallet2.getAddress(), + await recoverWallet2.getAddress(), + BigNumber.from(BloctoAccountSalt), + pxIndexWithParity2, + px2, + factory + ) + + // fund + await fund(account) + await fund(authorizedWallet2.address) + }) + + it('should be able to perform transactions with authorized key (send ERC20)', async () => { + // prepare + const receiveAccount = createTmpAccount() + await testERC20.mint(account.address, TWO_ETH) + + const data = txData(1, testERC20.address, BigNumber.from(0), + testERC20.interface.encodeFunctionData('transfer', [receiveAccount.address, ONE_ETH])) + + const accountLinkAuthorized = BloctoAccount__factory.connect(account.address, authorizedWallet2) + + // test send ERC20 + const before = await testERC20.balanceOf(account.address) + const beforeRecevive = await testERC20.balanceOf(receiveAccount.address) + + await accountLinkAuthorized.invoke0(data) + expect(await testERC20.balanceOf(account.address)).to.equal(before.sub(ONE_ETH)) + expect(await testERC20.balanceOf(receiveAccount.address)).to.equal(beforeRecevive.add(ONE_ETH)) + }) + + it('should be able to receive native token', async () => { + // prepare + const beforeRecevive = await ethers.provider.getBalance(account.address) + const tx = await ethersSigner.sendTransaction({ + to: account.address, + value: ONE_ETH // Sends exactly 1.0 ether + }) + const receipt = await tx.wait() + const receivedSelector = ethers.utils.id('Received(address,uint256)') + expect(receipt.logs[0].topics[0]).to.equal(receivedSelector) + expect(await ethers.provider.getBalance(account.address)).to.equal(beforeRecevive.add(ONE_ETH)) + }) + }) + + describe('wallet delegate function', () => { + const BloctoAccountSalt = 224230 + let account: BloctoAccount + const [authorizedWallet2, cosignerWallet2, recoverWallet2] = createAuthorizedCosignerRecoverWallet() + + before(async function () { + const [px2, pxIndexWithParity2] = getMergedKey(authorizedWallet2, authorizedWallet2, 1) + account = await createAccount( + ethersSigner, + await authorizedWallet2.getAddress(), + await cosignerWallet2.getAddress(), + await recoverWallet2.getAddress(), + BigNumber.from(BloctoAccountSalt), + pxIndexWithParity2, + px2, + factory + ) + + // fund + await fund(account) + await fund(cosignerWallet2.address) + }) + + it('should be able to delegate function', async () => { + await testERC20.mint(account.address, TWO_ETH) + + const interfaceId = testERC20.interface.encodeFunctionData('senderBalance') + await setDelegateByCosigner(account, interfaceId, testERC20.address, authorizedWallet2, cosignerWallet2) + + const accountERC20 = TestERC20__factory.connect(account.address, authorizedWallet2) + expect(await accountERC20.senderBalance()).to.equal(TWO_ETH) + }) + + it('should be able to delegate function 2', async () => { + await testERC20.mint(account.address, TWO_ETH) + await fund(authorizedWallet2.address) + await fund(authorizedWallet2.address) + + const interfaceId = testERC20.interface.encodeFunctionData('payableLookBalance') + await setDelegateByCosigner(account, interfaceId, testERC20.address, authorizedWallet2, cosignerWallet2) + + const accountERC20 = TestERC20__factory.connect(account.address, authorizedWallet2) + const beforeRecevive = await ethers.provider.getBalance(account.address) + await accountERC20.payableLookBalance({ value: ONE_ETH }) + expect(await ethers.provider.getBalance(account.address)).to.equal(beforeRecevive.add(ONE_ETH)) + }) + }) +}) diff --git a/test/entrypoint/UserOp.ts b/test/entrypoint/UserOp.ts index 0659fb9..65c4efe 100644 --- a/test/entrypoint/UserOp.ts +++ b/test/entrypoint/UserOp.ts @@ -5,11 +5,8 @@ import { keccak256 } from 'ethers/lib/utils' import { BigNumber, Contract, Signer, Wallet } from 'ethers' -import { AddressZero, callDataCost, rethrow } from '../testutils' +import { AddressZero, callDataCost, rethrow, hashMessageEIP191V0, sign2Str } from '../testutils' import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util' -import { - EntryPoint -} from '../../typechain' import { UserOperation } from './UserOperation' import { Create2Factory } from '../../src/Create2Factory' @@ -220,3 +217,22 @@ export async function fillAndSignWithCoSigner (op: Partial, signe signature: signature } } + +export async function fillSignWithEIP191V0 (op: Partial, signer: Wallet, cosigner: Wallet, entryPoint?: EntryPoint, account: string = ''): Promise { + const provider = entryPoint?.provider + const op2 = await fillUserOp(op, entryPoint) + + const chainId = await provider!.getNetwork().then(net => net.chainId) + const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId)) + + const hashData = hashMessageEIP191V0(chainId, account, message) + // console.log('hashData:', hashData) + const signerSignature = sign2Str(signer, hashData) + const cosignerSignature = sign2Str(cosigner, hashData) + + const signature = signerSignature + cosignerSignature.slice(2) + return { + ...op2, + signature: signature + } +} diff --git a/test/entrypoint/entrypoint.test.ts b/test/entrypoint/entrypoint.test.ts index ecbb5c1..f140cc7 100644 --- a/test/entrypoint/entrypoint.test.ts +++ b/test/entrypoint/entrypoint.test.ts @@ -5,42 +5,21 @@ import { EntryPoint, BloctoAccount, BloctoAccountFactory, - BloctoAccountCloneableWallet, - BloctoAccountCloneableWallet__factory, - BloctoAccountFactory, - BloctoAccountFactory__factory + BloctoAccountCloneableWallet__factory } from '../../typechain' import { - AddressZero, - createAccountOwner, fund, checkForGeth, - rethrow, - tostr, - getAccountInitCode, - getAccountInitCode2, - calcGasUsage, - ONE_ETH, - TWO_ETH, deployEntryPoint, - getBalance, createAddress, - getAccountAddress, - HashZero, simulationResultCatch, - createTmpAccount, createAccount, createAuthorizedCosignerRecoverWallet, getMergedKey } from '../testutils' -import { checkForBannedOps } from './entrypoint_utils' -import { DefaultsForUserOp, getUserOpHash, fillAndSignWithCoSigner } from './UserOp' -import { UserOperation } from './UserOperation' -import { PopulatedTransaction } from 'ethers/lib/ethers' +import { getUserOpHash, fillAndSignWithCoSigner } from './UserOp' import { ethers } from 'hardhat' -import { arrayify, defaultAbiCoder, hexConcat, hexZeroPad, parseEther } from 'ethers/lib/utils' -import { BytesLike } from '@ethersproject/bytes' -import { toChecksumAddress } from 'ethereumjs-util' +import { hexConcat } from 'ethers/lib/utils' describe('EntryPoint', function () { let entryPoint: EntryPoint diff --git a/test/schnorrMultiSign.test.ts b/test/schnorrMultiSign.test.ts index 03cfe51..855f634 100644 --- a/test/schnorrMultiSign.test.ts +++ b/test/schnorrMultiSign.test.ts @@ -3,18 +3,24 @@ import { ethers } from 'hardhat' import { BigNumber } from 'ethers' import { expect } from 'chai' import { + BloctoAccount, BloctoAccountCloneableWallet__factory, + BloctoAccount__factory, BloctoAccountFactory } from '../typechain' import { + fund, createAccount, deployEntryPoint, createAuthorizedCosignerRecoverWallet, - hashMessageEIP191V0 + hashMessageEIP191V0, + signMessage, + txData } from './testutils' import Schnorrkel from '../src/schnorrkel.js/index' import { DefaultSigner } from './schnorrUtils' +import { time } from 'console' const ERC1271_MAGICVALUE_BYTES32 = '0x1626ba7e' @@ -82,6 +88,20 @@ describe('Schnorr MultiSign Test', function () { ]) + hexPxIndexWithParity const result = await account.isValidSignature(msgKeccak256, sigData) expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) + + // revoke key + // only invoke from invoke functions + await expect(account.setMergedKey(hexPxIndexWithParity, '0x' + '0'.repeat(64))).to.revertedWith('must be called from `invoke()`') + + // test setMergedKey + const newNonce = (await account.nonce()).add(1) + const setMergedKeyData = txData(1, account.address, BigNumber.from(0), + account.interface.encodeFunctionData('setMergedKey', [Number('0x' + hexPxIndexWithParity), '0x' + '0'.repeat(64)])) + const sign = await signMessage(authorizedWallet, account.address, newNonce, setMergedKeyData) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) + await fund(cosignerWallet.address) + await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, newNonce, authorizedWallet.address, setMergedKeyData) + await expect(account.isValidSignature(msgKeccak256, sigData)).to.revertedWith('ecrecover failed') }) it('check none zero mergedKeyIndex', async () => { @@ -131,4 +151,139 @@ describe('Schnorr MultiSign Test', function () { const result = await account.isValidSignature(msgKeccak256, sigData) expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) }) + + describe('should update account key', () => { + const [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + const signerOne = new DefaultSigner(authorizedWallet) + const signerTwo = new DefaultSigner(cosignerWallet) + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] + let account: BloctoAccount + + let pxIndexWithParity: number + let hexPxIndexWithParity: string + + // test data + const msgPrefix = 'just a test message' + + async function validateSignData (): Promise { + // multisig + const msg = msgPrefix + + const msgKeccak256 = ethers.utils.solidityKeccak256(['string'], [msg]) + + const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] + const msgEIP191V0 = hashMessageEIP191V0((await ethers.provider.getNetwork()).chainId, account.address, msgKeccak256) + const { signature: sigOne, challenge: e } = signerOne.multiSignMessage(msgEIP191V0, publicKeys, publicNonces) + const { signature: sigTwo } = signerTwo.multiSignMessage(msgEIP191V0, publicKeys, publicNonces) + const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) + + const abiCoder = new ethers.utils.AbiCoder() + const sigData = abiCoder.encode(['bytes32', 'bytes32'], [ + e.buffer, + sSummed.buffer + ]) + hexPxIndexWithParity + return await account.isValidSignature(msgKeccak256, sigData) + } + + before(async function () { + const mergedKeyIndex = 128 + (0 << 1) + // const [authorizedWallet, cosignerWallet, recoverWallet] = createAuthorizedCosignerRecoverWallet() + // const signerOne = new DefaultSigner(authorizedWallet) + // const signerTwo = new DefaultSigner(cosignerWallet) + + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) + // because of the parity byte is 2, 3 so sub 2 + pxIndexWithParity = combinedPublicKey.buffer.slice(0, 1).readInt8() - 2 + mergedKeyIndex + hexPxIndexWithParity = ethers.utils.hexlify(pxIndexWithParity).slice(-2) + + account = await createAccount( + ethersSigner, + await authorizedWallet.getAddress(), + await cosignerWallet.getAddress(), + await recoverWallet.getAddress(), + BigNumber.from(203), + pxIndexWithParity, + px, + factory + ) + }) + + it('should sign Schnorr message', async () => { + expect(await validateSignData()).to.equal(ERC1271_MAGICVALUE_BYTES32) + }) + + it('should revert if setMergedKey not invoke from intrenal', async () => { + // revoke key + // only invoke from invoke functions + await expect(account.setMergedKey('0x' + hexPxIndexWithParity, '0x' + '0'.repeat(64))).to.revertedWith('must be called from `invoke()`') + }) + + it('should revert if revoke merged key', async () => { + // test setMergedKey + const newNonce = (await account.nonce()).add(1) + const setMergedKeyData = txData(1, account.address, BigNumber.from(0), + account.interface.encodeFunctionData('setMergedKey', [pxIndexWithParity, '0x' + '0'.repeat(64)])) + const sign = await signMessage(authorizedWallet, account.address, newNonce, setMergedKeyData) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) + await fund(cosignerWallet.address) + await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, newNonce, authorizedWallet.address, setMergedKeyData) + + await expect(validateSignData()).to.revertedWith('ecrecover failed') + }) + + it('should recover merged key', async () => { + await expect(validateSignData()).to.revertedWith('ecrecover failed') + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) + + // test setMergedKey + const newNonce = (await account.nonce()).add(1) + const setMergedKeyData = txData(1, account.address, BigNumber.from(0), + account.interface.encodeFunctionData('setMergedKey', [pxIndexWithParity, px])) + const sign = await signMessage(authorizedWallet, account.address, newNonce, setMergedKeyData) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) + await fund(cosignerWallet.address) + await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, newNonce, authorizedWallet.address, setMergedKeyData) + + const authVersion = await account.authVersion() + expect(await account.mergedKeys(authVersion.add(pxIndexWithParity))).to.equal(px) + }) + + // expect(await validateSignData()).to.equal(ERC1271_MAGICVALUE_BYTES32) + // await expect(validateSignData()).to.revertedWith('ecrecover failed') + + it('should revert if revoke merged key 2', async () => { + // test setMergedKey + const newNonce = (await account.nonce()).add(1) + const setMergedKeyData = txData(1, account.address, BigNumber.from(0), + account.interface.encodeFunctionData('setMergedKey', [pxIndexWithParity, '0x' + '0'.repeat(64)])) + const sign = await signMessage(authorizedWallet, account.address, newNonce, setMergedKeyData) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) + await fund(cosignerWallet.address) + await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, newNonce, authorizedWallet.address, setMergedKeyData) + + await expect(validateSignData()).to.revertedWith('ecrecover failed') + }) + + it('should update key by setAuthorized()', async () => { + // now the merged key is revoked, so we can set new merged key + await expect(validateSignData()).to.revertedWith('ecrecover failed') + + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) + const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) + // test setMergedKey + const newNonce = (await account.nonce()).add(1) + const setAuthorizedData = txData(1, account.address, BigNumber.from(0), + account.interface.encodeFunctionData('setAuthorized', [authorizedWallet.address, cosignerWallet.address, pxIndexWithParity, px])) + const sign = await signMessage(authorizedWallet, account.address, newNonce, setAuthorizedData) + const accountLinkCosigner = BloctoAccount__factory.connect(account.address, cosignerWallet) + await fund(cosignerWallet.address) + await accountLinkCosigner.invoke1CosignerSends(sign.v, sign.r, sign.s, newNonce, authorizedWallet.address, setAuthorizedData) + + const authVersion = await account.authVersion() + expect(await account.mergedKeys(authVersion.add(pxIndexWithParity))).to.equal(px) + expect(await account.authorizations(authVersion.add(authorizedWallet.address))).to.equal(cosignerWallet.address) + }) + }) }) diff --git a/test/testutils.ts b/test/testutils.ts index 8acd290..a1c14ef 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -23,6 +23,8 @@ import { expect } from 'chai' import { Create2Factory } from '../src/Create2Factory' import Schnorrkel from '../src/schnorrkel.js/index' import { DefaultSigner } from './schnorrUtils' +import { toBuffer, fromSigned, toUnsigned, bufferToInt, addHexPrefix } from 'ethereumjs-util' +import { intToHex, stripHexPrefix } from 'ethjs-util' export const AddressZero = ethers.constants.AddressZero export const HashZero = ethers.constants.HashZero @@ -337,12 +339,12 @@ export function hashMessageEIP191V0WithoutChainId (address: string, message: Byt ])) } -export async function signMessage (signerWallet: Wallet, accountAddress: string, nonce: BigNumber, data: Uint8Array): 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([ nonceBytesLike, - signerWallet.address, + addrForData, data ]) const sign = signerWallet._signingKey().signDigest(hashMessageEIP191V0((await ethers.provider.getNetwork()).chainId, accountAddress, dataForHash)) @@ -376,3 +378,35 @@ export function getMergedKey (wallet1: Wallet, wallet2: Wallet, mergedKeyIndex: return [px, pxIndexWithParity] } + +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}` + ) + } + + if (targetLength < 0) { + throw new Error( + `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 { + const rSig = fromSigned(r) + const sSig = fromSigned(s) + const vSig = bufferToInt(v) + const rStr = padWithZeroes(toUnsigned(rSig).toString('hex'), 64) + const sStr = padWithZeroes(toUnsigned(sSig).toString('hex'), 64) + const vStr = stripHexPrefix(intToHex(vSig)) + return addHexPrefix(rStr.concat(sStr, vStr)) +} + +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)) +} diff --git a/yarn.lock b/yarn.lock index f5bb4af..ee661d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -738,6 +738,13 @@ mcl-wasm "^0.7.1" rustbn.js "~0.2.0" +"@nomicfoundation/hardhat-network-helpers@^1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.8.tgz#e4fe1be93e8a65508c46d73c41fa26c7e9f84931" + integrity sha512-MNqQbzUJZnCMIYvlniC3U+kcavz/PhhQSsY90tbEtUyMj/IQqsLwIRZa4ctjABh3Bz0KCh9OXUZ7Yk/d9hr45Q== + dependencies: + ethereumjs-util "^7.1.4" + "@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.0.tgz#83a7367342bd053a76d04bbcf4f373fef07cf760" @@ -4487,7 +4494,7 @@ ethereumjs-util@^5.0.0, ethereumjs-util@^5.0.1, ethereumjs-util@^5.1.1, ethereum rlp "^2.0.0" safe-buffer "^5.1.1" -ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.2: +ethereumjs-util@^7.0.2, ethereumjs-util@^7.0.3, ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.4: version "7.1.5" resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181" integrity sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==