diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 308b4cb..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Build -on: - push: - branches: - - '*' - pull_request: - types: [opened, reopened, synchronize] - -env: - TS_NODE_TRANSPILE_ONLY: 1 - FORCE_COLORS: 1 - -# todo: extract shared seto/checkout/install/compile, instead of repeat in each job. -jobs: - - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/setup-node@v1 - with: - node-version: '14' - - uses: actions/checkout@v1 - - uses: actions/cache@v2 - with: - path: node_modules - key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }} - - run: yarn install - - run: yarn compile - - - run: yarn run ci - - gas-checks: - runs-on: ubuntu-latest - services: - localgeth: - image: dtr22/geth-dev - - steps: - - uses: actions/setup-node@v1 - with: - node-version: '14' - - uses: actions/checkout@v1 - - uses: actions/cache@v2 - with: - path: node_modules - key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }} - - run: yarn install - - run: yarn compile - - run: yarn ci-gas-calc - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/setup-node@v1 - with: - node-version: '14' - - uses: actions/checkout@v1 - - uses: actions/cache@v2 - with: - path: node_modules - key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }} - - run: yarn install - - run: yarn lint - - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/setup-node@v1 - with: - node-version: '14' - - uses: actions/checkout@v1 - - uses: actions/cache@v2 - with: - path: node_modules - key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }} - - run: yarn install - - - run: yarn compile - - - run: FORCE_COLOR=1 yarn coverage - - uses: actions/upload-artifact@v2 - with: - name: solidity-coverage - path: | - coverage/ - coverage.json - diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..50b298f --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,17 @@ +name: Account Abstraction workflow + +on: + workflow_dispatch: + pull_request: + branches: + - vechain + +jobs: + call-workflow-hardhat-tests: + uses: ./.github/workflows/test-contracts.yml + with: + shard-matrix: "{ \"shard\": [1,2,3] }" + secrets: inherit + + + diff --git a/.github/workflows/test-contracts.yml b/.github/workflows/test-contracts.yml new file mode 100644 index 0000000..75ee6ec --- /dev/null +++ b/.github/workflows/test-contracts.yml @@ -0,0 +1,47 @@ +name: Account abstraction contract tests + +on: + workflow_call: + inputs: + shard-matrix: + required: true + type: string + +jobs: + run-tests-and-build-report: + name: Test Smart Contracts with Hardhat + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + packages: read + strategy: + fail-fast: false + matrix: ${{ fromJSON(inputs.shard-matrix) }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Use Node v20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + registry-url: 'https://npm.pkg.github.com' + always-auth: true + scope: '@vechain' + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: yarn install + + - name: Smart contract tests + run: yarn test:shard${{ matrix.shard }}:compose:v2 + + + diff --git a/Contributing.md b/CONTRIBUTING.md similarity index 100% rename from Contributing.md rename to CONTRIBUTING.md diff --git a/README.md b/README.md index 02da47a..6b0117f 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,48 @@ +# VeChain Account Abstraction contracts + Implementation of contracts for [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) account abstraction via alternative mempool. +This project is based on [eth-infinitism v0.6.0 implementation](https://github.com/eth-infinitism/account-abstraction/tree/abff2aca61a8f0934e533d0d352978055fddbd96). + ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) -# Vechain Specific Changes -The changes mainly concern VTHO support, as the gas unit that is refunded. +## VeChain Specific Changes +The changes mainly concern VTHO support, as the gas unit that is prefunded. -# Test +## Test using Thor Solo -## Deploy all on Solo +The tests run using Docker Compose to bring up a Thor Solo instance. You should also install all dependencies first by running `yarn install`. -Make sure your `hardhat.config.ts` has the following line: +When using Docker Compose V1, please run the script: -```ts -vechain: { - url: VECHAIN_URL_SOLO -} +```bash +yarn test:compose:v1 +``` + +If you have Docker Compose V2, please run this instead: + +```bash +yarn test:compose:v2 ``` +If you need to find out first which one is your version, please execute (V1 would be the below) -And then deploy all contracts (entryPoint included) ```bash -yarn hardhat test --network vechain test/deploy-contracts.test.ts +➜ docker-compose -v +docker-compose version 1.29.2, build 5becea4c ``` -## Deploy EntryPoint on Testnet +The test files are placed in separate folders (`shard1`, `shard2`...) so we can parallelise the execution in the pipelines. + +## Test on networks + +**DISCLAIMER**: There are over a hundred tests in this repository. Further adjustments might be required in this case (for instance, make sure that the sender account is well-funded). + +### Deploy contracts on Testnet To deploy on testnet modify the `hardhat.config.ts` with the following ```ts -vechain: { +vechain_testnet: { url: VECHAIN_URL_TESTNET, accounts: { mnemonic: "your testnet mnemonic goes here" @@ -37,14 +52,14 @@ vechain: { And run the deployment script ```bash -yarn hardhat test --network vechain test/deploy-entrypoint.test.ts +yarn hardhat test --network vechain_testnet shard1/deploy-contracts.test.ts ``` -## Deploy EntryPoint on Mainnet +### Deploy contracts on Mainnet To deploy on testnet modify the `hardhat.config.ts` with the following ```ts -vechain: { +vechain_mainnet: { url: VECHAIN_URL_MAINNET, accounts: { mnemonic: "your mainnet mnemonic goes here" @@ -54,26 +69,22 @@ vechain: { And run the deployment script ```bash -yarn hardhat test --network vechain test/deploy-entrypoint.test.ts +yarn hardhat test --network vechain_mainnet shard1/deploy-contracts.test.ts ``` +### Run tests on a network -Update [./test/config.ts](./test/config.ts) with the addresses of the deployed contracts and +Update [./test/utils/config.ts](./test/utils/config.ts) with the addresses of the deployed contracts and then for testnet: -Run entryPoint tests: ```bash -yarn hardhat test test/entrypoint.test.ts --network vechain +yarn test:testnet ``` -Run paymaster tests: +And for mainnet: ```bash -yarn hardhat test test/paymaster.test.ts --network vechain +yarn test:mainnet ``` -Run simple wallet tests: -```bash -yarn hardhat test test/simple-wallet.test.ts --network vechain -``` -# Resources +## Resources - [Vitalik's post on account abstraction without Ethereum protocol changes](https://medium.com/infinitism/erc-4337-account-abstraction-without-ethereum-protocol-changes-d75c9d94dc4a) \ No newline at end of file diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index f095ddf..4446fe8 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -149,6 +149,8 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps); + emit BeforeExecution(); + uint256 opIndex = 0; for (uint256 a = 0; a < opasLen; a++) { UserOpsPerAggregator calldata opa = opsPerAggregator[a]; @@ -164,8 +166,6 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard } } - emit BeforeExecution(); - uint256 collected = 0; opIndex = 0; for (uint256 a = 0; a < opasLen; a++) { diff --git a/contracts/samples/SimpleAccount.sol b/contracts/samples/SimpleAccount.sol index f8df24a..41e6b80 100644 --- a/contracts/samples/SimpleAccount.sol +++ b/contracts/samples/SimpleAccount.sol @@ -43,13 +43,13 @@ contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, In function deposit(uint256 amount) public { _onlyOwner(); require(VTHO_TOKEN_CONTRACT.approve(address(_entryPoint), amount), "Aproval to EntryPoint Failed"); - _entryPoint.depositAmountTo(address(this), amount); + entryPoint().depositAmountTo(address(this), amount); } function withdrawAll() public { _onlyOwner(); IStakeManager.DepositInfo memory depositInfo = _entryPoint.getDepositInfo(address(this)); - _entryPoint.withdrawTo(address(this), depositInfo.deposit); + entryPoint().withdrawTo(address(this), depositInfo.deposit); } // solhint-disable-next-line no-empty-blocks @@ -121,12 +121,14 @@ contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, In } } - function _authorizeUpgrade(address newImplementation) internal view override { - (newImplementation); - _onlyOwner(); + /** + * check current account deposit in the entryPoint + */ + function getDeposit() public view returns (uint256) { + return entryPoint().balanceOf(address(this)); } - /** + /** * withdraw value from the account's deposit * @param withdrawAddress target to send to * @param amount to withdraw @@ -134,5 +136,10 @@ contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, In function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public onlyOwner { entryPoint().withdrawTo(withdrawAddress, amount); } + + function _authorizeUpgrade(address newImplementation) internal view override { + (newImplementation); + _onlyOwner(); + } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 8dfdbaa..0bcc627 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -5,16 +5,11 @@ import 'hardhat-deploy' import '@nomiclabs/hardhat-etherscan' import '@nomiclabs/hardhat-truffle5' -import { VECHAIN_URL_SOLO } from '@vechain/hardhat-vechain' +import { VECHAIN_URL_MAINNET, VECHAIN_URL_SOLO, VECHAIN_URL_TESTNET } from '@vechain/hardhat-vechain' import '@vechain/hardhat-ethers' import '@vechain/hardhat-web3' -const optimizedComilerSettings = { - version: '0.8.17', - settings: { - optimizer: { enabled: true, runs: 1000000 } - } -} +const shardNumber = process.env.shard // You need to export an object to set up your config // Go to https://hardhat.org/config/ to learn more @@ -22,7 +17,7 @@ const optimizedComilerSettings = { const config: HardhatUserConfig = { solidity: { compilers: [{ - version: '0.8.15', + version: '0.8.20', settings: { optimizer: { enabled: true, runs: 1000000 } } @@ -31,8 +26,19 @@ const config: HardhatUserConfig = { networks: { vechain: { url: VECHAIN_URL_SOLO + }, + vechain_testnet: { + url: VECHAIN_URL_TESTNET + }, + vechain_mainnet: { + url: VECHAIN_URL_MAINNET } }, + paths: { + tests: shardNumber !== undefined && shardNumber !== null && shardNumber !== '' + ? `./test/shard${shardNumber}` + : './test' + }, mocha: { timeout: 180000 } diff --git a/package.json b/package.json index 2f04cb5..ea80a33 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,17 @@ "lint:sol": "solhint -f unix \"contracts/**/*.sol\" --max-warnings 0", "gas-calc": "./scripts/gascalc", "mocha-gascalc": "TS_NODE_TRANSPILE_ONLY=1 npx ts-mocha --bail gascalc/*", - "test": "docker-compose up -d thor-solo && sleep 10 && npx hardhat test --network vechain; ret=$?; docker-compose down; exit $ret", + "test:compose:v1": "docker-compose up -d thor-solo && sleep 10 && npx hardhat test --network vechain; ret=$?; docker-compose down; exit $ret", + "test:compose:v2": "docker compose up -d thor-solo && sleep 10 && npx hardhat test --network vechain; ret=$?; docker compose down; exit $ret", + "test:shard1:compose:v2": "docker compose up -d thor-solo && sleep 10 && shard='1' npx hardhat test --network vechain; ret=$?; docker compose down; exit $ret", + "test:shard2:compose:v2": "docker compose up -d thor-solo && sleep 10 && shard='2' npx hardhat test --network vechain; ret=$?; docker compose down; exit $ret", + "test:shard3:compose:v2": "docker compose up -d thor-solo && sleep 10 && shard='3' npx hardhat test --network vechain; ret=$?; docker compose down; exit $ret", + "test:testnet": "NETWORK=testnet npx hardhat test --network vechain_testnet", + "test:mainnet": "NETWORK=mainnet npx hardhat test --network vechain_mainnet", "coverage": "COVERAGE=1 hardhat coverage", "deploy": "./scripts/hh-wrapper deploy", "test-dev": "hardhat test --network dev", - "ci": "yarn compile && hardhat test && yarn run runop", + "ci": "yarn test && yarn run runop", "ci-gas-calc": "yarn gas-calc && yarn check-gas-reports", "check-gas-reports": "./scripts/check-gas-reports", "runop": "hardhat run src/runop.ts ", diff --git a/src/AASigner.ts b/src/AASigner.ts index 8cee2fc..97f27c0 100644 --- a/src/AASigner.ts +++ b/src/AASigner.ts @@ -1,7 +1,15 @@ -import { BigNumber, Bytes, ethers, Event, Signer } from 'ethers' -import { zeroAddress } from 'ethereumjs-util' -import { BaseProvider, Provider, TransactionRequest } from '@ethersproject/providers' +import { TransactionResponse } from '@ethersproject/abstract-provider' +import { TransactionReceipt } from '@ethersproject/abstract-provider/src.ts/index' +import { BytesLike, hexValue } from '@ethersproject/bytes' import { Deferrable, resolveProperties } from '@ethersproject/properties' +import { BaseProvider, Provider, TransactionRequest } from '@ethersproject/providers' +import { zeroAddress } from 'ethereumjs-util' +import { BigNumber, Bytes, ethers, Event, Signer } from 'ethers' +import { getCreate2Address, hexConcat, Interface, keccak256 } from 'ethers/lib/utils' +import { clearInterval } from 'timers' +import { HashZero } from '../test/utils/testutils' +import { fillAndSign, getUserOpHash } from '../test/utils/UserOp' +import { UserOperation } from '../test/utils/UserOperation' import { EntryPoint, EntryPoint__factory, @@ -9,15 +17,7 @@ import { SimpleAccount, SimpleAccount__factory } from '../typechain' -import { BytesLike, hexValue } from '@ethersproject/bytes' -import { TransactionResponse } from '@ethersproject/abstract-provider' -import { fillAndSign, getUserOpHash } from '../test/UserOp' -import { UserOperation } from '../test/UserOperation' -import { TransactionReceipt } from '@ethersproject/abstract-provider/src.ts/index' -import { clearInterval } from 'timers' import { Create2Factory } from './Create2Factory' -import { getCreate2Address, hexConcat, Interface, keccak256 } from 'ethers/lib/utils' -import { HashZero } from '../test/testutils' export type SendUserOp = (userOp: UserOperation) => Promise diff --git a/src/runop.ts b/src/runop.ts index d9fd0b2..dcd91f9 100644 --- a/src/runop.ts +++ b/src/runop.ts @@ -1,14 +1,14 @@ // run a single op // "yarn run runop [--network ...]" +import { TransactionReceipt } from '@ethersproject/abstract-provider/src.ts/index' +import { providers } from 'ethers' +import { parseEther } from 'ethers/lib/utils' import hre, { ethers } from 'hardhat' -import { objdump } from '../test/testutils' +import '../test/utils/aa.init' +import { objdump } from '../test/utils/testutils' +import { EntryPoint__factory, TestCounter__factory } from '../typechain' import { AASigner, localUserOpSender, rpcUserOpSender } from './AASigner' -import { TestCounter__factory, EntryPoint__factory } from '../typechain' -import '../test/aa.init' -import { parseEther } from 'ethers/lib/utils' -import { providers } from 'ethers' -import { TransactionReceipt } from '@ethersproject/abstract-provider/src.ts/index'; // eslint-disable-next-line @typescript-eslint/no-floating-promises (async () => { diff --git a/test/_debugTx.ts b/test/_debugTx.ts deleted file mode 100644 index dc22598..0000000 --- a/test/_debugTx.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ethers } from 'hardhat' - -export interface DebugLog { - pc: number - op: string - gasCost: number - depth: number - stack: string[] - memory: string[] -} - -export interface DebugTransactionResult { - gas: number - failed: boolean - returnValue: string - structLogs: DebugLog[] -} - -export async function debugTransaction (txHash: string, disableMemory = true, disableStorage = true): Promise { - const debugTx = async (hash: string): Promise => await ethers.provider.send('debug_traceTransaction', [hash, { - disableMemory, - disableStorage - }]) - - return await debugTx(txHash) -} diff --git a/test/deploy-entrypoint.ts b/test/deploy-entrypoint.ts deleted file mode 100644 index d5055ef..0000000 --- a/test/deploy-entrypoint.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { artifacts } from 'hardhat' - -const TestUtil = artifacts.require('TestUtil') -const EntryPoint = artifacts.require('EntryPoint') -const SimpleAccountFactory = artifacts.require('SimpleAccountFactory') -const SimpleAccount = artifacts.require('SimpleAccount') -const TokenPaymaster = artifacts.require('TokenPaymaster') -const { expect } = require('chai') - -contract('Deployments', function (accounts) { - it('Adresses', async function () { - const entryPoint = await EntryPoint.new({ from: accounts[0] }) - console.log(' EntryPoint address: ', entryPoint.address) - }) -}) diff --git a/test/_create2factory.test.ts b/test/shard1/_create2factory.test.ts similarity index 57% rename from test/_create2factory.test.ts rename to test/shard1/_create2factory.test.ts index 3ed7f78..b5565e5 100644 --- a/test/_create2factory.test.ts +++ b/test/shard1/_create2factory.test.ts @@ -1,26 +1,22 @@ import { expect } from 'chai' -import { ethers } from 'hardhat' -import { EntryPoint, SimpleAccountFactory, SimpleAccountFactory__factory, TestUtil } from '../typechain' +import { artifacts, contract, ethers } from 'hardhat' +import { EntryPoint, SimpleAccountFactory, SimpleAccountFactory__factory } from '../../typechain' -const TestUtil = artifacts.require('TestUtil') -const EntryPoint = artifacts.require('EntryPoint') -const SimpleAccountFactory = artifacts.require('SimpleAccountFactory') -const { expect } = require('chai') +const EntryPointArtifact = artifacts.require('EntryPoint') +const SimpleAccountFactoryArtifact = artifacts.require('SimpleAccountFactory') -contract('Deployments', function (accounts) { - let testUtils: TestUtil +contract('Factory', function (accounts) { let entryPoint: EntryPoint let simpleAccountFactory: SimpleAccountFactory const provider = ethers.provider beforeEach('deploy all', async function () { - testUtils = await TestUtil.new({ from: accounts[0] }) - entryPoint = await EntryPoint.new({ from: accounts[0] }) - simpleAccountFactory = await SimpleAccountFactory.new(entryPoint.address, { from: accounts[0] }) + entryPoint = await EntryPointArtifact.new({ from: accounts[0] }) + simpleAccountFactory = await SimpleAccountFactoryArtifact.new(entryPoint.address, { from: accounts[0] }) }) it('should deploy to known address', async () => { - const factory = await SimpleAccountFactory__factory.connect(simpleAccountFactory.address, ethers.provider.getSigner()) + const factory = SimpleAccountFactory__factory.connect(simpleAccountFactory.address, ethers.provider.getSigner()) const simpleAccountAddress = await factory.getAddress(await ethers.provider.getSigner().getAddress(), 0) await factory.createAccount(await ethers.provider.getSigner().getAddress(), 0) @@ -30,7 +26,7 @@ contract('Deployments', function (accounts) { }) it('should deploy to different address based on salt', async () => { - const factory = await SimpleAccountFactory__factory.connect(simpleAccountFactory.address, ethers.provider.getSigner()) + const factory = SimpleAccountFactory__factory.connect(simpleAccountFactory.address, ethers.provider.getSigner()) const simpleAccountAddress = await factory.getAddress(await ethers.provider.getSigner().getAddress(), 123) await factory.createAccount(await ethers.provider.getSigner().getAddress(), 123) diff --git a/test/deploy-contracts.test.ts b/test/shard1/deploy-contracts.test.ts similarity index 76% rename from test/deploy-contracts.test.ts rename to test/shard1/deploy-contracts.test.ts index c303254..c57cc18 100644 --- a/test/deploy-contracts.test.ts +++ b/test/shard1/deploy-contracts.test.ts @@ -1,11 +1,9 @@ -import { artifacts } from 'hardhat' +import { artifacts, contract } from 'hardhat' const TestUtil = artifacts.require('TestUtil') const EntryPoint = artifacts.require('EntryPoint') const SimpleAccountFactory = artifacts.require('SimpleAccountFactory') -const SimpleAccount = artifacts.require('SimpleAccount') const TokenPaymaster = artifacts.require('TokenPaymaster') -const { expect } = require('chai') contract('Deployments', function (accounts) { it('Adresses', async function () { @@ -13,11 +11,6 @@ contract('Deployments', function (accounts) { const entryPoint = await EntryPoint.new({ from: accounts[0] }) const simpleAccountFactory = await SimpleAccountFactory.new(entryPoint.address, { from: accounts[0] }) - const tx = await simpleAccountFactory.createAccount(accounts[0], 0) - - const simpleAccountAddress = await simpleAccountFactory.getAddress(accounts[0], 0) - const simpleAccountContract = await new SimpleAccount(simpleAccountAddress) - const tokenPaymaster = await TokenPaymaster.new(simpleAccountFactory.address, 'ttt', entryPoint.address) const fakeSimpleAccountFactory = await SimpleAccountFactory.new(accounts[9], { from: accounts[0] }) diff --git a/test/helpers.test.ts b/test/shard1/helpers.test.ts similarity index 89% rename from test/helpers.test.ts rename to test/shard1/helpers.test.ts index dd69d55..409a73d 100644 --- a/test/helpers.test.ts +++ b/test/shard1/helpers.test.ts @@ -1,15 +1,12 @@ -import './aa.init' -import { BigNumber } from 'ethers' -import { AddressZero } from './testutils' import { expect } from 'chai' +import { BigNumber } from 'ethers' import { hexlify } from 'ethers/lib/utils' -import { TestHelpers, TestHelpers__factory } from '../typechain' import { ethers } from 'hardhat' +import { TestHelpers } from '../../typechain' +import '../utils/aa.init' +import { AddressZero } from '../utils/testutils' -const provider = ethers.provider -const ethersSigner = provider.getSigner() - -describe('#ValidationData helpers', function () { +describe('Helpers', function () { function pack (addr: string, validUntil: number, validAfter: number): BigNumber { return BigNumber.from(BigNumber.from(addr)) .add(BigNumber.from(validUntil).mul(BigNumber.from(2).pow(160))) @@ -22,7 +19,8 @@ describe('#ValidationData helpers', function () { const max48 = 2 ** 48 - 1 before(async () => { - helpers = await new TestHelpers__factory(ethersSigner).deploy() + const helpersFactory = await ethers.getContractFactory('TestHelpers') + helpers = await helpersFactory.deploy() }) it('#parseValidationData', async () => { diff --git a/test/paymaster.test.ts b/test/shard1/paymaster.test.ts similarity index 81% rename from test/paymaster.test.ts rename to test/shard1/paymaster.test.ts index 04031cd..4708639 100644 --- a/test/paymaster.test.ts +++ b/test/shard1/paymaster.test.ts @@ -13,29 +13,29 @@ import { TestCounter__factory, TokenPaymaster, TokenPaymaster__factory -} from '../typechain' -import config from './config' +} from '../../typechain' +import config from '../utils/config' import { AddressZero, calcGasUsage, - checkForGeth, - createAccount, + checkForBannedOps, + createAccountFromFactory, createAccountOwner, createAddress, - createRandomAccount, + createRandomAccountFromFactory, fund, getAccountAddress, getTokenBalance, ONE_ETH, rethrow -} from './testutils' -import { fillAndSign } from './UserOp' -import { UserOperation } from './UserOperation' +} from '../utils/testutils' +import { fillAndSign } from '../utils/UserOp' +import { UserOperation } from '../utils/UserOperation' const TokenPaymasterT = artifacts.require('TokenPaymaster') const TestCounterT = artifacts.require('TestCounter') -const ONE_HUNDERD_VTHO = '100000000000000000000' +const ONE_HUNDRED_VTHO = '100000000000000000000' describe('EntryPoint with paymaster', function () { let entryPoint: EntryPoint @@ -53,15 +53,23 @@ describe('EntryPoint with paymaster', function () { } before(async function () { - this.timeout(20000) - await checkForGeth() - - // Requires pre-deployment of entryPoint and Factory - entryPoint = await EntryPoint__factory.connect(config.entryPointAddress, ethers.provider.getSigner()) - factory = await SimpleAccountFactory__factory.connect(config.simpleAccountFactoryAddress, ethersSigner) - - accountOwner = createAccountOwner(); - ({ proxy: account } = await createAccount(ethersSigner, await accountOwner.getAddress())) + this.timeout(200000) + + if (process.env.NETWORK !== null && process.env.NETWORK !== undefined && process.env.NETWORK !== '') { + entryPoint = EntryPoint__factory.connect(config.entryPointAddress, ethers.provider.getSigner()) + factory = SimpleAccountFactory__factory.connect(config.simpleAccountFactoryAddress, ethersSigner) + } else { + const entryPointFactory = await ethers.getContractFactory('EntryPoint') + entryPoint = await entryPointFactory.deploy() + const accountFactoryFactory = await ethers.getContractFactory('SimpleAccountFactory') + factory = await accountFactoryFactory.deploy(entryPoint.address) + await factory.deployed() + } + + accountOwner = createAccountOwner() + + const createdAccount = await createAccountFromFactory(factory, ethersSigner, await accountOwner.getAddress()) + account = createdAccount.account await fund(account) }) @@ -73,7 +81,7 @@ describe('EntryPoint with paymaster', function () { before(async () => { const tokenPaymaster = await TokenPaymasterT.new(factory.address, 'ttt', entryPoint.address) - paymaster = await TokenPaymaster__factory.connect(tokenPaymaster.address, ethersSigner) + paymaster = TokenPaymaster__factory.connect(tokenPaymaster.address, ethersSigner) pmAddr = paymaster.address ownerAddr = await ethersSigner.getAddress() }) @@ -94,15 +102,14 @@ describe('EntryPoint with paymaster', function () { let paymaster: TokenPaymaster before(async () => { const tokenPaymaster = await TokenPaymasterT.new(factory.address, 'tst', entryPoint.address) - paymaster = await TokenPaymaster__factory.connect(tokenPaymaster.address, ethersSigner) - // await entryPoint.depositAmountTo(paymaster.address, BigNumber.from(ONE_HUNDERD_VTHO) ) + paymaster = TokenPaymaster__factory.connect(tokenPaymaster.address, ethersSigner) const vtho = ERC20__factory.connect(config.VTHOAddress, ethers.provider.getSigner()) - await vtho.approve(config.entryPointAddress, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(paymaster.address, BigNumber.from(ONE_HUNDERD_VTHO)) + await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDRED_VTHO)) + await entryPoint.depositAmountTo(paymaster.address, BigNumber.from(ONE_HUNDRED_VTHO)) - await vtho.approve(paymaster.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await paymaster.addStake(1, BigNumber.from(ONE_HUNDERD_VTHO)) + await vtho.approve(paymaster.address, BigNumber.from(ONE_HUNDRED_VTHO)) + await paymaster.addStake(1, BigNumber.from(ONE_HUNDRED_VTHO)) }) describe('#handleOps', () => { @@ -154,20 +161,17 @@ describe('EntryPoint with paymaster', function () { }, accountOwner, entryPoint) const preAddr = createOp.sender - await paymaster.mintTokens(preAddr, parseEther('1')) + await paymaster.mintTokens(preAddr, parseEther('1')).then(async tx => tx.wait()) // paymaster is the token, so no need for "approve" or any init function... - await entryPoint.simulateValidation(createOp, { gasLimit: 5e6 }).catch(e => e.message) - const [tx] = await ethers.provider.getBlock('latest').then(block => block.transactions) - // await checkForBannedOps(tx, true) + const transaction = await entryPoint.simulateValidation(createOp, { gasLimit: 1e7 }) + transaction.wait().catch(e => e.errorArgs) + const blockHash = transaction.blockHash ?? (await ethers.provider.getBlock('latest')).hash + await checkForBannedOps(blockHash, transaction.hash, true) - try { - const rcpt = await entryPoint.handleOps([createOp], beneficiaryAddress, { gasLimit: 1e7 }) - .catch(rethrow()).then(async tx => await tx!.wait()) // this sometimes fails - console.log('\t== create gasUsed=', rcpt.gasUsed.toString()) - await calcGasUsage(rcpt, entryPoint) - } catch (_) { - } + const rcpt = await entryPoint.handleOps([createOp], beneficiaryAddress, { gasLimit: 1e7 }).then(async tx => tx.wait()) + console.log('\t== create gasUsed=', rcpt.gasUsed.toString()) + await calcGasUsage(rcpt, entryPoint) created = true }) @@ -199,7 +203,7 @@ describe('EntryPoint with paymaster', function () { const beneficiaryAddress = createAddress() const testCounterContract = await TestCounterT.new() - const testCounter = await TestCounter__factory.connect(testCounterContract.address, ethersSigner) + const testCounter = TestCounter__factory.connect(testCounterContract.address, ethersSigner) const justEmit = testCounter.interface.encodeFunctionData('justemit') const execFromSingleton = account.interface.encodeFunctionData('execute', [testCounter.address, 0, justEmit]) @@ -207,12 +211,12 @@ describe('EntryPoint with paymaster', function () { const accounts: SimpleAccount[] = [] for (let i = 0; i < 4; i++) { - const { proxy: aAccount } = await createRandomAccount(ethersSigner, await accountOwner.getAddress()) + const { account: aAccount } = await createRandomAccountFromFactory(factory, ethersSigner, await accountOwner.getAddress()) // Fund account through EntryPoint const vtho = ERC20__factory.connect(config.VTHOAddress, ethers.provider.getSigner()) - await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(aAccount.address, BigNumber.from(ONE_HUNDERD_VTHO)) + await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDRED_VTHO)) + await entryPoint.depositAmountTo(aAccount.address, BigNumber.from(ONE_HUNDRED_VTHO)) await fund(aAccount) @@ -230,8 +234,7 @@ describe('EntryPoint with paymaster', function () { const pmBalanceBefore = await paymaster.balanceOf(paymaster.address).then(b => b.toNumber()) await entryPoint.handleOps(ops, beneficiaryAddress, { gasLimit: 1e7 }) - .catch(e => console.log(e.message)) - // .then(async tx => tx.wait()) + .then(async tx => tx.wait()) const totalPaid = await paymaster.balanceOf(paymaster.address).then(b => b.toNumber()) - pmBalanceBefore for (let i = 0; i < accounts.length; i++) { const bal = await getTokenBalance(paymaster, accounts[i].address) @@ -251,8 +254,9 @@ describe('EntryPoint with paymaster', function () { let approveCallData: string before(async function () { - this.timeout(200000); - ({ proxy: account2 } = await createAccount(ethersSigner, await accountOwner.getAddress())) + this.timeout(200000) + const accountFromFactory = await createAccountFromFactory(factory, ethersSigner, await accountOwner.getAddress()) + account2 = accountFromFactory.account await paymaster.mintTokens(account2.address, parseEther('1')) await paymaster.mintTokens(account.address, parseEther('1')) approveCallData = paymaster.interface.encodeFunctionData('approve', [account.address, ethers.constants.MaxUint256]) @@ -260,8 +264,8 @@ describe('EntryPoint with paymaster', function () { // Fund account through EntryPoint const vtho = ERC20__factory.connect(config.VTHOAddress, ethers.provider.getSigner()) - await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(account2.address, BigNumber.from(ONE_HUNDERD_VTHO)) + await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDRED_VTHO)) + await entryPoint.depositAmountTo(account2.address, BigNumber.from(ONE_HUNDRED_VTHO)) const approveOp = await fillAndSign({ sender: account2.address, diff --git a/test/simple-wallet.test.ts b/test/shard1/simple-wallet.test.ts similarity index 55% rename from test/simple-wallet.test.ts rename to test/shard1/simple-wallet.test.ts index 33470fb..770d785 100644 --- a/test/simple-wallet.test.ts +++ b/test/shard1/simple-wallet.test.ts @@ -1,65 +1,71 @@ import { expect } from 'chai' import { Wallet } from 'ethers' import { parseEther } from 'ethers/lib/utils' -import { ethers } from 'hardhat' +import { artifacts, ethers } from 'hardhat' import { - EntryPoint__factory, SimpleAccount, SimpleAccountFactory, SimpleAccountFactory__factory, SimpleAccount__factory, TestCounter, TestCounter__factory, - TestUtil, - TestUtil__factory -} from '../typechain' -import config from './config' + TestUtil +} from '../../typechain' +import config from '../utils/config' import { HashZero, ONE_ETH, - createAccount, + createAccountFromFactory, createAccountOwner, createAddress, getBalance, + getVeChainChainId, isDeployed -} from './testutils' -import { fillUserOpDefaults, getUserOpHash, packUserOp, signUserOp } from './UserOp' -import { UserOperation } from './UserOperation' -// const EntryPoint = artifacts.require('EntryPoint'); -// const SimpleAccountFactory = artifacts.require('SimpleAccountFactory'); -const SimpleAccountT = artifacts.require('SimpleAccount') +} from '../utils/testutils' +import { fillUserOpDefaults, getUserOpHash, packUserOp, signUserOp } from '../utils/UserOp' +import { UserOperation } from '../utils/UserOperation' -const ONE_HUNDERD_VTHO = '100000000000000000000' +const SimpleAccountT = artifacts.require('SimpleAccount') describe('SimpleAccount', function () { - let entryPoint: string + let simpleAccountFactory: SimpleAccountFactory let accounts: string[] let testUtil: TestUtil let accountOwner: Wallet const ethersSigner = ethers.provider.getSigner() before(async function () { - entryPoint = await EntryPoint__factory.connect(config.simpleAccountFactoryAddress, ethers.provider.getSigner()).address + if (process.env.NETWORK !== null && process.env.NETWORK !== undefined && process.env.NETWORK !== '') { + simpleAccountFactory = SimpleAccountFactory__factory.connect(config.simpleAccountFactoryAddress, ethersSigner) + } else { + const entryPointFactory = await ethers.getContractFactory('EntryPoint') + const entryPoint = await entryPointFactory.deploy() + const accountFactoryFactory = await ethers.getContractFactory('SimpleAccountFactory') + simpleAccountFactory = await accountFactoryFactory.deploy(entryPoint.address) + await simpleAccountFactory.deployed() + } + accounts = await ethers.provider.listAccounts() // ignore in geth.. this is just a sanity test. should be refactored to use a single-account mode.. if (accounts.length < 2) this.skip() - testUtil = await TestUtil__factory.connect(config.testUtilAddress, ethersSigner) + const testUtilFactory = await ethers.getContractFactory('TestUtil') + testUtil = await testUtilFactory.deploy() accountOwner = createAccountOwner() }) it('owner should be able to call transfer', async () => { - const { proxy: account } = await createAccount(ethers.provider.getSigner(), accounts[0]) + const { account } = await createAccountFromFactory(simpleAccountFactory, ethers.provider.getSigner(), accounts[0]) await ethersSigner.sendTransaction({ from: accounts[0], to: account.address, value: parseEther('2') }) await account.execute(accounts[2], ONE_ETH, '0x') }) it('other account should not be able to call transfer', async () => { - const { proxy: account } = await createAccount(ethers.provider.getSigner(), accounts[0]) + const { account } = await createAccountFromFactory(simpleAccountFactory, ethers.provider.getSigner(), accounts[0]) await expect(account.connect(ethers.provider.getSigner(1)).execute(accounts[2], ONE_ETH, '0x')) .to.be.revertedWith('account: not Owner or EntryPoint') }) it('should pack in js the same as solidity', async () => { - const op = await fillUserOpDefaults({ sender: accounts[0] }) + const op = fillUserOpDefaults({ sender: accounts[0] }) const packed = packUserOp(op) const actual = await testUtil.packUserOp(op) expect(actual).to.equal(packed) @@ -69,7 +75,8 @@ describe('SimpleAccount', function () { let account: SimpleAccount let counter: TestCounter before(async () => { - ({ proxy: account } = await createAccount(ethersSigner, await ethersSigner.getAddress())) + const accountFromFactory = await createAccountFromFactory(simpleAccountFactory, ethersSigner, await ethersSigner.getAddress()) + account = accountFromFactory.account counter = await new TestCounter__factory(ethersSigner).deploy() }) @@ -87,7 +94,7 @@ describe('SimpleAccount', function () { // Fund SimpleAccount with 2 VET await ethersSigner.sendTransaction({ from: accounts[0], to: account.address, value: parseEther('2') }) - const rcpt = await account.execute(target, ONE_ETH, '0x00').then(async t => await t.wait()) + await account.execute(target, ONE_ETH, '0x00').then(async t => await t.wait()) const actualBalance = await ethers.provider.getBalance(target) expect(actualBalance.toString()).to.not.eql('0') }) @@ -105,60 +112,11 @@ describe('SimpleAccount', function () { let userOpHash: string let preBalance: number let expectedPay: number - let simpleAccountFactory: SimpleAccountFactory const actualGasPrice = 1e9 // for testing directly validateUserOp, we initialize the account with EOA as entryPoint. let entryPointEoa: string - // before(async () => { - // // entryPointEoa = accounts[2]; - // // const epAsSigner = await ethers.getSigner(entryPointEoa); - - // // cant use "SimpleAccountFactory", since it attempts to increment nonce first - // // const implementation = await new SimpleAccount__factory(ethersSigner).deploy(entryPointEoa) - // // const accountAdress = "0x8488987B02135e6264d7741DfD46AF14e756152C"; - // // const implementation = await SimpleAccount__factory.connect(accountAdress, epAsSigner); - // // const proxy = await new ERC1967Proxy__factory(ethersSigner).deploy(implementation.address, '0x') - // // account = SimpleAccount__factory.connect(proxy.address, epAsSigner) - - // const epAsSigner = await ethers.getSigner(config.entryPointAddress); - // ({ proxy: account } = await createAccount(ethersSigner, await ethersSigner.getAddress())) - - // const entrypoint = EntryPoint__factory.connect(config.entryPointAddress, ethers.provider.getSigner()); - // const accountAdress = account.address; - // const vtho = ERC20__factory.connect(config.VTHOAddress, ethers.provider.getSigner()); - // await vtho.approve(config.entryPointAddress, BigNumber.from(ONE_HUNDERD_VTHO)); - // await entrypoint.depositAmountTo(accountAdress, BigNumber.from(ONE_HUNDERD_VTHO)); - - // // console.log("Signer: ", await ethersSigner.getAddress()); - // console.log("Account's EntryPoint: ", await account.entryPoint()); - // // console.log("AccountOwner: ", accountOwner.address); - // // console.log("entryPointEoa: ", entryPointEoa); - - // await ethersSigner.sendTransaction({ from: accounts[0], to: account.address, value: parseEther('0.2') }) - - // const callGasLimit = 200000 - // const verificationGasLimit = 100000 - // const maxFeePerGas = 3e9 - // const chainId = await ethers.provider.getNetwork().then(net => net.chainId) - - // userOp = signUserOp(fillUserOpDefaults({ - // sender: account.address, - // callGasLimit, - // verificationGasLimit, - // maxFeePerGas - // }), accountOwner, config.entryPointAddress, chainId) - - // userOpHash = await getUserOpHash(userOp, config.entryPointAddress, chainId) - - // expectedPay = actualGasPrice * (callGasLimit + verificationGasLimit) - - // preBalance = await getBalance(account.address) - // const ret = await account.validateUserOp(userOp, userOpHash, expectedPay, { gasPrice: actualGasPrice}) - // await ret.wait() - // }) - before(async () => { entryPointEoa = accounts[2] const epAsSigner = await ethers.getSigner(entryPointEoa) @@ -170,7 +128,7 @@ describe('SimpleAccount', function () { const callGasLimit = 200000 const verificationGasLimit = 100000 const maxFeePerGas = 3e9 - const chainId = await ethers.provider.send('eth_chainId', []) // await ethers.provider.getNetwork().then(net => net.chainId) + const chainId = await getVeChainChainId() userOp = signUserOp(fillUserOpDefaults({ sender: account.address, @@ -179,7 +137,7 @@ describe('SimpleAccount', function () { maxFeePerGas }), accountOwner, entryPointEoa, chainId) - userOpHash = await getUserOpHash(userOp, entryPointEoa, chainId) + userOpHash = getUserOpHash(userOp, entryPointEoa, chainId) expectedPay = actualGasPrice * (callGasLimit + verificationGasLimit) @@ -205,7 +163,7 @@ describe('SimpleAccount', function () { it('sanity: check deployer', async () => { const ownerAddr = createAddress() // const deployer = await new SimpleAccountFactory__factory(ethersSigner).deploy(entryPoint) - const deployer = await SimpleAccountFactory__factory.connect(config.simpleAccountFactoryAddress, ethers.provider.getSigner()) + const deployer = SimpleAccountFactory__factory.connect(simpleAccountFactory.address, ethers.provider.getSigner()) const target = await deployer.callStatic.createAccount(ownerAddr, 1234) // expect(await isDeployed(target)).to.eq(false) await deployer.createAccount(ownerAddr, 1234) diff --git a/test/shard2/entrypoint.test.ts b/test/shard2/entrypoint.test.ts new file mode 100644 index 0000000..c965309 --- /dev/null +++ b/test/shard2/entrypoint.test.ts @@ -0,0 +1,560 @@ +import { expect } from 'chai' +import crypto from 'crypto' +import { BigNumber, Wallet } from 'ethers/lib/ethers' +import { hexConcat } from 'ethers/lib/utils' +import { artifacts, ethers } from 'hardhat' +import { + ERC20__factory, + EntryPoint, + EntryPoint__factory, + SimpleAccount, + SimpleAccountFactory, + SimpleAccountFactory__factory, + TestCounter__factory +} from '../../typechain' +import { + fillAndSign, + getUserOpHash +} from '../utils/UserOp' +import '../utils/aa.init' +import config from '../utils/config' +import { + AddressZero, + checkForBannedOps, + createAccountFromFactory, + createAccountOwner, + createAddress, + createRandomAccountFromFactory, + createRandomAccountOwner, + createRandomAddress, + fund, + fundVtho, + getAccountAddress, + getAccountInitCode, + getBalance, + getVeChainChainId, + simulationResultCatch +} from '../utils/testutils' + +const TestCounterT = artifacts.require('TestCounter') +const ONE_HUNDRED_VTHO = '100000000000000000000' +const ONE_THOUSAND_VTHO = '1000000000000000000000' + +function getRandomInt (min: number, max: number): number { + min = Math.ceil(min) + max = Math.floor(max) + const range = max - min + if (range <= 0) { + throw new Error('Max must be greater than min') + } + const randomBytes = crypto.randomBytes(4) + const randomValue = randomBytes.readUInt32BE(0) + return min + (randomValue % range) +} + +describe('EntryPoint', function () { + let simpleAccountFactory: SimpleAccountFactory + let entryPointAddress: string + + let accountOwner: Wallet + const ethersSigner = ethers.provider.getSigner() + let account: SimpleAccount + + before(async function () { + let entryPoint + if (process.env.NETWORK !== null && process.env.NETWORK !== undefined && process.env.NETWORK !== '') { + entryPoint = EntryPoint__factory.connect(config.entryPointAddress, ethers.provider.getSigner()) + entryPointAddress = entryPoint.address + simpleAccountFactory = SimpleAccountFactory__factory.connect(config.simpleAccountFactoryAddress, ethersSigner) + } else { + const entryPointFactory = await ethers.getContractFactory('EntryPoint') + entryPoint = await entryPointFactory.deploy() + entryPointAddress = entryPoint.address + + const accountFactoryFactory = await ethers.getContractFactory('SimpleAccountFactory') + simpleAccountFactory = await accountFactoryFactory.deploy(entryPoint.address) + await simpleAccountFactory.deployed() + } + + accountOwner = createAccountOwner() + + const createdAccount = await createAccountFromFactory(simpleAccountFactory, ethersSigner, await accountOwner.getAddress()) + account = createdAccount.account + await fund(account) + + // sanity: validate helper functions + const sampleOp = await fillAndSign({ + sender: account.address + }, accountOwner, entryPoint) + + const chainId = await getVeChainChainId() + expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(sampleOp)) + }) + + describe('Stake Management', () => { + describe('with deposit', () => { + let address2: string + let entryPoint: EntryPoint + const signer2 = ethers.provider.getSigner(2) + const vtho = ERC20__factory.connect(config.VTHOAddress, signer2) + const DEPOSIT = 1000 + + before(() => { + entryPoint = EntryPoint__factory.connect(entryPointAddress, signer2) + }) + + beforeEach(async function () { + // Approve transfer from signer to Entrypoint and deposit + await vtho.approve(entryPointAddress, DEPOSIT) + address2 = await signer2.getAddress() + }) + + afterEach(async function () { + // Reset state by withdrawing deposit + const balance = await entryPoint.balanceOf(address2) + await entryPoint.withdrawTo(address2, balance) + }) + + it('should transfer full approved amount into EntryPoint', async () => { + // Transfer approved amount to entrpoint + await entryPoint.depositTo(address2) + + // Check amount has been deposited + expect(await entryPoint.balanceOf(address2)).to.eql(DEPOSIT) + expect(await entryPoint.getDepositInfo(await signer2.getAddress())).to.eql({ + deposit: DEPOSIT, + staked: false, + stake: 0, + unstakeDelaySec: 0, + withdrawTime: 0 + }) + + // Check updated allowance + expect(await vtho.allowance(address2, entryPointAddress)).to.eql(0) + }) + + it('should transfer partial approved amount into EntryPoint', async () => { + // Transfer partial amount to entrpoint + const ONE = 1 + await entryPoint.depositAmountTo(address2, DEPOSIT - ONE) + + // Check amount has been deposited + expect(await entryPoint.balanceOf(address2)).to.eql(DEPOSIT - ONE) + expect(await entryPoint.getDepositInfo(await signer2.getAddress())).to.eql({ + deposit: DEPOSIT - ONE, + staked: false, + stake: 0, + unstakeDelaySec: 0, + withdrawTime: 0 + }) + + // Check updated allowance + expect(await vtho.allowance(address2, entryPointAddress)).to.eql(ONE) + }) + + it('should fail to transfer more than approved amount into EntryPoint', async () => { + // Check transferring more than the amount fails + await expect(entryPoint.depositAmountTo(address2, DEPOSIT + 1)).to.revertedWith('amount to deposit > allowance') + }) + + it('should fail to withdraw larger amount than available', async () => { + const addrTo = createAddress() + await expect(entryPoint.withdrawTo(addrTo, DEPOSIT)).to.revertedWith('Withdraw amount too large') + }) + + it('should withdraw amount', async () => { + const addrTo = createRandomAddress() + await entryPoint.depositTo(address2) + const depositBefore = await entryPoint.balanceOf(address2) + await entryPoint.withdrawTo(addrTo, 1) + expect(await entryPoint.balanceOf(address2)).to.equal(depositBefore.sub(1)) + expect(await vtho.balanceOf(addrTo)).to.equal(1) + }) + }) + + describe('without stake', () => { + let entryPoint: EntryPoint + const signer3 = ethers.provider.getSigner(3) + const vtho = ERC20__factory.connect(config.VTHOAddress, signer3) + before(() => { + entryPoint = EntryPoint__factory.connect(entryPointAddress, signer3) + }) + it('should fail to stake without approved amount', async () => { + await vtho.approve(entryPointAddress, 0) + await expect(entryPoint.addStake(0)).to.revertedWith('amount to stake == 0') + }) + it('should fail to stake more than approved amount', async () => { + await vtho.approve(entryPointAddress, 100) + await expect(entryPoint.addStakeAmount(0, 101)).to.revertedWith('amount to stake > allowance') + }) + it('should fail to stake without delay', async () => { + await vtho.approve(entryPointAddress, 100) + await expect(entryPoint.addStake(0)).to.revertedWith('must specify unstake delay') + await expect(entryPoint.addStakeAmount(0, 100)).to.revertedWith('must specify unstake delay') + }) + it('should fail to unlock', async () => { + await expect(entryPoint.unlockStake()).to.revertedWith('not staked') + }) + }) + + describe('with stake', () => { + let entryPoint: EntryPoint + let address4: string + + const UNSTAKE_DELAY_SEC = 60 + const signer4 = ethers.provider.getSigner(4) + const vtho = ERC20__factory.connect(config.VTHOAddress, signer4) + + before(async () => { + entryPoint = EntryPoint__factory.connect(entryPointAddress, signer4) + address4 = await signer4.getAddress() + await vtho.approve(entryPointAddress, 2000) + await entryPoint.addStake(UNSTAKE_DELAY_SEC) + }) + it('should report "staked" state', async () => { + const { stake, staked, unstakeDelaySec, withdrawTime } = await entryPoint.getDepositInfo(address4) + expect({ staked, unstakeDelaySec, withdrawTime }).to.eql({ + staked: true, + unstakeDelaySec: UNSTAKE_DELAY_SEC, + withdrawTime: 0 + }) + expect(stake.toNumber()).to.greaterThanOrEqual(2000) + }) + + it('should succeed to stake again', async () => { + const { stake } = await entryPoint.getDepositInfo(address4) + await vtho.approve(entryPointAddress, 1000) + await entryPoint.addStake(UNSTAKE_DELAY_SEC) + const { stake: stakeAfter } = await entryPoint.getDepositInfo(address4) + expect(stakeAfter).to.eq(stake.add(1000)) + }) + it('should fail to withdraw before unlock', async () => { + await expect(entryPoint.withdrawStake(AddressZero)).to.revertedWith('must call unlockStake() first') + }) + describe('with unlocked stake', () => { + let withdrawTime1: number + before(async () => { + const transaction = await entryPoint.unlockStake() + withdrawTime1 = await ethers.provider.getBlock(transaction.blockHash!).then(block => block.timestamp) + UNSTAKE_DELAY_SEC + }) + it('should report as "not staked"', async () => { + expect(await entryPoint.getDepositInfo(address4).then(info => info.staked)).to.eq(false) + }) + it('should report unstake state', async () => { + const { stake, staked, unstakeDelaySec, withdrawTime } = await entryPoint.getDepositInfo(address4) + expect({ staked, unstakeDelaySec, withdrawTime }).to.eql({ + staked: false, + unstakeDelaySec: UNSTAKE_DELAY_SEC, + withdrawTime: withdrawTime1 + }) + + expect(stake.toNumber()).to.greaterThanOrEqual(3000) + }) + it('should fail to withdraw before unlock timeout', async () => { + await expect(entryPoint.withdrawStake(AddressZero)).to.revertedWith('Stake withdrawal is not due') + }) + it('should fail to unlock again', async () => { + await expect(entryPoint.unlockStake()).to.revertedWith('already unstaking') + }) + describe('after unstake delay', () => { + before(async () => { + await new Promise(resolve => setTimeout(resolve, 60000)) + }) + it('should fail to unlock again', async () => { + await expect(entryPoint.unlockStake()).to.revertedWith('already unstaking') + }) + it('adding stake should reset "unlockStake"', async () => { + await vtho.approve(entryPointAddress, 1000) + await entryPoint.addStake(UNSTAKE_DELAY_SEC) + const { stake, staked, unstakeDelaySec, withdrawTime } = await entryPoint.getDepositInfo(address4) + expect({ staked, unstakeDelaySec, withdrawTime }).to.eql({ + staked: true, + unstakeDelaySec: UNSTAKE_DELAY_SEC, + withdrawTime: 0 + }) + + expect(stake.toNumber()).to.greaterThanOrEqual(4000) + }) + it('should succeed to withdraw', async () => { + await entryPoint.unlockStake().catch(e => console.log(e.message)) + + // wait 2 minutes + await new Promise((resolve) => setTimeout(resolve, 120000)) + + const { stake } = await entryPoint.getDepositInfo(address4) + const addr1 = createRandomAddress() + await entryPoint.withdrawStake(addr1) + expect(await vtho.balanceOf(addr1)).to.eq(stake) + const { stake: stakeAfter, withdrawTime, unstakeDelaySec } = await entryPoint.getDepositInfo(address4) + + expect({ stakeAfter, withdrawTime, unstakeDelaySec }).to.eql({ + stakeAfter: BigNumber.from(0), + unstakeDelaySec: 0, + withdrawTime: 0 + }) + }) + }) + }) + }) + describe('with deposit', () => { + let account: SimpleAccount + const signer5 = ethers.provider.getSigner(5) + const vtho = ERC20__factory.connect(config.VTHOAddress, signer5) + before(async () => { + const accountFromFactory = await createAccountFromFactory(simpleAccountFactory, signer5, await signer5.getAddress()) + account = accountFromFactory.account + await vtho.transfer(account.address, BigNumber.from(ONE_THOUSAND_VTHO)) + await account.deposit(ONE_THOUSAND_VTHO, { gasLimit: 1e7 }).then(async tx => tx.wait()) + expect(await getBalance(account.address)).to.equal(0) + expect(await account.getDeposit()).to.eql(ONE_THOUSAND_VTHO) + }) + it('should be able to withdraw', async () => { + const depositBefore = await account.getDeposit() + await account.withdrawDepositTo(account.address, ONE_HUNDRED_VTHO).then(async tx => tx.wait()) + expect(await account.getDeposit()).to.equal(depositBefore.sub(ONE_HUNDRED_VTHO)) + }) + }) + }) + + describe('#simulateValidation', () => { + const accountOwner1 = createAccountOwner() + let entryPoint: EntryPoint + let account1: SimpleAccount + const signer2 = ethers.provider.getSigner(2) + const vtho = ERC20__factory.connect(config.VTHOAddress, signer2) + + before(async () => { + entryPoint = EntryPoint__factory.connect(entryPointAddress, signer2) + const accountFromFactory = await createAccountFromFactory(simpleAccountFactory, ethersSigner, await accountOwner1.getAddress()) + account1 = accountFromFactory.account + + await fund(account1) + + // Fund account + await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDRED_VTHO)) + await entryPoint.depositAmountTo(account.address, BigNumber.from(ONE_HUNDRED_VTHO)) + + // Fund account1 + await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDRED_VTHO)) + await entryPoint.depositAmountTo(account1.address, BigNumber.from(ONE_HUNDRED_VTHO)) + }) + + it('should fail if validateUserOp fails', async () => { + // using wrong nonce + const op = await fillAndSign({ sender: account.address, nonce: 1234 }, accountOwner, entryPoint) + await expect(entryPoint.callStatic.simulateValidation(op)).to + .revertedWith('AA25 invalid account nonce') + }) + + it('should report signature failure without revert', async () => { + // (this is actually a feature of the wallet, not the entrypoint) + // using wrong owner for account1 + // (zero gas price so it doesn't fail on prefund) + const op = await fillAndSign({ sender: account1.address, maxFeePerGas: 0 }, accountOwner, entryPoint) + const { returnInfo } = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) + expect(returnInfo.sigFailed).to.be.true + }) + + it('should revert if wallet not deployed (and no initcode)', async () => { + const op = await fillAndSign({ + sender: createAddress(), + nonce: 0, + verificationGasLimit: 1000 + }, accountOwner, entryPoint) + await expect(entryPoint.callStatic.simulateValidation(op)).to + .revertedWith('AA20 account not deployed') + }) + + it('should revert on oog if not enough verificationGas', async () => { + const op = await fillAndSign({ sender: account.address, verificationGasLimit: 1000 }, accountOwner, entryPoint) + await expect(entryPoint.callStatic.simulateValidation(op)).to + .revertedWith('AA23 reverted (or OOG)') + }) + + it('should succeed if validateUserOp succeeds', async () => { + const op = await fillAndSign({ sender: account1.address }, accountOwner1, entryPoint) + await fund(account1) + await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) + }) + + it('should return empty context if no paymaster', async () => { + const op = await fillAndSign({ sender: account1.address, maxFeePerGas: 0 }, accountOwner1, entryPoint) + const { returnInfo } = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) + expect(returnInfo.paymasterContext).to.eql('0x') + }) + + it('should return stake of sender', async () => { + const stakeValue = BigNumber.from(456) + const unstakeDelay = 3 + + const accountOwner = createRandomAccountOwner() + const accountFromFactory = await createRandomAccountFromFactory(simpleAccountFactory, ethersSigner, accountOwner.address) + const account2 = accountFromFactory.account + + await fund(account2) + await fundVtho(account2.address, entryPoint) + await vtho.transfer(account2.address, ONE_HUNDRED_VTHO) + + // allow vtho from account to entrypoint + const callData0 = account.interface.encodeFunctionData('execute', [vtho.address, 0, vtho.interface.encodeFunctionData('approve', [entryPoint.address, stakeValue])]) + + const vthoOp = await fillAndSign({ + sender: account2.address, + callData: callData0, + callGasLimit: BigNumber.from(123456) + }, accountOwner, entryPoint) + + const beneficiary = createRandomAddress() + + // Aprove some VTHO to entrypoint + await entryPoint.handleOps([vthoOp], beneficiary, { gasLimit: 1e7 }) + + // Call execute on account via userOp instead of directly + const callData = account.interface.encodeFunctionData('execute', [entryPoint.address, 0, entryPoint.interface.encodeFunctionData('addStake', [unstakeDelay])]) + const opp = await fillAndSign({ + sender: account2.address, + callData, + callGasLimit: BigNumber.from(1234567), + verificationGasLimit: BigNumber.from(1234567) + }, accountOwner, entryPoint) + + // call entryPoint.addStake from account + await entryPoint.handleOps([opp], createRandomAddress(), { gasLimit: 1e7 }) + + // reverts, not from owner + // let ret = await account2.execute(entryPoint.address, stakeValue, entryPoint.interface.encodeFunctionData('addStake', [unstakeDelay]), {gasLimit: 1e7}) + const op = await fillAndSign({ sender: account2.address }, accountOwner, entryPoint) + const result = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) + expect(result.senderInfo).to.eql({ stake: stakeValue, unstakeDelaySec: unstakeDelay }) + }) + + it('should prevent overflows: fail if any numeric value is more than 120 bits', async () => { + const op = await fillAndSign({ + preVerificationGas: BigNumber.from(2).pow(130), + sender: account1.address + }, accountOwner1, entryPoint) + await expect( + entryPoint.callStatic.simulateValidation(op) + ).to.revertedWith('gas values overflow') + }) + + it('should fail creation for wrong sender', async () => { + const op1 = await fillAndSign({ + initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory), + sender: '0x'.padEnd(42, '1'), + verificationGasLimit: 3e6 + }, accountOwner1, entryPoint) + await expect(entryPoint.callStatic.simulateValidation(op1)) + .to.revertedWith('AA14 initCode must return sender') + }) + + it('should report failure on insufficient verificationGas (OOG) for creation', async () => { + const accountOwner1 = createRandomAccountOwner() + const initCode = getAccountInitCode(accountOwner1.address, simpleAccountFactory) + const sender = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) + const op0 = await fillAndSign({ + initCode, + sender, + verificationGasLimit: 5e5, + maxFeePerGas: 0 + }, accountOwner1, entryPoint) + // must succeed with enough verification gas. + await expect(entryPoint.callStatic.simulateValidation(op0, { gasLimit: 1e6 })) + .to.revertedWith('ValidationResult') + + const op1 = await fillAndSign({ + initCode, + sender, + verificationGasLimit: 1e5, + maxFeePerGas: 0 + }, accountOwner1, entryPoint) + await expect(entryPoint.callStatic.simulateValidation(op1, { gasLimit: 1e6 })) + .to.revertedWith('AA13 initCode failed or OOG') + }) + + it('should succeed for creating an account', async () => { + const accountOwner1 = createRandomAccountOwner() + const sender = await getAccountAddress(accountOwner1.address, simpleAccountFactory) + + // Fund sender + await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDRED_VTHO)) + await entryPoint.depositAmountTo(sender, BigNumber.from(ONE_HUNDRED_VTHO)) + + const op1 = await fillAndSign({ + sender, + initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory) + }, accountOwner1, entryPoint) + await fund(op1.sender) + + await entryPoint.callStatic.simulateValidation(op1).catch(simulationResultCatch) + }) + + it('should not call initCode from entrypoint', async () => { + // a possible attack: call an account's execFromEntryPoint through initCode. This might lead to stolen funds. + const { account } = await createAccountFromFactory(simpleAccountFactory, ethersSigner, await accountOwner.getAddress()) + const sender = createAddress() + const op1 = await fillAndSign({ + initCode: hexConcat([ + account.address, + account.interface.encodeFunctionData('execute', [sender, 0, '0x']) + ]), + sender + }, accountOwner, entryPoint) + const error = await entryPoint.callStatic.simulateValidation(op1).catch(e => e) + expect(error.message).to.match(/initCode failed or OOG/, error) + }) + + it('should not use banned ops during simulateValidation', async () => { + const salt = getRandomInt(1, 2147483648) + const op1 = await fillAndSign({ + initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory, salt), + sender: await getAccountAddress(accountOwner1.address, simpleAccountFactory, salt) + }, accountOwner1, entryPoint) + + await fund(op1.sender) + await fundVtho(op1.sender, entryPoint) + + const transaction = await entryPoint.simulateValidation(op1, { gasLimit: 1e7 }) + transaction.wait().catch(e => e.errorArgs) + const blockHash = transaction.blockHash ?? (await ethers.provider.getBlock('latest')).hash + await checkForBannedOps(blockHash, transaction.hash, false) + }) + }) + + describe('#simulateHandleOp', () => { + let entryPoint: EntryPoint + const signer2 = ethers.provider.getSigner(2) + + before(() => { + entryPoint = EntryPoint__factory.connect(entryPointAddress, signer2) + }) + + it('should simulate execution', async () => { + const accountOwner1 = createAccountOwner() + const { account } = await createAccountFromFactory(simpleAccountFactory, ethersSigner, await accountOwner.getAddress()) + await fund(account) + const testCounterContract = await TestCounterT.new() + const counter = TestCounter__factory.connect(testCounterContract.address, ethersSigner) + + const count = counter.interface.encodeFunctionData('count') + const callData = account.interface.encodeFunctionData('execute', [counter.address, 0, count]) + // deliberately broken signature.. simulate should work with it too. + const userOp = await fillAndSign({ + sender: account.address, + callData + }, accountOwner1, entryPoint) + + const ret = await entryPoint.callStatic.simulateHandleOp(userOp, + counter.address, + counter.interface.encodeFunctionData('counters', [account.address]) + ).catch(e => e.errorArgs) + + const [countResult] = counter.interface.decodeFunctionResult('counters', ret.targetResult) + expect(countResult).to.eql(1) + expect(ret.targetSuccess).to.be.true + + // actual counter is zero + expect(await counter.counters(account.address)).to.eql(0) + }) + }) +}) diff --git a/test/entrypoint.test.ts b/test/shard3/entrypoint.test.ts similarity index 64% rename from test/entrypoint.test.ts rename to test/shard3/entrypoint.test.ts index 4124474..91d3964 100644 --- a/test/entrypoint.test.ts +++ b/test/shard3/entrypoint.test.ts @@ -1,13 +1,16 @@ import { expect } from 'chai' +import crypto from 'crypto' import { toChecksumAddress } from 'ethereumjs-util' import { BigNumber, PopulatedTransaction, Wallet } from 'ethers/lib/ethers' -import { BytesLike, defaultAbiCoder, hexConcat, hexZeroPad, parseEther } from 'ethers/lib/utils' +import { BytesLike, arrayify, defaultAbiCoder, hexConcat, hexZeroPad, parseEther } from 'ethers/lib/utils' import { artifacts, ethers } from 'hardhat' import { ERC20__factory, + EntryPoint, EntryPoint__factory, SimpleAccount, SimpleAccountFactory, + SimpleAccountFactory__factory, TestAggregatedAccount, TestAggregatedAccountFactory__factory, TestAggregatedAccount__factory, @@ -22,26 +25,25 @@ import { TestSignatureAggregator, TestSignatureAggregator__factory, TestWarmColdAccount__factory -} from '../typechain' +} from '../../typechain' import { DefaultsForUserOp, fillAndSign, getUserOpHash -} from './UserOp' -import { UserOperation } from './UserOperation' -import { debugTransaction } from './_debugTx' -import './aa.init' -import config from './config' +} from '../utils/UserOp' +import { UserOperation } from '../utils/UserOperation' +import '../utils/aa.init' +import config from '../utils/config' +import { debugTracers } from '../utils/debugTx' import { AddressZero, HashZero, ONE_ETH, TWO_ETH, - checkForBannedOps, - createAccount, + createAccountFromFactory, createAccountOwner, createAddress, - createRandomAccount, + createRandomAccountFromFactory, createRandomAccountOwner, createRandomAddress, decodeRevertReason, @@ -51,11 +53,11 @@ import { getAccountInitCode, getAggregatedAccountInitCode, getBalance, + getVeChainChainId, simulationResultCatch, simulationResultWithAggregationCatch, tostr -} from './testutils' -import crypto from 'crypto' +} from '../utils/testutils' const TestCounterT = artifacts.require('TestCounter') const TestSignatureAggregatorT = artifacts.require('TestSignatureAggregator') @@ -66,7 +68,7 @@ const TestExpirePaymasterT = artifacts.require('TestExpirePaymaster') const TestRevertAccountT = artifacts.require('TestRevertAccount') const TestAggregatedAccountFactoryT = artifacts.require('TestAggregatedAccountFactory') const TestWarmColdAccountT = artifacts.require('TestWarmColdAccount') -const ONE_HUNDERD_VTHO = '100000000000000000000' +const ONE_HUNDRED_VTHO = '100000000000000000000' const ONE_THOUSAND_VTHO = '1000000000000000000000' function getRandomInt (min: number, max: number): number { @@ -83,6 +85,7 @@ function getRandomInt (min: number, max: number): number { describe('EntryPoint', function () { let simpleAccountFactory: SimpleAccountFactory + let entryPointAddress: string let accountOwner: Wallet const ethersSigner = ethers.provider.getSigner() @@ -92,14 +95,25 @@ describe('EntryPoint', function () { const paymasterStake = ethers.utils.parseEther('2') before(async function () { - const chainId = await ethers.provider.send('eth_chainId', []) // await ethers.provider.getNetwork().then(net => net.chainId); - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, ethers.provider.getSigner()) - - accountOwner = createAccountOwner(); - ({ - proxy: account, - accountFactory: simpleAccountFactory - } = await createAccount(ethersSigner, await accountOwner.getAddress())) + let entryPoint + if (process.env.NETWORK !== null && process.env.NETWORK !== undefined && process.env.NETWORK !== '') { + entryPoint = EntryPoint__factory.connect(config.entryPointAddress, ethers.provider.getSigner()) + entryPointAddress = entryPoint.address + simpleAccountFactory = SimpleAccountFactory__factory.connect(config.simpleAccountFactoryAddress, ethersSigner) + } else { + const entryPointFactory = await ethers.getContractFactory('EntryPoint') + entryPoint = await entryPointFactory.deploy() + entryPointAddress = entryPoint.address + + const accountFactoryFactory = await ethers.getContractFactory('SimpleAccountFactory') + simpleAccountFactory = await accountFactoryFactory.deploy(entryPoint.address) + await simpleAccountFactory.deployed() + } + + accountOwner = createAccountOwner() + + const createdAccount = await createAccountFromFactory(simpleAccountFactory, ethersSigner, await accountOwner.getAddress()) + account = createdAccount.account await fund(account) // sanity: validate helper functions @@ -107,464 +121,14 @@ describe('EntryPoint', function () { sender: account.address }, accountOwner, entryPoint) + const chainId = await getVeChainChainId() expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(sampleOp)) }) - describe('Stake Management', () => { - describe('with deposit', () => { - let address2: string - const signer2 = ethers.provider.getSigner(2) - const vtho = ERC20__factory.connect(config.VTHOAddress, signer2) - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer2) - const DEPOSIT = 1000 - - beforeEach(async function () { - // Approve transfer from signer to Entrypoint and deposit - await vtho.approve(config.entryPointAddress, DEPOSIT) - address2 = await signer2.getAddress() - }) - - afterEach(async function () { - // Reset state by withdrawing deposit - const balance = await entryPoint.balanceOf(address2) - await entryPoint.withdrawTo(address2, balance) - }) - - it('should transfer full approved amount into EntryPoint', async () => { - // Transfer approved amount to entrpoint - await entryPoint.depositTo(address2) - - // Check amount has been deposited - expect(await entryPoint.balanceOf(address2)).to.eql(DEPOSIT) - expect(await entryPoint.getDepositInfo(await signer2.getAddress())).to.eql({ - deposit: DEPOSIT, - staked: false, - stake: 0, - unstakeDelaySec: 0, - withdrawTime: 0 - }) - - // Check updated allowance - expect(await vtho.allowance(address2, config.entryPointAddress)).to.eql(0) - }) - - it('should transfer partial approved amount into EntryPoint', async () => { - // Transfer partial amount to entrpoint - const ONE = 1 - await entryPoint.depositAmountTo(address2, DEPOSIT - ONE) - - // Check amount has been deposited - expect(await entryPoint.balanceOf(address2)).to.eql(DEPOSIT - ONE) - expect(await entryPoint.getDepositInfo(await signer2.getAddress())).to.eql({ - deposit: DEPOSIT - ONE, - staked: false, - stake: 0, - unstakeDelaySec: 0, - withdrawTime: 0 - }) - - // Check updated allowance - expect(await vtho.allowance(address2, config.entryPointAddress)).to.eql(ONE) - }) - - it('should fail to transfer more than approved amount into EntryPoint', async () => { - // Check transferring more than the amount fails - expect(entryPoint.depositAmountTo(address2, DEPOSIT + 1)).to.revertedWith('amount to deposit > allowance') - }) - - it('should fail to withdraw larger amount than available', async () => { - const addrTo = createAddress() - await expect(entryPoint.withdrawTo(addrTo, DEPOSIT)).to.revertedWith('Withdraw amount too large') - }) - - it('should withdraw amount', async () => { - const addrTo = createRandomAddress() - await entryPoint.depositTo(address2) - const depositBefore = await entryPoint.balanceOf(address2) - await entryPoint.withdrawTo(addrTo, 1) - expect(await entryPoint.balanceOf(address2)).to.equal(depositBefore.sub(1)) - expect(await vtho.balanceOf(addrTo)).to.equal(1) - }) - }) - - describe('without stake', () => { - const signer3 = ethers.provider.getSigner(3) - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer3) - const vtho = ERC20__factory.connect(config.VTHOAddress, signer3) - it('should fail to stake without approved amount', async () => { - await vtho.approve(config.entryPointAddress, 0) - await expect(entryPoint.addStake(0)).to.revertedWith('amount to stake == 0') - }) - it('should fail to stake more than approved amount', async () => { - await vtho.approve(config.entryPointAddress, 100) - await expect(entryPoint.addStakeAmount(0, 101)).to.revertedWith('amount to stake > allowance') - }) - it('should fail to stake without delay', async () => { - await vtho.approve(config.entryPointAddress, 100) - await expect(entryPoint.addStake(0)).to.revertedWith('must specify unstake delay') - await expect(entryPoint.addStakeAmount(0, 100)).to.revertedWith('must specify unstake delay') - }) - it('should fail to unlock', async () => { - await expect(entryPoint.unlockStake()).to.revertedWith('not staked') - }) - }) - - describe('with stake', () => { - const UNSTAKE_DELAY_SEC = 60 - let address4: string - const signer4 = ethers.provider.getSigner(4) - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer4) - const vtho = ERC20__factory.connect(config.VTHOAddress, signer4) - - before(async () => { - address4 = await signer4.getAddress() - await vtho.approve(config.entryPointAddress, 2000) - await entryPoint.addStake(UNSTAKE_DELAY_SEC) - }) - it('should report "staked" state', async () => { - const { stake, staked, unstakeDelaySec, withdrawTime } = await entryPoint.getDepositInfo(address4) - expect({ staked, unstakeDelaySec, withdrawTime }).to.eql({ - staked: true, - unstakeDelaySec: UNSTAKE_DELAY_SEC, - withdrawTime: 0 - }) - expect(stake.toNumber()).to.greaterThanOrEqual(2000) - }) - - it('should succeed to stake again', async () => { - const { stake } = await entryPoint.getDepositInfo(address4) - await vtho.approve(config.entryPointAddress, 1000) - await entryPoint.addStake(UNSTAKE_DELAY_SEC) - const { stake: stakeAfter } = await entryPoint.getDepositInfo(address4) - expect(stakeAfter).to.eq(stake.add(1000)) - }) - it('should fail to withdraw before unlock', async () => { - await expect(entryPoint.withdrawStake(AddressZero)).to.revertedWith('must call unlockStake() first') - }) - describe('with unlocked stake', () => { - let withdrawTime1: number - before(async () => { - const transaction = await entryPoint.unlockStake() - withdrawTime1 = await ethers.provider.getBlock(transaction.blockHash!).then(block => block.timestamp) + UNSTAKE_DELAY_SEC - }) - it('should report as "not staked"', async () => { - expect(await entryPoint.getDepositInfo(address4).then(info => info.staked)).to.eq(false) - }) - it('should report unstake state', async () => { - const { stake, staked, unstakeDelaySec, withdrawTime } = await entryPoint.getDepositInfo(address4) - expect({ staked, unstakeDelaySec, withdrawTime }).to.eql({ - staked: false, - unstakeDelaySec: UNSTAKE_DELAY_SEC, - withdrawTime: withdrawTime1 - }) - - expect(stake.toNumber()).to.greaterThanOrEqual(3000) - }) - it('should fail to withdraw before unlock timeout', async () => { - await expect(entryPoint.withdrawStake(AddressZero)).to.revertedWith('Stake withdrawal is not due') - }) - it('should fail to unlock again', async () => { - await expect(entryPoint.unlockStake()).to.revertedWith('already unstaking') - }) - describe('after unstake delay', () => { - before(async () => { - // wait 61 seconds - await new Promise(r => setTimeout(r, 60000)) - }) - it('should fail to unlock again', async () => { - await expect(entryPoint.unlockStake()).to.revertedWith('already unstaking') - }) - it('adding stake should reset "unlockStake"', async () => { - await vtho.approve(config.entryPointAddress, 1000) - await entryPoint.addStake(UNSTAKE_DELAY_SEC) - const { stake, staked, unstakeDelaySec, withdrawTime } = await entryPoint.getDepositInfo(address4) - expect({ staked, unstakeDelaySec, withdrawTime }).to.eql({ - staked: true, - unstakeDelaySec: UNSTAKE_DELAY_SEC, - withdrawTime: 0 - }) - - expect(stake.toNumber()).to.greaterThanOrEqual(4000) - }) - it('should succeed to withdraw', async () => { - await entryPoint.unlockStake().catch(e => console.log(e.message)) - - // wait 65 seconds - await new Promise(r => setTimeout(r, 120000)) - - const { stake } = await entryPoint.getDepositInfo(address4) - const addr1 = createRandomAddress() - await entryPoint.withdrawStake(addr1) - expect(await vtho.balanceOf(addr1)).to.eq(stake) - const { stake: stakeAfter, withdrawTime, unstakeDelaySec } = await entryPoint.getDepositInfo(address4) - - expect({ stakeAfter, withdrawTime, unstakeDelaySec }).to.eql({ - stakeAfter: BigNumber.from(0), - unstakeDelaySec: 0, - withdrawTime: 0 - }) - }) - }) - }) - }) - describe('with deposit', () => { - const signer5 = ethers.provider.getSigner(5) - const vtho = ERC20__factory.connect(config.VTHOAddress, signer5) - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer5) - let account: SimpleAccount - let address5: string - before(async () => { - address5 = await signer5.getAddress() - await account.addDeposit(ONE_ETH) - expect(await getBalance(account.address)).to.equal(0) - expect(await account.getDeposit()).to.eql(ONE_ETH) - }) - }) - }) - - describe('#simulateValidation', () => { - const accountOwner1 = createAccountOwner() - let account1: SimpleAccount - let address2: string - const signer2 = ethers.provider.getSigner(2) - const vtho = ERC20__factory.connect(config.VTHOAddress, signer2) - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer2) - const DEPOSIT = 1000 - - before(async () => { - ({ proxy: account1 } = await createAccount(ethersSigner, await accountOwner1.getAddress())) - - await fund(account1) - - // Fund account - await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(account.address, BigNumber.from(ONE_HUNDERD_VTHO)) - - // Fund account1 - await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(account1.address, BigNumber.from(ONE_HUNDERD_VTHO)) - }) - - it('should fail if validateUserOp fails', async () => { - // using wrong nonce - const op = await fillAndSign({ sender: account.address, nonce: 1234 }, accountOwner, entryPoint) - await expect(entryPoint.callStatic.simulateValidation(op)).to - .revertedWith('AA25 invalid account nonce') - }) - - it('should report signature failure without revert', async () => { - // (this is actually a feature of the wallet, not the entrypoint) - // using wrong owner for account1 - // (zero gas price so it doesn't fail on prefund) - const op = await fillAndSign({ sender: account1.address, maxFeePerGas: 0 }, accountOwner, entryPoint) - const { returnInfo } = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) - expect(returnInfo.sigFailed).to.be.true - }) - - it('should revert if wallet not deployed (and no initcode)', async () => { - const op = await fillAndSign({ - sender: createAddress(), - nonce: 0, - verificationGasLimit: 1000 - }, accountOwner, entryPoint) - await expect(entryPoint.callStatic.simulateValidation(op)).to - .revertedWith('AA20 account not deployed') - }) - - it('should revert on oog if not enough verificationGas', async () => { - const op = await fillAndSign({ sender: account.address, verificationGasLimit: 1000 }, accountOwner, entryPoint) - await expect(entryPoint.callStatic.simulateValidation(op)).to - .revertedWith('AA23 reverted (or OOG)') - }) - - it('should succeed if validateUserOp succeeds', async () => { - const op = await fillAndSign({ sender: account1.address }, accountOwner1, entryPoint) - await fund(account1) - await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) - }) - - it('should return empty context if no paymaster', async () => { - const op = await fillAndSign({ sender: account1.address, maxFeePerGas: 0 }, accountOwner1, entryPoint) - const { returnInfo } = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) - expect(returnInfo.paymasterContext).to.eql('0x') - }) - - it('should return stake of sender', async () => { - const stakeValue = BigNumber.from(456) - const unstakeDelay = 3 - - const accountOwner = createRandomAccountOwner() - const { proxy: account2 } = await createRandomAccount(ethersSigner, accountOwner.address) - - await fund(account2) - await fundVtho(account2.address) - await vtho.transfer(account2.address, ONE_HUNDERD_VTHO) - - // allow vtho from account to entrypoint - const callData0 = account.interface.encodeFunctionData('execute', [vtho.address, 0, vtho.interface.encodeFunctionData('approve', [entryPoint.address, stakeValue])]) - - const vthoOp = await fillAndSign({ - sender: account2.address, - callData: callData0, - callGasLimit: BigNumber.from(123456) - }, accountOwner, entryPoint) - - const beneficiary = createRandomAddress() - - // Aprove some VTHO to entrypoint - await entryPoint.handleOps([vthoOp], beneficiary, { gasLimit: 1e7 }) - - // Call execute on account via userOp instead of directly - const callData = account.interface.encodeFunctionData('execute', [entryPoint.address, 0, entryPoint.interface.encodeFunctionData('addStake', [unstakeDelay])]) - const opp = await fillAndSign({ - sender: account2.address, - callData, - callGasLimit: BigNumber.from(1234567), - verificationGasLimit: BigNumber.from(1234567) - }, accountOwner, entryPoint) - - // call entryPoint.addStake from account - const ret = await entryPoint.handleOps([opp], createRandomAddress(), { gasLimit: 1e7 }) - - // reverts, not from owner - // let ret = await account2.execute(entryPoint.address, stakeValue, entryPoint.interface.encodeFunctionData('addStake', [unstakeDelay]), {gasLimit: 1e7}) - const op = await fillAndSign({ sender: account2.address }, accountOwner, entryPoint) - const result = await entryPoint.callStatic.simulateValidation(op).catch(simulationResultCatch) - expect(result.senderInfo).to.eql({ stake: stakeValue, unstakeDelaySec: unstakeDelay }) - }) - - it('should prevent overflows: fail if any numeric value is more than 120 bits', async () => { - const op = await fillAndSign({ - preVerificationGas: BigNumber.from(2).pow(130), - sender: account1.address - }, accountOwner1, entryPoint) - await expect( - entryPoint.callStatic.simulateValidation(op) - ).to.revertedWith('gas values overflow') - }) - - it('should fail creation for wrong sender', async () => { - const op1 = await fillAndSign({ - initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory), - sender: '0x'.padEnd(42, '1'), - verificationGasLimit: 3e6 - }, accountOwner1, entryPoint) - await expect(entryPoint.callStatic.simulateValidation(op1)) - .to.revertedWith('AA14 initCode must return sender') - }) - - it('should report failure on insufficient verificationGas (OOG) for creation', async () => { - const accountOwner1 = createRandomAccountOwner() - const initCode = getAccountInitCode(accountOwner1.address, simpleAccountFactory) - const sender = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) - const op0 = await fillAndSign({ - initCode, - sender, - verificationGasLimit: 5e5, - maxFeePerGas: 0 - }, accountOwner1, entryPoint) - // must succeed with enough verification gas. - await expect(entryPoint.callStatic.simulateValidation(op0, { gasLimit: 1e6 })) - .to.revertedWith('ValidationResult') - - const op1 = await fillAndSign({ - initCode, - sender, - verificationGasLimit: 1e5, - maxFeePerGas: 0 - }, accountOwner1, entryPoint) - await expect(entryPoint.callStatic.simulateValidation(op1, { gasLimit: 1e6 })) - .to.revertedWith('AA13 initCode failed or OOG') - }) - - it('should succeed for creating an account', async () => { - const accountOwner1 = createRandomAccountOwner() - const sender = await getAccountAddress(accountOwner1.address, simpleAccountFactory) - - // Fund sender - await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(sender, BigNumber.from(ONE_HUNDERD_VTHO)) - - const op1 = await fillAndSign({ - sender, - initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory) - }, accountOwner1, entryPoint) - await fund(op1.sender) - - await entryPoint.callStatic.simulateValidation(op1).catch(simulationResultCatch) - }) - - it('should not call initCode from entrypoint', async () => { - // a possible attack: call an account's execFromEntryPoint through initCode. This might lead to stolen funds. - const { proxy: account } = await createAccount(ethersSigner, await accountOwner.getAddress()) - const sender = createAddress() - const op1 = await fillAndSign({ - initCode: hexConcat([ - account.address, - account.interface.encodeFunctionData('execute', [sender, 0, '0x']) - ]), - sender - }, accountOwner, entryPoint) - const error = await entryPoint.callStatic.simulateValidation(op1).catch(e => e) - expect(error.message).to.match(/initCode failed or OOG/, error) - }) - - it('should not use banned ops during simulateValidation', async () => { - const salt = getRandomInt(1, 2147483648) - const op1 = await fillAndSign({ - initCode: getAccountInitCode(accountOwner1.address, simpleAccountFactory, salt), - sender: await getAccountAddress(accountOwner1.address, simpleAccountFactory, salt) - }, accountOwner1, entryPoint) - - await fund(op1.sender) - await fundVtho(op1.sender) - - await entryPoint.simulateValidation(op1, { gasLimit: 1e7 }).catch(e => e) - const block = await ethers.provider.getBlock('latest') - const hash = block.transactions[0] - await checkForBannedOps(hash, false) - }) - }) - - describe('#simulateHandleOp', () => { - let address2: string - const signer2 = ethers.provider.getSigner(2) - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer2) - - it('should simulate execution', async () => { - const accountOwner1 = createAccountOwner() - const { proxy: account } = await createAccount(ethersSigner, await accountOwner.getAddress()) - await fund(account) - const testCounterContract = await TestCounterT.new() - const counter = await TestCounter__factory.connect(testCounterContract.address, ethersSigner) - - const count = counter.interface.encodeFunctionData('count') - const callData = account.interface.encodeFunctionData('execute', [counter.address, 0, count]) - // deliberately broken signature.. simulate should work with it too. - const userOp = await fillAndSign({ - sender: account.address, - callData - }, accountOwner1, entryPoint) - - const ret = await entryPoint.callStatic.simulateHandleOp(userOp, - counter.address, - counter.interface.encodeFunctionData('counters', [account.address]) - ).catch(e => e.errorArgs) - - const [countResult] = counter.interface.decodeFunctionResult('counters', ret.targetResult) - expect(countResult).to.eql(1) - expect(ret.targetSuccess).to.be.true - - // actual counter is zero - expect(await counter.counters(account.address)).to.eql(0) - }) - }) - describe('flickering account validation', () => { + let entryPoint: EntryPoint const signer2 = ethers.provider.getSigner(2) - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer2) - - // NaN + // NaN: In VeChain there is no basefee // it('should prevent leakage of basefee', async () => { // const maliciousAccountContract = await MaliciousAccountT.new(entryPoint.address, { value: parseEther('1') }) // const maliciousAccount = MaliciousAccount__factory.connect(maliciousAccountContract.address, ethersSigner); @@ -611,6 +175,10 @@ describe('EntryPoint', function () { // } // }) + before(() => { + entryPoint = EntryPoint__factory.connect(entryPointAddress, signer2) + }) + it('should limit revert reason length before emitting it', async () => { const vtho = ERC20__factory.connect(config.VTHOAddress, signer2) const revertLength = 1e5 @@ -623,30 +191,28 @@ describe('EntryPoint', function () { sender: testRevertAccount.address, callGasLimit: 1e5, maxFeePerGas: 1, + maxPriorityFeePerGas: 1, nonce: await entryPoint.getNonce(testRevertAccount.address, 0), verificationGasLimit: 1e6, callData: badData.data! } - await vtho.approve(testRevertAccount.address, ONE_HUNDERD_VTHO) + await vtho.approve(testRevertAccount.address, ONE_HUNDRED_VTHO) const beneficiaryAddress = createRandomAddress() await expect(entryPoint.callStatic.simulateValidation(badOp, { gasLimit: 1e7 })).to.revertedWith('ValidationResult') - // const tx = await entryPoint.handleOps([badOp], beneficiaryAddress, {gasLimit: 1e7}) // { gasLimit: 3e5 }) - // const receipt = await tx.wait() - // const userOperationRevertReasonEvent = receipt.events?.find(event => event.event === 'UserOperationRevertReason') - // expect(userOperationRevertReasonEvent?.event).to.equal('UserOperationRevertReason') - // const revertReason = Buffer.from(arrayify(userOperationRevertReasonEvent?.args?.revertReason)) - // expect(revertReason.length).to.equal(REVERT_REASON_MAX_LEN) + const tx = await entryPoint.handleOps([badOp], beneficiaryAddress, { gasLimit: 1e7 }) // { gasLimit: 3e5 }) + const receipt = await tx.wait() + const userOperationRevertReasonEvent = receipt.events?.find(event => event.event === 'UserOperationRevertReason') + expect(userOperationRevertReasonEvent?.event).to.equal('UserOperationRevertReason') + const revertReason = Buffer.from(arrayify(userOperationRevertReasonEvent?.args?.revertReason)) + expect(revertReason.length).to.equal(REVERT_REASON_MAX_LEN) }) describe('warm/cold storage detection in simulation vs execution', () => { const TOUCH_GET_AGGREGATOR = 1 const TOUCH_PAYMASTER = 2 - const vtho = ERC20__factory.connect(config.VTHOAddress, signer2) it('should prevent detection through getAggregator()', async () => { - // const testWarmColdAccountContract = await new TestWarmColdAccount__factory(ethersSigner).deploy(entryPoint.address, - // { value: parseEther('1') }) const testWarmColdAccountContract = await TestWarmColdAccountT.new(entryPoint.address, { value: parseEther('1') }) const testWarmColdAccount = TestWarmColdAccount__factory.connect(testWarmColdAccountContract.address, ethersSigner) const badOp: UserOperation = { @@ -671,12 +237,12 @@ describe('EntryPoint', function () { const testWarmColdAccountContract = await TestWarmColdAccountT.new(entryPoint.address, { value: parseEther('1') }) const testWarmColdAccount = TestWarmColdAccount__factory.connect(testWarmColdAccountContract.address, ethersSigner) - await fundVtho(testWarmColdAccountContract.address) + await fundVtho(testWarmColdAccountContract.address, entryPoint) const paymasterContract = await TestPaymasterAcceptAllT.new(entryPoint.address) const paymaster = TestPaymasterAcceptAll__factory.connect(paymasterContract.address, ethersSigner) - await fundVtho(paymaster.address) + await fundVtho(paymaster.address, entryPoint) await paymaster.deposit(ONE_ETH, { gasLimit: 1e7 }) const badOp: UserOperation = { @@ -702,7 +268,7 @@ describe('EntryPoint', function () { describe('2d nonces', () => { const signer2 = ethers.provider.getSigner(2) - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer2) + let entryPoint: EntryPoint const beneficiaryAddress = createRandomAddress() let sender: string @@ -710,10 +276,11 @@ describe('EntryPoint', function () { const keyShifted = BigNumber.from(key).shl(64) before(async () => { - const { proxy } = await createRandomAccount(ethersSigner, accountOwner.address) - sender = proxy.address + entryPoint = EntryPoint__factory.connect(entryPointAddress, signer2) + const { account } = await createRandomAccountFromFactory(simpleAccountFactory, ethersSigner, accountOwner.address) + sender = account.address await fund(sender) - await fundVtho(sender) + await fundVtho(sender, entryPoint) }) it('should fail nonce with new key and seq!=0', async () => { @@ -726,13 +293,13 @@ describe('EntryPoint', function () { describe('with key=1, seq=1', () => { before(async () => { - await fundVtho(sender) + await fundVtho(sender, entryPoint) const op = await fillAndSign({ sender, nonce: keyShifted }, accountOwner, entryPoint) - const ret = await entryPoint.handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }) + await entryPoint.handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }) }) it('should get next nonce value by getNonce', async () => { @@ -748,7 +315,7 @@ describe('EntryPoint', function () { }) it('should allow manual nonce increment', async () => { - await fundVtho(sender) + await fundVtho(sender, entryPoint) // must be called from account itself const incNonceKey = 5 @@ -774,15 +341,20 @@ describe('EntryPoint', function () { }) describe('without paymaster (account pays in eth)', () => { + let entryPoint: EntryPoint const signer2 = ethers.provider.getSigner(2) const vtho = ERC20__factory.connect(config.VTHOAddress, signer2) - const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer2) + + before(() => { + entryPoint = EntryPoint__factory.connect(entryPointAddress, signer2) + }) + describe('#handleOps', () => { let counter: TestCounter let accountExecFromEntryPoint: PopulatedTransaction before(async () => { const testCounterContract = await TestCounterT.new() - counter = await TestCounter__factory.connect(testCounterContract.address, ethersSigner) + counter = TestCounter__factory.connect(testCounterContract.address, ethersSigner) const count = await counter.populateTransaction.count() accountExecFromEntryPoint = await account.populateTransaction.execute(counter.address, 0, count.data!) }) @@ -791,9 +363,11 @@ describe('EntryPoint', function () { // wallet-reported signature failure should revert in handleOps const wrongOwner = createAccountOwner() + await fundVtho(account.address, entryPoint) + // Fund wrong owner - await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(wrongOwner.address, BigNumber.from(ONE_HUNDERD_VTHO)) + await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDRED_VTHO)) + await entryPoint.depositAmountTo(wrongOwner.address, BigNumber.from(ONE_HUNDRED_VTHO)) const op = await fillAndSign({ sender: account.address @@ -811,6 +385,8 @@ describe('EntryPoint', function () { }, accountOwner, entryPoint) const beneficiaryAddress = createAddress() + await fundVtho(account.address, entryPoint) + const countBefore = await counter.counters(account.address) // for estimateGas, must specify maxFeePerGas, otherwise our gas check fails console.log(' == est gas=', await entryPoint.estimateGas.handleOps([op], beneficiaryAddress, { maxFeePerGas: 1e9 }).then(tostr)) @@ -838,7 +414,7 @@ describe('EntryPoint', function () { const count = await counter.populateTransaction.gasWaster(iterations, '') const accountExec = await account.populateTransaction.execute(counter.address, 0, count.data!) - await fundVtho(account.address) + await fundVtho(account.address, entryPoint) const op = await fillAndSign({ sender: account.address, @@ -862,7 +438,6 @@ describe('EntryPoint', function () { }).then(async t => await t.wait()) console.log('rcpt.gasUsed=', rcpt.gasUsed.toString(), rcpt.transactionHash) - // await calcGasUsage(rcpt, entryPoint, beneficiaryAddress) // check that the state of the counter contract is updated // this ensures that the `callGasLimit` is high enough @@ -911,7 +486,7 @@ describe('EntryPoint', function () { }, accountOwner, entryPoint) const beneficiaryAddress = createAddress() - await fundVtho(op.sender) + await fundVtho(op.sender, entryPoint) // (gasLimit, to prevent estimateGas to fail on missing maxFeePerGas, see above..) const rcpt = await entryPoint.handleOps([op], beneficiaryAddress, { @@ -919,7 +494,7 @@ describe('EntryPoint', function () { gasLimit: 1e7 }).then(async t => await t.wait()) - const ops = await debugTransaction(rcpt.transactionHash).then(tx => tx.structLogs.map(op => op.op)) + const ops = await debugTracers(rcpt.blockHash, rcpt.transactionHash).then(tx => tx.structLogs.map(op => op.op)) expect(ops).to.include('GAS') expect(ops).to.not.include('BASEFEE') }) @@ -939,14 +514,14 @@ describe('EntryPoint', function () { callGasLimit: 1e6 }, accountOwner, entryPoint) - var beneficiaryAddress = createRandomAddress() + let beneficiaryAddress = createRandomAddress() - const ret = await entryPoint.handleOps([depositVTHOOp], beneficiaryAddress, { + await entryPoint.handleOps([depositVTHOOp], beneficiaryAddress, { maxFeePerGas: 1e9, gasLimit: 1e7 }).then(async t => await t.wait()) - var beneficiaryAddress = createRandomAddress() + beneficiaryAddress = createRandomAddress() const op = await fillAndSign({ sender: account.address, @@ -977,8 +552,6 @@ describe('EntryPoint', function () { expect(balAfter).to.equal(balBefore, 'should pay from stake, not balance') const depositUsed = depositBefore.sub(depositAfter) expect(await vtho.balanceOf(beneficiaryAddress)).to.equal(depositUsed) - - // await calcGasUsage(rcpt, entryPoint, beneficiaryAddress) }) it('should pay for reverted tx', async () => { @@ -990,7 +563,7 @@ describe('EntryPoint', function () { }, accountOwner, entryPoint) const beneficiaryAddress = createAddress() - const rcpt = await entryPoint.handleOps([op], beneficiaryAddress, { + await entryPoint.handleOps([op], beneficiaryAddress, { maxFeePerGas: 1e9, gasLimit: 1e7 }).then(async t => await t.wait()) @@ -1016,7 +589,6 @@ describe('EntryPoint', function () { expect(countAfter.toNumber()).to.equal(countBefore.toNumber() + 1) console.log('rcpt.gasUsed=', rcpt.gasUsed.toString(), rcpt.transactionHash) - // await calcGasUsage(rcpt, entryPoint, beneficiaryAddress) }) it('should fail to call recursively into handleOps', async () => { @@ -1094,10 +666,10 @@ describe('EntryPoint', function () { const preAddr = await getAccountAddress(accountOwner.address, simpleAccountFactory, salt) await fund(preAddr) // send VET - await vtho.transfer(preAddr, BigNumber.from(ONE_HUNDERD_VTHO)) // send VTHO + await vtho.transfer(preAddr, BigNumber.from(ONE_HUNDRED_VTHO)) // send VTHO // Fund preAddr through EntryPoint - await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(preAddr, BigNumber.from(ONE_HUNDERD_VTHO)) + await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDRED_VTHO)) + await entryPoint.depositAmountTo(preAddr, BigNumber.from(ONE_HUNDRED_VTHO)) createOp = await fillAndSign({ initCode: getAccountInitCode(accountOwner.address, simpleAccountFactory, salt), @@ -1106,50 +678,24 @@ describe('EntryPoint', function () { }, accountOwner, entryPoint) - await expect(await ethers.provider.getCode(preAddr).then(x => x.length)).to.equal(2, 'account exists before creation') + expect(await ethers.provider.getCode(preAddr).then(x => x.length)).to.equal(2, 'account exists before creation') const ret = await entryPoint.handleOps([createOp], beneficiaryAddress, { gasLimit: 1e7 }) - const rcpt = await ret.wait() const hash = await entryPoint.getUserOpHash(createOp) await expect(ret).to.emit(entryPoint, 'AccountDeployed') // eslint-disable-next-line @typescript-eslint/no-base-to-string .withArgs(hash, createOp.sender, toChecksumAddress(createOp.initCode.toString().slice(0, 42)), AddressZero) - - // await calcGasUsage(rcpt!, entryPoint, beneficiaryAddress) }) it('should reject if account already created', async function () { - const salt = 20 - const preAddr = await getAccountAddress(accountOwner.address, simpleAccountFactory, salt) + const preAddr = await getAccountAddress(accountOwner.address, simpleAccountFactory) - await fund(preAddr) // send VET - await vtho.transfer(preAddr, BigNumber.from(ONE_HUNDERD_VTHO)) // send VTHO - // Fund preAddr through EntryPoint - await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(preAddr, BigNumber.from(ONE_HUNDERD_VTHO)) - - createOp = await fillAndSign({ - initCode: getAccountInitCode(accountOwner.address, simpleAccountFactory, salt), - callGasLimit: 1e6, - verificationGasLimit: 2e6 - - }, accountOwner, entryPoint) - - // If account already exists don't deploy it - if (await ethers.provider.getCode(preAddr).then(x => x.length) !== 2) { - const ret = await entryPoint.handleOps([createOp], beneficiaryAddress, { - gasLimit: 1e7 - }) + if (await ethers.provider.getCode(preAddr).then(x => x.length) === 2) { + this.skip() } - createOp = await fillAndSign({ - initCode: getAccountInitCode(accountOwner.address, simpleAccountFactory, salt), - callGasLimit: 1e6, - verificationGasLimit: 2e6 - }, accountOwner, entryPoint) - - expect(entryPoint.callStatic.handleOps([createOp], beneficiaryAddress, { + await expect(entryPoint.callStatic.handleOps([createOp], beneficiaryAddress, { gasLimit: 1e7 })).to.revertedWith('sender already constructed') }) @@ -1174,21 +720,22 @@ describe('EntryPoint', function () { const accountOwner2 = createAccountOwner() let account2: SimpleAccount - before('before', async () => { + before(async () => { const testCounterContract = await TestCounterT.new() - counter = await TestCounter__factory.connect(testCounterContract.address, ethersSigner) + counter = TestCounter__factory.connect(testCounterContract.address, ethersSigner) const count = await counter.populateTransaction.count() accountExecCounterFromEntryPoint = await account.populateTransaction.execute(counter.address, 0, count.data!) const salt = getRandomInt(1, 2147483648) - account1 = await getAccountAddress(accountOwner1.address, simpleAccountFactory, salt); - ({ proxy: account2 } = await createRandomAccount(ethersSigner, await accountOwner2.getAddress())) + account1 = await getAccountAddress(accountOwner1.address, simpleAccountFactory, salt) + const accountFromFactory = await createRandomAccountFromFactory(simpleAccountFactory, ethersSigner, await accountOwner2.getAddress()) + account2 = accountFromFactory.account await fund(account1) - await fundVtho(account1) + await fundVtho(account1, entryPoint) await fund(account2.address) - await fundVtho(account2.address) + await fundVtho(account2.address, entryPoint) // execute and increment counter const op1 = await fillAndSign({ @@ -1208,24 +755,17 @@ describe('EntryPoint', function () { await entryPoint.callStatic.simulateValidation(op2, { gasPrice: 1e9 }).catch(simulationResultCatch) await fund(op1.sender) - await fundVtho(op1.sender) + await fundVtho(op1.sender, entryPoint) await fund(account2.address) - await fundVtho(account2.address) + await fundVtho(account2.address, entryPoint) - const res = await entryPoint.handleOps([op1!, op2], beneficiaryAddress, { gasLimit: 1e7, gasPrice: 1e9 })// .catch((rethrow())).then(async r => r!.wait()) - // console.log(ret.events!.map(e=>({ev:e.event, ...objdump(e.args!)}))) + await entryPoint.handleOps([op1!, op2], beneficiaryAddress, { gasLimit: 1e7, gasPrice: 1e9 }) }) it('should execute', async () => { expect(await counter.counters(account1)).equal(1) expect(await counter.counters(account2.address)).equal(1) }) - it('should pay for tx', async () => { - // const cost1 = prebalance1.sub(await ethers.provider.getBalance(account1)) - // const cost2 = prebalance2.sub(await ethers.provider.getBalance(account2.address)) - // console.log('cost1=', cost1) - // console.log('cost2=', cost2) - }) }) describe('aggregation tests', () => { @@ -1247,9 +787,9 @@ describe('EntryPoint', function () { aggAccount2 = TestAggregatedAccount__factory.connect(aggAccount2Contract.address, ethersSigner) await ethersSigner.sendTransaction({ to: aggAccount.address, value: parseEther('0.1') }) - await fundVtho(aggAccount.address) + await fundVtho(aggAccount.address, entryPoint) await ethersSigner.sendTransaction({ to: aggAccount2.address, value: parseEther('0.1') }) - await fundVtho(aggAccount2.address) + await fundVtho(aggAccount2.address, entryPoint) }) it('should fail to execute aggregated account without an aggregator', async () => { const userOp = await fillAndSign({ @@ -1301,7 +841,6 @@ describe('EntryPoint', function () { }, accountOwner, entryPoint) const wrongSig = hexZeroPad('0x123456', 32) - const aggAddress: string = aggregator.address await expect( entryPoint.callStatic.handleAggregatedOps([{ userOps: [userOp], @@ -1315,7 +854,7 @@ describe('EntryPoint', function () { const aggAccount3 = await TestAggregatedAccountT.new(entryPoint.address, aggregator3.address) await ethersSigner.sendTransaction({ to: aggAccount3.address, value: parseEther('0.1') }) - await fundVtho(aggAccount3.address) + await fundVtho(aggAccount3.address, entryPoint) const userOp1 = await fillAndSign({ sender: aggAccount.address @@ -1352,7 +891,7 @@ describe('EntryPoint', function () { signature: '0x' }] const rcpt = await entryPoint.handleAggregatedOps(aggInfos, beneficiaryAddress, { gasLimit: 3e6 }).then(async ret => ret.wait()) - const events = rcpt.events?.map((ev: Event) => { + const events = rcpt.events?.map((ev: any) => { if (ev.event === 'UserOperationEvent') { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return `userOp(${ev.args?.sender})` @@ -1367,6 +906,7 @@ describe('EntryPoint', function () { `agg(${aggregator.address})`, `userOp(${userOp1.sender})`, `userOp(${userOp2.sender})`, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `agg(${aggregator3.address})`, `userOp(${userOp_agg3.sender})`, `agg(${AddressZero})`, @@ -1398,7 +938,7 @@ describe('EntryPoint', function () { const factory = TestAggregatedAccountFactory__factory.connect(factoryContract.address, ethersSigner) initCode = await getAggregatedAccountInitCode(entryPoint.address, factory) addr = await entryPoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender) - await fundVtho(addr) + await fundVtho(addr, entryPoint) await ethersSigner.sendTransaction({ to: addr, value: parseEther('0.1') }) userOp = await fillAndSign({ initCode @@ -1406,7 +946,7 @@ describe('EntryPoint', function () { }) it('simulateValidation should return aggregator and its stake', async () => { await vtho.approve(aggregator.address, TWO_ETH) - const tx = await aggregator.addStake(entryPoint.address, 3, TWO_ETH, { gasLimit: 1e7 }) + await aggregator.addStake(entryPoint.address, 3, TWO_ETH, { gasLimit: 1e7 }) const { aggregatorInfo } = await entryPoint.callStatic.simulateValidation(userOp).catch(simulationResultWithAggregationCatch) expect(aggregatorInfo.aggregator).to.equal(aggregator.address) expect(aggregatorInfo.stakeInfo.stake).to.equal(TWO_ETH) @@ -1428,7 +968,6 @@ describe('EntryPoint', function () { describe('with paymaster (account with no eth)', () => { let paymaster: TestPaymasterAcceptAll let counter: TestCounter - let paymasterAddress: string let accountExecFromEntryPoint: PopulatedTransaction const account2Owner = createAccountOwner() @@ -1436,9 +975,8 @@ describe('EntryPoint', function () { // paymaster = await new TestPaymasterAcceptAll__factory(ethersSigner).deploy(entryPoint.address) const paymasterContract = await TestPaymasterAcceptAllT.new(entryPoint.address) paymaster = TestPaymasterAcceptAll__factory.connect(paymasterContract.address, ethersSigner) - paymasterAddress = paymasterContract.address // Approve VTHO to paymaster before adding stake - await vtho.approve(paymasterContract.address, ONE_HUNDERD_VTHO) + await vtho.approve(paymasterContract.address, ONE_HUNDRED_VTHO) await paymaster.addStake(globalUnstakeDelaySec, paymasterStake, { gasLimit: 1e7 }) const counterContract = await TestCounterT.new() counter = TestCounter__factory.connect(counterContract.address, ethersSigner) @@ -1475,7 +1013,7 @@ describe('EntryPoint', function () { const paymasterContract = await TestPaymasterAcceptAllT.new(entryPoint.address) const paymaster = TestPaymasterAcceptAll__factory.connect(paymasterContract.address, ethersSigner) - await fundVtho(paymaster.address) + await fundVtho(paymaster.address, entryPoint) await paymaster.deposit(ONE_ETH, { gasLimit: 1e7 }) const balanceBefore = await entryPoint.balanceOf(paymaster.address) @@ -1488,7 +1026,7 @@ describe('EntryPoint', function () { }, account2Owner, entryPoint) const beneficiaryAddress = createRandomAddress() - const rcpt = await entryPoint.handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }).then(async t => t.wait()) + await entryPoint.handleOps([op], beneficiaryAddress, { gasLimit: 1e7 }).then(async t => t.wait()) // const { actualGasCost } = await calcGasUsage(rcpt, entryPoint, beneficiaryAddress) const balanceAfter = await entryPoint.balanceOf(paymaster.address) @@ -1496,7 +1034,7 @@ describe('EntryPoint', function () { expect(paymasterPaid.toNumber()).to.greaterThan(0) }) it('simulateValidation should return paymaster stake and delay', async () => { - // await fundVtho(paymasterAddress); + // await fundVtho(paymasterAddress, entryPoint); const paymasterContract = await TestPaymasterAcceptAllT.new(entryPoint.address) const paymaster = TestPaymasterAcceptAll__factory.connect(paymasterContract.address, ethersSigner) @@ -1505,7 +1043,7 @@ describe('EntryPoint', function () { // Vtho uses the same signer as paymaster await vtho.approve(paymasterContract.address, ONE_THOUSAND_VTHO) await paymaster.addStake(2, paymasterStake, { gasLimit: 1e7 }) - await paymaster.deposit(ONE_HUNDERD_VTHO, { gasLimit: 1e7 }) + await paymaster.deposit(ONE_HUNDRED_VTHO, { gasLimit: 1e7 }) const anOwner = createRandomAccountOwner() const op = await fillAndSign({ @@ -1545,7 +1083,7 @@ describe('EntryPoint', function () { describe('validateUserOp time-range', function () { it('should accept non-expired owner', async () => { - await fundVtho(account.address) + await fundVtho(account.address, entryPoint) const userOp = await fillAndSign({ sender: account.address }, sessionOwner, entryPoint) @@ -1555,7 +1093,7 @@ describe('EntryPoint', function () { }) it('should not reject expired owner', async () => { - await fundVtho(account.address) + await fundVtho(account.address, entryPoint) const expiredOwner = createAccountOwner() await account.addTemporaryOwner(expiredOwner.address, 123, now - 60) const userOp = await fillAndSign({ @@ -1571,13 +1109,12 @@ describe('EntryPoint', function () { let paymaster: TestExpirePaymaster let now: number before('init account with session key', async function () { - // this.timeout(20000) - await new Promise(r => setTimeout(r, 20000)) + await new Promise((resolve) => setTimeout(resolve, 20000)) // Deploy Paymaster const paymasterContract = await TestExpirePaymasterT.new(entryPoint.address) paymaster = TestExpirePaymaster__factory.connect(paymasterContract.address, ethersSigner) // Approve VTHO to paymaster before adding stake - await fundVtho(paymasterContract.address, ONE_HUNDERD_VTHO) + await fundVtho(paymasterContract.address, entryPoint, ONE_HUNDRED_VTHO) await paymaster.addStake(1, paymasterStake, { gasLimit: 1e7 }) await paymaster.deposit(parseEther('0.1'), { gasLimit: 1e7 }) @@ -1586,7 +1123,7 @@ describe('EntryPoint', function () { it('should accept non-expired paymaster request', async () => { const timeRange = defaultAbiCoder.encode(['uint48', 'uint48'], [123, now + 60]) - await fundVtho(account.address) + await fundVtho(account.address, entryPoint) const userOp = await fillAndSign({ sender: account.address, paymasterAndData: hexConcat([paymaster.address, timeRange]) @@ -1655,7 +1192,7 @@ describe('EntryPoint', function () { const expiredOwner = createRandomAccountOwner() await account.addTemporaryOwner(expiredOwner.address, 1, 2) - await fundVtho(account.address) + await fundVtho(account.address, entryPoint) const userOp = await fillAndSign({ sender: account.address @@ -1666,7 +1203,7 @@ describe('EntryPoint', function () { // this test passed when running it individually but fails when its run alonside the other tests it('should revert on date owner', async () => { - await fundVtho(account.address) + await fundVtho(account.address, entryPoint) const futureOwner = createRandomAccountOwner() await account.addTemporaryOwner(futureOwner.address, now + 1000, now + 2000) diff --git a/test/test_custom.ts b/test/test_custom.ts deleted file mode 100644 index 2bcff3d..0000000 --- a/test/test_custom.ts +++ /dev/null @@ -1,35 +0,0 @@ -import './aa.init' -import { expect } from 'chai' -import { - ERC20__factory, - EntryPoint__factory, - SimpleAccount, - SimpleAccountFactory, - SimpleAccount__factory -} from '../typechain' -import { - fund, - createAccount, - createAccountOwner, - AddressZero, - createAddress -} from './testutils' -import { BigNumber, Wallet } from 'ethers/lib/ethers' -import { ethers } from 'hardhat' -import { - fillAndSign, - getUserOpHash -} from './UserOp' -import config from './config' - -describe('EntryPoint', function () { - it('should transfer full approved amount into EntryPoint', async () => { - const entrypoint = EntryPoint__factory.connect(config.entryPointAddress, ethers.provider.getSigner()) - const accountAdress = '0xd272ec7265f813048F61a3D97613936E6e9dcce7' - const vtho = ERC20__factory.connect(config.VTHOAddress, ethers.provider.getSigner()) - await vtho.approve(config.entryPointAddress, 7195485000000000) - await entrypoint.depositAmountTo(accountAdress, 7195485000000000) - const deposit = await entrypoint.getDepositInfo(accountAdress) - console.log(deposit) - }) -}) diff --git a/test/UserOp.ts b/test/utils/UserOp.ts similarity index 92% rename from test/UserOp.ts rename to test/utils/UserOp.ts index 5cd5550..a4bf1d7 100644 --- a/test/UserOp.ts +++ b/test/utils/UserOp.ts @@ -1,17 +1,17 @@ +import { ecsign, keccak256 as keccak256_buffer, toRpcSig } from 'ethereumjs-util' +import { BigNumber, Contract, Signer, Wallet } from 'ethers' import { arrayify, defaultAbiCoder, hexDataSlice, keccak256 } from 'ethers/lib/utils' -import { BigNumber, Contract, Signer, Wallet } from 'ethers' -import { AddressZero, callDataCost, rethrow } from './testutils' -import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util' +import { Create2Factory } from '../../src/Create2Factory' import { EntryPoint -} from '../typechain' +} from '../../typechain' +import { AddressZero, callDataCost, getVeChainChainId, rethrow } from './testutils' import { UserOperation } from './UserOperation' -import { Create2Factory } from '../src/Create2Factory' export function packUserOp (op: UserOperation, forSignature = true): string { if (forSignature) { @@ -60,7 +60,7 @@ export function packUserOp1 (op: UserOperation): string { ]) } -export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: number): string { +export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: BigNumber): string { const userOpHash = keccak256(packUserOp(op, true)) const enc = defaultAbiCoder.encode( ['bytes32', 'address', 'uint256'], @@ -82,7 +82,7 @@ export const DefaultsForUserOp: UserOperation = { signature: '0x' } -export function signUserOp (op: UserOperation, signer: Wallet, entryPoint: string, chainId: number): UserOperation { +export function signUserOp (op: UserOperation, signer: Wallet, entryPoint: string, chainId: BigNumber): UserOperation { const message = getUserOpHash(op, entryPoint, chainId) const msg1 = Buffer.concat([ Buffer.from('\x19Ethereum Signed Message:\n32', 'ascii'), @@ -174,8 +174,8 @@ export async function fillUserOp (op: Partial, entryPoint?: Entry } if (op1.maxFeePerGas == null) { if (provider == null) throw new Error('must have entryPoint to autofill maxFeePerGas') - const block = await provider.getBlock('latest') - op1.maxFeePerGas = op1.maxPriorityFeePerGas ?? DefaultsForUserOp.maxPriorityFeePerGas + // In VeChain there is no base gas fee + op1.maxFeePerGas = BigNumber.from(op1.maxPriorityFeePerGas ?? DefaultsForUserOp.maxPriorityFeePerGas) } // TODO: this is exactly what fillUserOp below should do - but it doesn't. // adding this manually @@ -192,10 +192,14 @@ export async function fillUserOp (op: Partial, entryPoint?: Entry } export async function fillAndSign (op: Partial, signer: Wallet | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { - const provider = entryPoint?.provider const op2 = await fillUserOp(op, entryPoint, getNonceFunction) - const chainId = await provider!.send('eth_chainId', []) // await provider!.getNetwork().then(net => net.chainId) + const chainId = await getVeChainChainId() + + if (signer instanceof Wallet) { + return signUserOp(op2, signer, entryPoint!.address, chainId) + } + const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId)) return { diff --git a/test/UserOperation.ts b/test/utils/UserOperation.ts similarity index 100% rename from test/UserOperation.ts rename to test/utils/UserOperation.ts diff --git a/test/aa.init.ts b/test/utils/aa.init.ts similarity index 100% rename from test/aa.init.ts rename to test/utils/aa.init.ts diff --git a/test/chaiHelper.ts b/test/utils/chaiHelper.ts similarity index 100% rename from test/chaiHelper.ts rename to test/utils/chaiHelper.ts diff --git a/test/config.ts b/test/utils/config.ts similarity index 79% rename from test/config.ts rename to test/utils/config.ts index a84e305..de083cc 100644 --- a/test/config.ts +++ b/test/utils/config.ts @@ -1,3 +1,5 @@ +// Some of these addresses do not actually belong to the contracts they are named after, they are meant to be replaced + const config = { VTHOAddress: '0x0000000000000000000000000000456E65726779', testUtilAddress: '0x06b35287803bE5D21dc52FC77E651912cCabdF89', diff --git a/test/utils/debugTx.ts b/test/utils/debugTx.ts new file mode 100644 index 0000000..191ad72 --- /dev/null +++ b/test/utils/debugTx.ts @@ -0,0 +1,33 @@ +import { VECHAIN_URL_SOLO } from '@vechain/hardhat-vechain' + +export interface DebugLog { + pc: number + op: string + gasCost: number + depth: number + stack: string[] + memory: string[] +} + +export interface DebugTransactionResult { + gas: number + failed: boolean + returnValue: string + structLogs: DebugLog[] +} + +export async function debugTracers (blockHash: string, txHash: string, clauseNumber?: number, url?: string): Promise { + const result = await fetch(`${url ?? VECHAIN_URL_SOLO}/debug/tracers`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: '', + target: `${blockHash}/${txHash}/${clauseNumber ?? 0}` + }) + }) + + return result.json() +} diff --git a/test/solidityTypes.ts b/test/utils/solidityTypes.ts similarity index 100% rename from test/solidityTypes.ts rename to test/utils/solidityTypes.ts diff --git a/test/testutils.ts b/test/utils/testutils.ts similarity index 80% rename from test/testutils.ts rename to test/utils/testutils.ts index f9ef4a8..4d9abcd 100644 --- a/test/testutils.ts +++ b/test/utils/testutils.ts @@ -1,50 +1,58 @@ -import config from './config' import { ERC20__factory, EntryPoint, - EntryPoint__factory, - SimpleAccountFactory, - SimpleAccountFactory__factory - , IERC20, IEntryPoint, SimpleAccount, + SimpleAccountFactory, SimpleAccount__factory, TestAggregatedAccountFactory -} from '../typechain' +} from '../../typechain' +import config from './config' -import { ethers } from 'hardhat' +import { BytesLike } from '@ethersproject/bytes' +import { expect } from 'chai' +import { randomInt } from 'crypto' +import { BigNumber, BigNumberish, Contract, ContractReceipt, Signer, Wallet } from 'ethers' import { arrayify, hexConcat, keccak256, parseEther } from 'ethers/lib/utils' -import { BigNumber, BigNumberish, Contract, ContractReceipt, Signer, Wallet } from 'ethers' -import { BytesLike } from '@ethersproject/bytes' -import { expect } from 'chai' -import { debugTransaction } from './_debugTx' +import { ethers } from 'hardhat' +import { debugTracers } from './debugTx' import { UserOperation } from './UserOperation' -import { randomInt } from 'crypto' -export async function createAccount ( +export async function createAccountFromFactory ( + accountFactory: SimpleAccountFactory, ethersSigner: Signer, - accountOwner: string + accountOwner: string, + salt = 0 ): Promise<{ - proxy: SimpleAccount + account: SimpleAccount accountFactory: SimpleAccountFactory }> { - const accountFactory = new SimpleAccountFactory__factory() - .attach(config.simpleAccountFactoryAddress) - .connect(ethersSigner) - await accountFactory.createAccount(accountOwner, 0) - const accountAddress = await accountFactory.getAddress(accountOwner, 0) - const proxy = SimpleAccount__factory.connect(accountAddress, ethersSigner) + await accountFactory.createAccount(accountOwner, salt) + const accountAddress = await accountFactory.getAddress(accountOwner, salt) + const account = SimpleAccount__factory.connect(accountAddress, ethersSigner) return { - accountFactory, - proxy + account, + accountFactory } } +export async function createRandomAccountFromFactory ( + accountFactory: SimpleAccountFactory, + ethersSigner: Signer, + accountOwner: string +): Promise<{ + account: SimpleAccount + accountFactory: SimpleAccountFactory + }> { + const salt = seed++ + return createAccountFromFactory(accountFactory, ethersSigner, accountOwner, salt) +} + export const AddressZero = ethers.constants.AddressZero export const HashZero = ethers.constants.HashZero export const ONE_ETH = parseEther('1') @@ -53,7 +61,6 @@ export const FIVE_ETH = parseEther('5') const signer2 = ethers.provider.getSigner(2) const vtho = ERC20__factory.connect(config.VTHOAddress, signer2) -const entryPoint = EntryPoint__factory.connect(config.entryPointAddress, signer2) export const tostr = (x: any): string => x != null ? x.toString() : 'null' @@ -117,7 +124,7 @@ export function callDataCost (data: string): number { .reduce((sum, x) => sum + x) } -export async function fundVtho (contractOrAddress: string | Contract, ONE_HUNDERD_VTHO = '100000000000000000000'): Promise { +export async function fundVtho (contractOrAddress: string | Contract, entryPoint: EntryPoint, vthoAmount = '100000000000000000000'): Promise { let address: string if (typeof contractOrAddress === 'string') { address = contractOrAddress @@ -125,15 +132,15 @@ export async function fundVtho (contractOrAddress: string | Contract, ONE_HUNDER address = contractOrAddress.address } - await vtho.transfer(address, BigNumber.from(ONE_HUNDERD_VTHO)) // send VTHO + await vtho.transfer(address, BigNumber.from(vthoAmount)) // send VTHO // Fund preAddr through EntryPoint - await vtho.approve(entryPoint.address, BigNumber.from(ONE_HUNDERD_VTHO)) - await entryPoint.depositAmountTo(address, BigNumber.from(ONE_HUNDERD_VTHO)) + await vtho.approve(entryPoint.address, BigNumber.from(vthoAmount)) + await entryPoint.depositAmountTo(address, BigNumber.from(vthoAmount)) } 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 actualGas = rcpt.gasUsed + const logs = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent()) const { actualGasCost, actualGasUsed } = logs[0].args console.log('\t== actual gasUsed (from tx receipt)=', actualGas.toString()) console.log('\t== calculated gasUsed (paid to beneficiary)=', actualGasUsed) @@ -230,29 +237,6 @@ export function decodeRevertReason (data: string, nullIfNoMatch = true): string return null } -let currentNode: string = '' - -// basic geth support -// - by default, has a single account. our code needs more. -export async function checkForGeth (): Promise { - // @ts-ignore - const provider = ethers.provider._hardhatProvider - - currentNode = await provider.request({ method: 'web3_clientVersion' }) - - console.log('node version:', currentNode) - // NOTE: must run geth with params: - // --http.api personal,eth,net,web3 - // --allow-insecure-unlock - if (currentNode.match(/geth/i) != null) { - for (let i = 0; i < 2; i++) { - const acc = await provider.request({ method: 'personal_newAccount', params: ['pass'] }).catch(rethrow) - await provider.request({ method: 'personal_unlockAccount', params: [acc, 'pass'] }).catch(rethrow) - await fund(acc, '10') - } - } -} - // remove "array" members, convert values to strings. // so Result obj like // { '0': "a", '1': 20, first: "a", second: 20 } @@ -267,13 +251,13 @@ export function objdump (obj: { [key: string]: any }): any { }), {}) } -export async function checkForBannedOps (txHash: string, checkPaymaster: boolean): Promise { - const tx = await debugTransaction(txHash) +export async function checkForBannedOps (blockHash: string, txHash: string, checkPaymaster: boolean): Promise { + const tx = await debugTracers(blockHash, txHash) const logs = tx.structLogs - const blockHash = logs.map((op, index) => ({ op: op.op, index })).filter(op => op.op === 'NUMBER') - expect(blockHash.length).to.equal(2, 'expected exactly 2 call to NUMBER (Just before and after validateUserOperation)') - const validateAccountOps = logs.slice(0, blockHash[0].index - 1) - const validatePaymasterOps = logs.slice(blockHash[0].index + 1) + const numberOps = logs.map((op, index) => ({ op: op.op, index })).filter(op => op.op === 'NUMBER') + expect(numberOps.length).to.equal(2, 'expected exactly 2 call to NUMBER (Just before and after validateUserOperation)') + const validateAccountOps = logs.slice(0, numberOps[0].index - 1) + const validatePaymasterOps = logs.slice(numberOps[0].index + 1) const ops = validateAccountOps.filter(log => log.depth > 1).map(log => log.op) const paymasterOps = validatePaymasterOps.filter(log => log.depth > 1).map(log => log.op) @@ -330,22 +314,9 @@ export function userOpsWithoutAgg (userOps: UserOperation[]): IEntryPoint.UserOp }] } -export async function createRandomAccount ( - ethersSigner: Signer, - accountOwner: string -): Promise<{ - proxy: SimpleAccount - accountFactory: SimpleAccountFactory - }> { - const accountFactory = new SimpleAccountFactory__factory() - .attach(config.simpleAccountFactoryAddress) - .connect(ethersSigner) - const salt = seed++ - await accountFactory.createAccount(accountOwner, salt) - const accountAddress = await accountFactory.getAddress(accountOwner, salt) - const proxy = SimpleAccount__factory.connect(accountAddress, ethersSigner) - return { - accountFactory, - proxy +export async function getVeChainChainId (): Promise { + if (process.env.NETWORK !== null && process.env.NETWORK !== undefined && process.env.NETWORK !== '') { + return ethers.provider.send('eth_chainId', []) } + return BigNumber.from('0x00000000c05a20fbca2bf6ae3affba6af4a74b800b585bf7a4988aba7aea69f6') }