diff --git a/contracts/samples/LegacyTokenPaymaster.sol b/contracts/samples/LegacyTokenPaymaster.sol deleted file mode 100644 index 1589696d..00000000 --- a/contracts/samples/LegacyTokenPaymaster.sol +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; - -/* solhint-disable reason-string */ - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "../core/BasePaymaster.sol"; -import "../core/UserOperationLib.sol"; -import "../core/Helpers.sol"; - -/** - * A sample paymaster that defines itself as a token to pay for gas. - * The paymaster IS the token to use, since a paymaster cannot use an external contract. - * Also, the exchange rate has to be fixed, since it can't reference an external Uniswap or other exchange contract. - * subclass should override "getTokenValueOfEth" to provide actual token exchange rate, settable by the owner. - * Known Limitation: this paymaster is exploitable when put into a batch with multiple ops (of different accounts): - * - while a single op can't exploit the paymaster (if postOp fails to withdraw the tokens, the user's op is reverted, - * and then we know we can withdraw the tokens), multiple ops with different senders (all using this paymaster) - * in a batch can withdraw funds from 2nd and further ops, forcing the paymaster itself to pay (from its deposit) - * - Possible workarounds are either use a more complex paymaster scheme (e.g. the DepositPaymaster) or - * to whitelist the account and the called method ids. - */ -contract LegacyTokenPaymaster is BasePaymaster, ERC20 { - using UserOperationLib for PackedUserOperation; - - //calculated cost of the postOp - uint256 constant public COST_OF_POST = 15000; - - address public immutable theFactory; - - constructor(address accountFactory, string memory _symbol, IEntryPoint _entryPoint) ERC20(_symbol, _symbol) BasePaymaster(_entryPoint) { - theFactory = accountFactory; - //make it non-empty - _mint(address(this), 1); - - //owner is allowed to withdraw tokens from the paymaster's balance - _approve(address(this), msg.sender, type(uint256).max); - } - - - /** - * helpers for owner, to mint and withdraw tokens. - * @param recipient - the address that will receive the minted tokens. - * @param amount - the amount it will receive. - */ - function mintTokens(address recipient, uint256 amount) external onlyOwner { - _mint(recipient, amount); - } - - /** - * transfer paymaster ownership. - * owner of this paymaster is allowed to withdraw funds (tokens transferred to this paymaster's balance) - * when changing owner, the old owner's withdrawal rights are revoked. - */ - function transferOwnership(address newOwner) public override virtual onlyOwner { - // remove allowance of current owner - _approve(address(this), owner(), 0); - super.transferOwnership(newOwner); - // new owner is allowed to withdraw tokens from the paymaster's balance - _approve(address(this), newOwner, type(uint256).max); - } - - //Note: this method assumes a fixed ratio of token-to-eth. subclass should override to supply oracle - // or a setter. - function getTokenValueOfEth(uint256 valueEth) internal view virtual returns (uint256 valueToken) { - return valueEth / 100; - } - - /** - * validate the request: - * if this is a constructor call, make sure it is a known account. - * verify the sender has enough tokens. - * (since the paymaster is also the token, there is no notion of "approval") - */ - function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) - internal view override returns (bytes memory context, uint256 validationData) { - uint256 tokenPrefund = getTokenValueOfEth(requiredPreFund); - - uint256 postOpGasLimit = userOp.unpackPostOpGasLimit(); - require( postOpGasLimit > COST_OF_POST, "TokenPaymaster: gas too low for postOp"); - - if (userOp.initCode.length != 0) { - _validateConstructor(userOp); - require(balanceOf(userOp.sender) >= tokenPrefund, "TokenPaymaster: no balance (pre-create)"); - } else { - - require(balanceOf(userOp.sender) >= tokenPrefund, "TokenPaymaster: no balance"); - } - - return (abi.encode(userOp.sender), SIG_VALIDATION_SUCCESS); - } - - // when constructing an account, validate constructor code and parameters - // we trust our factory (and that it doesn't have any other public methods) - function _validateConstructor(PackedUserOperation calldata userOp) internal virtual view { - address factory = address(bytes20(userOp.initCode[0 : 20])); - require(factory == theFactory, "TokenPaymaster: wrong account factory"); - } - - /** - * actual charge of user. - * this method will be called just after the user's TX with mode==OpSucceeded|OpReverted (account pays in both cases) - * BUT: if the user changed its balance in a way that will cause postOp to revert, then it gets called again, after reverting - * the user's TX , back to the state it was before the transaction started (before the validatePaymasterUserOp), - * and the transaction should succeed there. - */ - function _postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas) internal override { - //we don't really care about the mode, we just pay the gas with the user's tokens. - (mode); - address sender = abi.decode(context, (address)); - uint256 charge = getTokenValueOfEth(actualGasCost + COST_OF_POST * actualUserOpFeePerGas); - //actualGasCost is known to be no larger than the above requiredPreFund, so the transfer should succeed. - _transfer(sender, address(this), charge); - } -} diff --git a/contracts/samples/TokenPaymaster.sol b/contracts/samples/TokenPaymaster.sol deleted file mode 100644 index 1d4c50d6..00000000 --- a/contracts/samples/TokenPaymaster.sol +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; - -// Import the required libraries and contracts -import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; - -import "../interfaces/IEntryPoint.sol"; -import "../core/BasePaymaster.sol"; -import "../core/Helpers.sol"; -import "./utils/UniswapHelper.sol"; -import "./utils/OracleHelper.sol"; - -/// @title Sample ERC-20 Token Paymaster for ERC-4337 -/// This Paymaster covers gas fees in exchange for ERC20 tokens charged using allowance pre-issued by ERC-4337 accounts. -/// The contract refunds excess tokens if the actual gas cost is lower than the initially provided amount. -/// The token price cannot be queried in the validation code due to storage access restrictions of ERC-4337. -/// The price is cached inside the contract and is updated in the 'postOp' stage if the change is >10%. -/// It is theoretically possible the token has depreciated so much since the last 'postOp' the refund becomes negative. -/// The contract reverts the inner user transaction in that case but keeps the charge. -/// The contract also allows honest clients to prepay tokens at a higher price to avoid getting reverted. -/// It also allows updating price configuration and withdrawing tokens by the contract owner. -/// The contract uses an Oracle to fetch the latest token prices. -/// @dev Inherits from BasePaymaster. -contract TokenPaymaster is BasePaymaster, UniswapHelper, OracleHelper { - - using UserOperationLib for PackedUserOperation; - - struct TokenPaymasterConfig { - /// @notice The price markup percentage applied to the token price (1e26 = 100%). Ranges from 1e26 to 2e26 - uint256 priceMarkup; - - /// @notice Exchange tokens to native currency if the EntryPoint balance of this Paymaster falls below this value - uint128 minEntryPointBalance; - - /// @notice Estimated gas cost for refunding tokens after the transaction is completed - uint48 refundPostopCost; - - /// @notice Transactions are only valid as long as the cached price is not older than this value - uint48 priceMaxAge; - } - - event ConfigUpdated(TokenPaymasterConfig tokenPaymasterConfig); - - event UserOperationSponsored(address indexed user, uint256 actualTokenCharge, uint256 actualGasCost, uint256 actualTokenPriceWithMarkup); - - event Received(address indexed sender, uint256 value); - - /// @notice All 'price' variables are multiplied by this value to avoid rounding up - uint256 private constant PRICE_DENOMINATOR = 1e26; - - TokenPaymasterConfig public tokenPaymasterConfig; - - /// @notice Initializes the TokenPaymaster contract with the given parameters. - /// @param _token The ERC20 token used for transaction fee payments. - /// @param _entryPoint The EntryPoint contract used in the Account Abstraction infrastructure. - /// @param _wrappedNative The ERC-20 token that wraps the native asset for current chain. - /// @param _uniswap The Uniswap V3 SwapRouter contract. - /// @param _tokenPaymasterConfig The configuration for the Token Paymaster. - /// @param _oracleHelperConfig The configuration for the Oracle Helper. - /// @param _uniswapHelperConfig The configuration for the Uniswap Helper. - /// @param _owner The address that will be set as the owner of the contract. - constructor( - IERC20Metadata _token, - IEntryPoint _entryPoint, - IERC20 _wrappedNative, - ISwapRouter _uniswap, - TokenPaymasterConfig memory _tokenPaymasterConfig, - OracleHelperConfig memory _oracleHelperConfig, - UniswapHelperConfig memory _uniswapHelperConfig, - address _owner - ) - BasePaymaster( - _entryPoint - ) - OracleHelper( - _oracleHelperConfig - ) - UniswapHelper( - _token, - _wrappedNative, - _uniswap, - _uniswapHelperConfig - ) - { - setTokenPaymasterConfig(_tokenPaymasterConfig); - transferOwnership(_owner); - } - - /// @notice Updates the configuration for the Token Paymaster. - /// @param _tokenPaymasterConfig The new configuration struct. - function setTokenPaymasterConfig( - TokenPaymasterConfig memory _tokenPaymasterConfig - ) public onlyOwner { - require(_tokenPaymasterConfig.priceMarkup <= 2 * PRICE_DENOMINATOR, "TPM: price markup too high"); - require(_tokenPaymasterConfig.priceMarkup >= PRICE_DENOMINATOR, "TPM: price markup too low"); - tokenPaymasterConfig = _tokenPaymasterConfig; - emit ConfigUpdated(_tokenPaymasterConfig); - } - - function setUniswapConfiguration( - UniswapHelperConfig memory _uniswapHelperConfig - ) external onlyOwner { - _setUniswapHelperConfiguration(_uniswapHelperConfig); - } - - /// @notice Allows the contract owner to withdraw a specified amount of tokens from the contract. - /// @param to The address to transfer the tokens to. - /// @param amount The amount of tokens to transfer. - function withdrawToken(address to, uint256 amount) external onlyOwner { - SafeERC20.safeTransfer(token, to, amount); - } - - /// @notice Validates a paymaster user operation and calculates the required token amount for the transaction. - /// @param userOp The user operation data. - /// @param requiredPreFund The maximum cost (in native token) the paymaster has to prefund. - /// @return context The context containing the token amount and user sender address (if applicable). - /// @return validationResult A uint256 value indicating the result of the validation (always 0 in this implementation). - function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256 requiredPreFund) - internal - override - returns (bytes memory context, uint256 validationResult) {unchecked { - uint256 priceMarkup = tokenPaymasterConfig.priceMarkup; - uint256 dataLength = userOp.paymasterAndData.length - PAYMASTER_DATA_OFFSET; - require(dataLength == 0 || dataLength == 32, - "TPM: invalid data length" - ); - uint256 maxFeePerGas = userOp.unpackMaxFeePerGas(); - uint256 refundPostopCost = tokenPaymasterConfig.refundPostopCost; - require(refundPostopCost < userOp.unpackPostOpGasLimit(), "TPM: postOpGasLimit too low"); - uint256 preChargeNative = requiredPreFund + (refundPostopCost * maxFeePerGas); - // note: as price is in native-asset-per-token and we want more tokens increasing it means dividing it by markup - uint256 cachedPriceWithMarkup = cachedPrice * PRICE_DENOMINATOR / priceMarkup; - if (dataLength == 32) { - uint256 clientSuppliedPrice = uint256(bytes32(userOp.paymasterAndData[PAYMASTER_DATA_OFFSET : PAYMASTER_DATA_OFFSET + 32])); - if (clientSuppliedPrice < cachedPriceWithMarkup) { - // note: smaller number means 'more native asset per token' - cachedPriceWithMarkup = clientSuppliedPrice; - } - } - uint256 tokenAmount = weiToToken(preChargeNative, cachedPriceWithMarkup); - SafeERC20.safeTransferFrom(token, userOp.sender, address(this), tokenAmount); - context = abi.encode(tokenAmount, userOp.sender); - validationResult = _packValidationData( - false, - uint48(cachedPriceTimestamp + tokenPaymasterConfig.priceMaxAge), - 0 - ); - } - } - - /// @notice Performs post-operation tasks, such as updating the token price and refunding excess tokens. - /// @dev This function is called after a user operation has been executed or reverted. - /// @param context The context containing the token amount and user sender address. - /// @param actualGasCost The actual gas cost of the transaction. - /// @param actualUserOpFeePerGas - the gas price this UserOp pays. This value is based on the UserOp's maxFeePerGas - // and maxPriorityFee (and basefee) - // It is not the same as tx.gasprice, which is what the bundler pays. - function _postOp(PostOpMode, bytes calldata context, uint256 actualGasCost, uint256 actualUserOpFeePerGas) internal override { - unchecked { - uint256 priceMarkup = tokenPaymasterConfig.priceMarkup; - ( - uint256 preCharge, - address userOpSender - ) = abi.decode(context, (uint256, address)); - uint256 _cachedPrice = updateCachedPrice(false); - // note: as price is in native-asset-per-token and we want more tokens increasing it means dividing it by markup - uint256 cachedPriceWithMarkup = _cachedPrice * PRICE_DENOMINATOR / priceMarkup; - // Refund tokens based on actual gas cost - uint256 actualChargeNative = actualGasCost + tokenPaymasterConfig.refundPostopCost * actualUserOpFeePerGas; - uint256 actualTokenNeeded = weiToToken(actualChargeNative, cachedPriceWithMarkup); - if (preCharge > actualTokenNeeded) { - // If the initially provided token amount is greater than the actual amount needed, refund the difference - SafeERC20.safeTransfer( - token, - userOpSender, - preCharge - actualTokenNeeded - ); - } else if (preCharge < actualTokenNeeded) { - // Attempt to cover Paymaster's gas expenses by withdrawing the 'overdraft' from the client - // If the transfer reverts also revert the 'postOp' to remove the incentive to cheat - SafeERC20.safeTransferFrom( - token, - userOpSender, - address(this), - actualTokenNeeded - preCharge - ); - } - - emit UserOperationSponsored(userOpSender, actualTokenNeeded, actualGasCost, cachedPriceWithMarkup); - refillEntryPointDeposit(_cachedPrice); - } - } - - /// @notice If necessary this function uses this Paymaster's token balance to refill the deposit on EntryPoint - /// @param _cachedPrice the token price that will be used to calculate the swap amount. - function refillEntryPointDeposit(uint256 _cachedPrice) private { - uint256 currentEntryPointBalance = entryPoint.balanceOf(address(this)); - if ( - currentEntryPointBalance < tokenPaymasterConfig.minEntryPointBalance - ) { - uint256 swappedWeth = _maybeSwapTokenToWeth(token, _cachedPrice); - unwrapWeth(swappedWeth); - entryPoint.depositTo{value: address(this).balance}(address(this)); - } - } - - receive() external payable { - emit Received(msg.sender, msg.value); - } - - function withdrawEth(address payable recipient, uint256 amount) external onlyOwner { - (bool success,) = recipient.call{value: amount}(""); - require(success, "withdraw failed"); - } -} diff --git a/contracts/samples/VerifyingPaymaster.sol b/contracts/samples/VerifyingPaymaster.sol deleted file mode 100644 index 1ddce2f4..00000000 --- a/contracts/samples/VerifyingPaymaster.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; - -/* solhint-disable reason-string */ -/* solhint-disable no-inline-assembly */ - -import "../core/BasePaymaster.sol"; -import "../core/UserOperationLib.sol"; -import "../core/Helpers.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -/** - * A sample paymaster that uses external service to decide whether to pay for the UserOp. - * The paymaster trusts an external signer to sign the transaction. - * The calling user must pass the UserOp to that external signer first, which performs - * whatever off-chain verification before signing the UserOp. - * Note that this signature is NOT a replacement for the account-specific signature: - * - the paymaster checks a signature to agree to PAY for GAS. - * - the account checks a signature to prove identity and account ownership. - */ -contract VerifyingPaymaster is BasePaymaster { - - using UserOperationLib for PackedUserOperation; - - address public immutable verifyingSigner; - - uint256 private constant VALID_TIMESTAMP_OFFSET = PAYMASTER_DATA_OFFSET; - - uint256 private constant SIGNATURE_OFFSET = VALID_TIMESTAMP_OFFSET + 64; - - constructor(IEntryPoint _entryPoint, address _verifyingSigner) BasePaymaster(_entryPoint) { - verifyingSigner = _verifyingSigner; - } - - /** - * return the hash we're going to sign off-chain (and validate on-chain) - * this method is called by the off-chain service, to sign the request. - * it is called on-chain from the validatePaymasterUserOp, to validate the signature. - * note that this signature covers all fields of the UserOperation, except the "paymasterAndData", - * which will carry the signature itself. - */ - function getHash(PackedUserOperation calldata userOp, uint48 validUntil, uint48 validAfter) - public view returns (bytes32) { - //can't use userOp.hash(), since it contains also the paymasterAndData itself. - address sender = userOp.getSender(); - return - keccak256( - abi.encode( - sender, - userOp.nonce, - keccak256(userOp.initCode), - keccak256(userOp.callData), - userOp.accountGasLimits, - uint256(bytes32(userOp.paymasterAndData[PAYMASTER_VALIDATION_GAS_OFFSET : PAYMASTER_DATA_OFFSET])), - userOp.preVerificationGas, - userOp.gasFees, - block.chainid, - address(this), - validUntil, - validAfter - ) - ); - } - - /** - * verify our external signer signed this request. - * the "paymasterAndData" is expected to be the paymaster and a signature over the entire request params - * paymasterAndData[:20] : address(this) - * paymasterAndData[20:84] : abi.encode(validUntil, validAfter) - * paymasterAndData[84:] : signature - */ - function _validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 /*userOpHash*/, uint256 requiredPreFund) - internal view override returns (bytes memory context, uint256 validationData) { - (requiredPreFund); - - (uint48 validUntil, uint48 validAfter, bytes calldata signature) = parsePaymasterAndData(userOp.paymasterAndData); - //ECDSA library supports both 64 and 65-byte long signatures. - // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA" - require(signature.length == 64 || signature.length == 65, "VerifyingPaymaster: invalid signature length in paymasterAndData"); - bytes32 hash = MessageHashUtils.toEthSignedMessageHash(getHash(userOp, validUntil, validAfter)); - - //don't revert on signature failure: return SIG_VALIDATION_FAILED - if (verifyingSigner != ECDSA.recover(hash, signature)) { - return ("", _packValidationData(true, validUntil, validAfter)); - } - - //no need for other on-chain validation: entire UserOp should have been checked - // by the external service prior to signing it. - return ("", _packValidationData(false, validUntil, validAfter)); - } - - function parsePaymasterAndData(bytes calldata paymasterAndData) public pure returns (uint48 validUntil, uint48 validAfter, bytes calldata signature) { - (validUntil, validAfter) = abi.decode(paymasterAndData[VALID_TIMESTAMP_OFFSET :], (uint48, uint48)); - signature = paymasterAndData[SIGNATURE_OFFSET :]; - } -} diff --git a/gascalc/5-token-paymaster.gas.ts b/gascalc/5-token-paymaster.gas.ts deleted file mode 100644 index b9505882..00000000 --- a/gascalc/5-token-paymaster.gas.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { parseEther } from 'ethers/lib/utils' -import { - TestERC20__factory, TestOracle2__factory, - TestUniswap__factory, - TestWrappedNativeToken__factory, TokenPaymaster, - TokenPaymaster__factory -} from '../typechain' -import { ethers } from 'hardhat' -import { GasCheckCollector, GasChecker } from './GasChecker' -import { Create2Factory } from '../src/Create2Factory' -import { hexValue } from '@ethersproject/bytes' -import { - OracleHelper as OracleHelperNamespace, - UniswapHelper as UniswapHelperNamespace -} from '../typechain/contracts/samples/TokenPaymaster' -import { BigNumber } from 'ethers' -import { createAccountOwner } from '../test/testutils' -// const ethersSigner = ethers.provider.getSigner() - -context('Token Paymaster', function () { - this.timeout(60000) - const g = new GasChecker() - - let paymasterAddress: string - before(async () => { - await GasCheckCollector.init() - const globalSigner = ethers.provider.getSigner() - const create2Factory = new Create2Factory(ethers.provider, globalSigner) - - const ethersSigner = createAccountOwner() - await globalSigner.sendTransaction({ to: ethersSigner.getAddress(), value: parseEther('10') }) - - const minEntryPointBalance = 1e17.toString() - const initialPriceToken = 100000000 // USD per TOK - const initialPriceEther = 500000000 // USD per ETH - const priceDenominator = BigNumber.from(10).pow(26) - - const tokenInit = await new TestERC20__factory(ethersSigner).getDeployTransaction(6) - const tokenAddress = await create2Factory.deploy(tokenInit, 0) - const token = TestERC20__factory.connect(tokenAddress, ethersSigner) - - const wethInit = await new TestWrappedNativeToken__factory(ethersSigner).getDeployTransaction() - const wethAddress = await create2Factory.deploy(wethInit, 0) - const testUniswapInit = await new TestUniswap__factory(ethersSigner).getDeployTransaction(wethAddress) - const testUniswapAddress = await create2Factory.deploy(testUniswapInit, 0) - - const tokenPaymasterConfig: TokenPaymaster.TokenPaymasterConfigStruct = { - priceMaxAge: 86400, - refundPostopCost: 40000, - minEntryPointBalance, - priceMarkup: priceDenominator.mul(15).div(10) // +50% - } - - const nativeAssetOracleInit = await new TestOracle2__factory(ethersSigner).getDeployTransaction(initialPriceEther, 8) - const nativeAssetOracleAddress = await create2Factory.deploy(nativeAssetOracleInit, 0, 10_000_000) - const tokenOracleInit = await new TestOracle2__factory(ethersSigner).getDeployTransaction(initialPriceToken, 8) - const tokenOracleAddress = await create2Factory.deploy(tokenOracleInit, 0, 10_000_000) - - const oracleHelperConfig: OracleHelperNamespace.OracleHelperConfigStruct = { - cacheTimeToLive: 100000000, - maxOracleRoundAge: 0, - nativeOracle: nativeAssetOracleAddress, - nativeOracleReverse: false, - priceUpdateThreshold: priceDenominator.mul(2).div(10), // +20% - tokenOracle: tokenOracleAddress, - tokenOracleReverse: false, - tokenToNativeOracle: false - } - - const uniswapHelperConfig: UniswapHelperNamespace.UniswapHelperConfigStruct = { - minSwapAmount: 1, - slippage: 5, - uniswapPoolFee: 3 - } - - const owner = await ethersSigner.getAddress() - - const paymasterInit = hexValue(new TokenPaymaster__factory(ethersSigner).getDeployTransaction( - tokenAddress, - g.entryPoint().address, - wethAddress, - testUniswapAddress, - tokenPaymasterConfig, - oracleHelperConfig, - uniswapHelperConfig, - owner - ).data!) - paymasterAddress = await create2Factory.deploy(paymasterInit, 0) - const paymaster = TokenPaymaster__factory.connect(paymasterAddress, ethersSigner) - await paymaster.addStake(1, { value: 1 }) - await g.entryPoint().depositTo(paymaster.address, { value: parseEther('10') }) - await paymaster.updateCachedPrice(true) - await g.createAccounts1(11) - await token.sudoMint(await ethersSigner.getAddress(), parseEther('20')) - await token.transfer(paymaster.address, parseEther('0.1')) - for (const address of g.createdAccounts) { - await token.transfer(address, parseEther('1')) - await token.sudoApprove(address, paymaster.address, ethers.constants.MaxUint256) - } - - console.log('==addresses:', { - ethersSigner: await ethersSigner.getAddress(), - paymasterAddress, - nativeAssetOracleAddress, - tokenOracleAddress, - tokenAddress, - owner, - createdAccounts: g.createdAccounts - }) - }) - - it('token paymaster', async function () { - await g.addTestRow({ title: 'token paymaster', count: 1, paymaster: paymasterAddress, diffLastGas: false }) - await g.addTestRow({ - title: 'token paymaster with diff', - count: 2, - paymaster: paymasterAddress, - diffLastGas: true - }) - }) - - it('token paymaster 10', async function () { - if (g.skipLong()) this.skip() - - await g.addTestRow({ title: 'token paymaster', count: 10, paymaster: paymasterAddress, diffLastGas: false }) - await g.addTestRow({ - title: 'token paymaster with diff', - count: 11, - paymaster: paymasterAddress, - diffLastGas: true - }) - }) -}) diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 0c443cc6..3cc13daf 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -43,13 +43,5 @@ ║ paymaster+postOp │ 10 │ 456453 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp with diff │ 11 │ │ 41369 │ 12110 ║ -╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 121560 │ │ ║ -╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61106 │ 31847 ║ -╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 671961 │ │ ║ -╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61176 │ 31917 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/test/paymaster.test.ts b/test/paymaster.test.ts deleted file mode 100644 index 87cea2fb..00000000 --- a/test/paymaster.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { Wallet } from 'ethers' -import { ethers } from 'hardhat' -import { expect } from 'chai' -import { - SimpleAccount, - LegacyTokenPaymaster, - LegacyTokenPaymaster__factory, - TestCounter__factory, - SimpleAccountFactory, - SimpleAccountFactory__factory, EntryPoint -} from '../typechain' -import { - AddressZero, - createAccountOwner, - fund, - getBalance, - getTokenBalance, - rethrow, - checkForGeth, - calcGasUsage, - deployEntryPoint, - createAddress, - ONE_ETH, - createAccount, - getAccountAddress, decodeRevertReason -} from './testutils' -import { fillSignAndPack, simulateValidation } from './UserOp' -import { hexConcat, parseEther } from 'ethers/lib/utils' -import { PackedUserOperation } from './UserOperation' -import { hexValue } from '@ethersproject/bytes' - -describe('EntryPoint with paymaster', function () { - let entryPoint: EntryPoint - let accountOwner: Wallet - const ethersSigner = ethers.provider.getSigner() - let account: SimpleAccount - const beneficiaryAddress = '0x'.padEnd(42, '1') - let factory: SimpleAccountFactory - - function getAccountDeployer (entryPoint: string, accountOwner: string, _salt: number = 0): string { - return hexConcat([ - factory.address, - hexValue(factory.interface.encodeFunctionData('createAccount', [accountOwner, _salt])!) - ]) - } - - before(async function () { - this.timeout(20000) - await checkForGeth() - - entryPoint = await deployEntryPoint() - factory = await new SimpleAccountFactory__factory(ethersSigner).deploy(entryPoint.address) - - accountOwner = createAccountOwner(); - ({ proxy: account } = await createAccount(ethersSigner, await accountOwner.getAddress(), entryPoint.address, factory)) - await fund(account) - }) - - describe('#TokenPaymaster', () => { - let paymaster: LegacyTokenPaymaster - const otherAddr = createAddress() - let ownerAddr: string - let pmAddr: string - - before(async () => { - paymaster = await new LegacyTokenPaymaster__factory(ethersSigner).deploy(factory.address, 'ttt', entryPoint.address) - pmAddr = paymaster.address - ownerAddr = await ethersSigner.getAddress() - }) - - it('paymaster should revert on wrong entryPoint type', async () => { - // account is a sample contract with supportsInterface (which is obviously not an entrypoint) - const notEntryPoint = account - // a contract that has "supportsInterface" but with different interface value.. - await expect(new LegacyTokenPaymaster__factory(ethersSigner).deploy(factory.address, 'ttt', notEntryPoint.address)) - .to.be.revertedWith('IEntryPoint interface mismatch') - await expect(new LegacyTokenPaymaster__factory(ethersSigner).deploy(factory.address, 'ttt', AddressZero)) - .to.be.revertedWith('') - }) - - it('owner should have allowance to withdraw funds', async () => { - expect(await paymaster.allowance(pmAddr, ownerAddr)).to.equal(ethers.constants.MaxUint256) - expect(await paymaster.allowance(pmAddr, otherAddr)).to.equal(0) - }) - - it('should allow only NEW owner to move funds after transferOwnership', async () => { - await paymaster.transferOwnership(otherAddr) - expect(await paymaster.allowance(pmAddr, otherAddr)).to.equal(ethers.constants.MaxUint256) - expect(await paymaster.allowance(pmAddr, ownerAddr)).to.equal(0) - }) - }) - - describe('using TokenPaymaster (account pays in paymaster tokens)', () => { - let paymaster: LegacyTokenPaymaster - before(async () => { - paymaster = await new LegacyTokenPaymaster__factory(ethersSigner).deploy(factory.address, 'tst', entryPoint.address) - await entryPoint.depositTo(paymaster.address, { value: parseEther('1') }) - await paymaster.addStake(1, { value: parseEther('2') }) - }) - - describe('#handleOps', () => { - let calldata: string - before(async () => { - const updateEntryPoint = await account.populateTransaction.withdrawDepositTo(AddressZero, 0).then(tx => tx.data!) - calldata = await account.populateTransaction.execute(account.address, 0, updateEntryPoint).then(tx => tx.data!) - }) - it('paymaster should reject if account doesn\'t have tokens', async () => { - const op = await fillSignAndPack({ - sender: account.address, - paymaster: paymaster.address, - paymasterPostOpGasLimit: 3e5, - callData: calldata - }, accountOwner, entryPoint) - expect(await entryPoint.callStatic.handleOps([op], beneficiaryAddress, { - gasLimit: 1e7 - }).catch(e => decodeRevertReason(e))) - .to.include('TokenPaymaster: no balance') - expect(await entryPoint.handleOps([op], beneficiaryAddress, { - gasLimit: 1e7 - }).catch(e => decodeRevertReason(e))) - .to.include('TokenPaymaster: no balance') - }) - }) - - describe('create account', () => { - let createOp: PackedUserOperation - let created = false - const beneficiaryAddress = createAddress() - - it('should reject if account not funded', async () => { - const op = await fillSignAndPack({ - initCode: getAccountDeployer(entryPoint.address, accountOwner.address, 1), - verificationGasLimit: 1e7, - paymaster: paymaster.address, - paymasterPostOpGasLimit: 3e5 - }, accountOwner, entryPoint) - expect(await entryPoint.callStatic.handleOps([op], beneficiaryAddress, { - gasLimit: 1e7 - }).catch(e => decodeRevertReason(e))) - .to.include('TokenPaymaster: no balance') - }) - - it('should succeed to create account with tokens', async () => { - createOp = await fillSignAndPack({ - initCode: getAccountDeployer(entryPoint.address, accountOwner.address, 3), - verificationGasLimit: 2e6, - paymaster: paymaster.address, - paymasterPostOpGasLimit: 3e5, - nonce: 0 - }, accountOwner, entryPoint) - - const preAddr = createOp.sender - await paymaster.mintTokens(preAddr, parseEther('1')) - // paymaster is the token, so no need for "approve" or any init function... - - // const snapshot = await ethers.provider.send('evm_snapshot', []) - await simulateValidation(createOp, entryPoint.address, { gasLimit: 5e6 }) - // TODO: can't do opcode banning with EntryPointSimulations (since its not on-chain) add when we can debug_traceCall - // const [tx] = await ethers.provider.getBlock('latest').then(block => block.transactions) - // await checkForBannedOps(tx, true) - // await ethers.provider.send('evm_revert', [snapshot]) - - const rcpt = await entryPoint.handleOps([createOp], beneficiaryAddress, { - gasLimit: 1e7 - }).catch(rethrow()).then(async tx => await tx!.wait()) - console.log('\t== create gasUsed=', rcpt.gasUsed.toString()) - await calcGasUsage(rcpt, entryPoint) - created = true - }) - - it('account should pay for its creation (in tst)', async function () { - if (!created) this.skip() - // TODO: calculate needed payment - const ethRedeemed = await getBalance(beneficiaryAddress) - expect(ethRedeemed).to.above(100000) - - const accountAddr = await getAccountAddress(accountOwner.address, factory) - const postBalance = await getTokenBalance(paymaster, accountAddr) - expect(1e18 - postBalance).to.above(10000) - }) - - it('should reject if account already created', async function () { - if (!created) this.skip() - await expect(entryPoint.callStatic.handleOps([createOp], beneficiaryAddress, { - gasLimit: 1e7 - }).catch(rethrow())).to.revertedWith('sender already constructed') - }) - - it('batched request should each pay for its share', async function () { - this.timeout(20000) - // validate context is passed correctly to postOp - // (context is the account to pay with) - - const beneficiaryAddress = createAddress() - const testCounter = await new TestCounter__factory(ethersSigner).deploy() - const justEmit = testCounter.interface.encodeFunctionData('justemit') - const execFromSingleton = account.interface.encodeFunctionData('execute', [testCounter.address, 0, justEmit]) - - const ops: PackedUserOperation[] = [] - const accounts: SimpleAccount[] = [] - - for (let i = 0; i < 4; i++) { - const { proxy: aAccount } = await createAccount(ethersSigner, await accountOwner.getAddress(), entryPoint.address) - await paymaster.mintTokens(aAccount.address, parseEther('1')) - const op = await fillSignAndPack({ - sender: aAccount.address, - callData: execFromSingleton, - paymaster: paymaster.address, - paymasterPostOpGasLimit: 3e5 - }, accountOwner, entryPoint) - - accounts.push(aAccount) - ops.push(op) - } - - const pmBalanceBefore = await paymaster.balanceOf(paymaster.address).then(b => b.toNumber()) - await entryPoint.handleOps(ops, beneficiaryAddress).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) - const paid = parseEther('1').sub(bal.toString()).toNumber() - - // roughly each account should pay 1/4th of total price, within 15% - // (first account pays more, for warming up..) - expect(paid).to.be.closeTo(totalPaid / 4, paid * 0.15) - } - }) - - // accounts attempt to grief paymaster: both accounts pass validatePaymasterUserOp (since they have enough balance) - // but the execution of account1 drains account2. - // as a result, the postOp of the paymaster reverts, and cause entire handleOp to revert. - describe('grief attempt', () => { - let account2: SimpleAccount - let approveCallData: string - before(async function () { - this.timeout(20000); - ({ proxy: account2 } = await createAccount(ethersSigner, await accountOwner.getAddress(), entryPoint.address)) - await paymaster.mintTokens(account2.address, parseEther('1')) - await paymaster.mintTokens(account.address, parseEther('1')) - approveCallData = paymaster.interface.encodeFunctionData('approve', [account.address, ethers.constants.MaxUint256]) - // need to call approve from account2. use paymaster for that - const approveOp = await fillSignAndPack({ - sender: account2.address, - callData: account2.interface.encodeFunctionData('execute', [paymaster.address, 0, approveCallData]), - paymaster: paymaster.address, - paymasterPostOpGasLimit: 3e5 - }, accountOwner, entryPoint) - await entryPoint.handleOps([approveOp], beneficiaryAddress) - expect(await paymaster.allowance(account2.address, account.address)).to.eq(ethers.constants.MaxUint256) - }) - - it('griefing attempt in postOp should cause the execution part of UserOp to revert', async () => { - // account1 is approved to withdraw going to withdraw account2's balance - - const account2Balance = await paymaster.balanceOf(account2.address) - const transferCost = parseEther('1').sub(account2Balance) - const withdrawAmount = account2Balance.sub(transferCost.mul(0)) - const withdrawTokens = paymaster.interface.encodeFunctionData('transferFrom', [account2.address, account.address, withdrawAmount]) - const execFromEntryPoint = account.interface.encodeFunctionData('execute', [paymaster.address, 0, withdrawTokens]) - - const userOp1 = await fillSignAndPack({ - sender: account.address, - callData: execFromEntryPoint, - paymaster: paymaster.address, - paymasterPostOpGasLimit: 3e5 - }, accountOwner, entryPoint) - - // account2's operation is unimportant, as it is going to be reverted - but the paymaster will have to pay for it. - const userOp2 = await fillSignAndPack({ - sender: account2.address, - callData: execFromEntryPoint, - paymaster: paymaster.address, - paymasterPostOpGasLimit: 3e5, - callGasLimit: 1e6 - }, accountOwner, entryPoint) - - const rcpt = - await entryPoint.handleOps([ - userOp1, - userOp2 - ], beneficiaryAddress) - - const transferEvents = await paymaster.queryFilter(paymaster.filters.Transfer(), rcpt.blockHash) - const [log1, log2] = await entryPoint.queryFilter(entryPoint.filters.UserOperationEvent(), rcpt.blockHash) - expect(log1.args.success).to.eq(true) - expect(log2.args.success).to.eq(false) - expect(transferEvents.length).to.eq(2) - }) - }) - }) - describe('withdraw', () => { - const withdrawAddress = createAddress() - it('should fail to withdraw before unstake', async function () { - this.timeout(20000) - await expect( - paymaster.withdrawStake(withdrawAddress) - ).to.revertedWith('must call unlockStake') - }) - it('should be able to withdraw after unstake delay', async () => { - await paymaster.unlockStake() - const amount = await entryPoint.getDepositInfo(paymaster.address).then(info => info.stake) - expect(amount).to.be.gte(ONE_ETH.div(2)) - await ethers.provider.send('evm_mine', [Math.floor(Date.now() / 1000) + 1000]) - await paymaster.withdrawStake(withdrawAddress) - expect(await ethers.provider.getBalance(withdrawAddress)).to.eql(amount) - expect(await entryPoint.getDepositInfo(paymaster.address).then(info => info.stake)).to.eq(0) - }) - }) - }) -}) diff --git a/test/samples/OracleHelper.test.ts b/test/samples/OracleHelper.test.ts deleted file mode 100644 index 84900e09..00000000 --- a/test/samples/OracleHelper.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { assert } from 'chai' -import { ethers } from 'hardhat' - -import { AddressZero } from '../testutils' - -import { - EntryPoint, - EntryPoint__factory, - TestERC20, - TestERC20__factory, - TestOracle2, - TestOracle2__factory, - TokenPaymaster, - TokenPaymaster__factory -} from '../../typechain' -import { - OracleHelper as OracleHelperNamespace, - UniswapHelper as UniswapHelperNamespace -} from '../../typechain/contracts/samples/TokenPaymaster' -import { BigNumber } from 'ethers' -import { parseEther } from 'ethers/lib/utils' - -const priceDenominator = BigNumber.from(10).pow(26) - -const sampleResponses = { - 'LINK/USD': { - decimals: 8, - answer: '633170000', // Answer: $6.3090 - note: price is USD per LINK - roundId: '110680464442257310968', - startedAt: '1684929731', - updatedAt: '1684929731', - answeredInRound: '110680464442257310968' - }, - 'ETH/USD': { - decimals: 8, - answer: '181451000000', // Answer: $1,817.65 - USD per ETH - roundId: '110680464442257311466', - startedAt: '1684929347', - updatedAt: '1684929347', - answeredInRound: '110680464442257311466' - }, - 'LINK/ETH': { // the direct route may be better in some use-cases - decimals: 18, - answer: '3492901256673149', // Answer: Ξ0.0034929013 - the answer is exact ETH.WEI per LINK - roundId: '73786976294838213626', - startedAt: '1684924307', - updatedAt: '1684924307', - answeredInRound: '73786976294838213626' - }, - 'ETH/BTC': { // considering BTC to be a token to test a reverse price feed logic with real data - decimals: 8, - answer: '6810994', // ₿0.06810994 - roundId: '18446744073709566497', - startedAt: '1684943615', - updatedAt: '1684943615', - answeredInRound: '18446744073709566497' - } -} - -// note: direct or reverse designations are quite arbitrary -describe('OracleHelper', function () { - function testOracleFiguredPriceOut (): void { - it('should figure out the correct price', async function () { - await testEnv.paymaster.updateCachedPrice(true) - const cachedPrice = await testEnv.paymaster.cachedPrice() - const tokensPerEtherCalculated = await testEnv.paymaster.weiToToken(parseEther('1'), cachedPrice) - assert.equal(cachedPrice.toString(), testEnv.expectedPrice.toString(), 'price not right') - assert.equal(tokensPerEtherCalculated.toString(), testEnv.expectedTokensPerEtherCalculated.toString(), 'tokens amount not right') - }) - } - - function getOracleConfig ({ - nativeOracleReverse, - tokenOracleReverse, - tokenToNativeOracle - }: { - nativeOracleReverse: boolean - tokenOracleReverse: boolean - tokenToNativeOracle: boolean - }): OracleHelperNamespace.OracleHelperConfigStruct { - return { - nativeOracleReverse, - tokenOracleReverse, - tokenToNativeOracle, - nativeOracle: tokenToNativeOracle ? AddressZero : testEnv.nativeAssetOracle.address, - tokenOracle: testEnv.tokenOracle.address, - cacheTimeToLive: 0, - maxOracleRoundAge: 0, - priceUpdateThreshold: 0 - } - } - - interface TestEnv { - owner: string - expectedPrice: string - expectedTokensPerEtherCalculated: string - tokenPaymasterConfig: TokenPaymaster.TokenPaymasterConfigStruct - uniswapHelperConfig: UniswapHelperNamespace.UniswapHelperConfigStruct - token: TestERC20 - paymaster: TokenPaymaster - tokenOracle: TestOracle2 - nativeAssetOracle: TestOracle2 - } - - // @ts-ignore - const testEnv: TestEnv = {} - - let entryPoint: EntryPoint - - before(async function () { - const ethersSigner = ethers.provider.getSigner() - testEnv.owner = await ethersSigner.getAddress() - - testEnv.tokenPaymasterConfig = { - priceMaxAge: 86400, - refundPostopCost: 40000, - minEntryPointBalance: 0, - priceMarkup: priceDenominator.mul(19).div(10) // 190% - } - testEnv.uniswapHelperConfig = { - minSwapAmount: 1, - slippage: 5, - uniswapPoolFee: 3 - } - - // TODO: what do I need to do with the oracle decimals? - testEnv.tokenOracle = await new TestOracle2__factory(ethersSigner).deploy(1, 0) - testEnv.nativeAssetOracle = await new TestOracle2__factory(ethersSigner).deploy(1, 0) - - testEnv.token = await new TestERC20__factory(ethersSigner).deploy(18) - - entryPoint = await new EntryPoint__factory(ethersSigner).deploy() - - testEnv.paymaster = await new TokenPaymaster__factory(ethersSigner).deploy( - testEnv.token.address, - entryPoint.address, - AddressZero, - testEnv.owner, // cannot approve to AddressZero - testEnv.tokenPaymasterConfig, - getOracleConfig({ - nativeOracleReverse: false, - tokenOracleReverse: false, - tokenToNativeOracle: false - }), - testEnv.uniswapHelperConfig, - testEnv.owner - ) - }) - - describe('with one-hop direct price ETH per TOKEN', function () { - before(async function () { - const res = sampleResponses['LINK/ETH'] // note: Chainlink Oracle names are opposite direction of 'answer' - await testEnv.tokenOracle.setPrice(res.answer) // Ξ0.0034929013 - await testEnv.tokenOracle.setDecimals(res.decimals) - // making sure the native asset oracle is not accessed during the calculation - await testEnv.nativeAssetOracle.setPrice('0xfffffffffffffffffffff') - const tokenOracleDecimalPower = BigNumber.from(10).pow(res.decimals) - testEnv.expectedPrice = - BigNumber.from(res.answer) - .mul(priceDenominator) - .div(tokenOracleDecimalPower) - .toString() - - testEnv.expectedTokensPerEtherCalculated = - BigNumber - .from(parseEther('1')) - .mul(tokenOracleDecimalPower) - .div(res.answer) - .toString() - - const ethersSigner = ethers.provider.getSigner() - testEnv.paymaster = await new TokenPaymaster__factory(ethersSigner).deploy( - testEnv.token.address, - entryPoint.address, - AddressZero, - testEnv.owner, // cannot approve to AddressZero - testEnv.tokenPaymasterConfig, - getOracleConfig({ - tokenToNativeOracle: true, - tokenOracleReverse: false, - nativeOracleReverse: false - }), - testEnv.uniswapHelperConfig, - testEnv.owner - ) - }) - - testOracleFiguredPriceOut() - }) - - describe('with one-hop reverse price TOKEN per ETH', function () { - before(async function () { - const res = sampleResponses['ETH/BTC'] - await testEnv.tokenOracle.setPrice(res.answer) // ₿0.06810994 - await testEnv.tokenOracle.setDecimals(res.decimals) - // making sure the native asset oracle is not accessed during the calculation - await testEnv.nativeAssetOracle.setPrice('0xfffffffffffffffffffff') - const tokenOracleDecimalPower = BigNumber.from(10).pow(res.decimals) - testEnv.expectedPrice = - BigNumber.from(priceDenominator) - .mul(tokenOracleDecimalPower) - .div(res.answer) - .toString() - - const expectedTokensPerEtherCalculated = - BigNumber - .from(parseEther('1')) - .mul(res.answer) - .div(tokenOracleDecimalPower) - .toString() - - testEnv.expectedTokensPerEtherCalculated = - BigNumber - .from(parseEther('1')) - .mul(priceDenominator.toString()) - .div(testEnv.expectedPrice) - .toString() - - // sanity check for the price calculation - use direct price and cached-like reverse price - assert.equal(expectedTokensPerEtherCalculated.toString(), testEnv.expectedTokensPerEtherCalculated.toString()) - - const ethersSigner = ethers.provider.getSigner() - testEnv.paymaster = await new TokenPaymaster__factory(ethersSigner).deploy( - testEnv.token.address, - entryPoint.address, - AddressZero, - testEnv.owner, // cannot approve to AddressZero - testEnv.tokenPaymasterConfig, - getOracleConfig({ - tokenToNativeOracle: true, - tokenOracleReverse: true, - nativeOracleReverse: false - }), - testEnv.uniswapHelperConfig, - testEnv.owner - ) - }) - testOracleFiguredPriceOut() - }) - - describe('with two-hops price USD-per-TOKEN and USD-per-ETH', function () { - before(async function () { - const resToken = sampleResponses['LINK/USD'] - const resNative = sampleResponses['ETH/USD'] - - await testEnv.tokenOracle.setPrice(resToken.answer) // $6.3090 - await testEnv.tokenOracle.setDecimals(resToken.decimals) - - await testEnv.nativeAssetOracle.setPrice(resNative.answer) // $1,817.65 - await testEnv.nativeAssetOracle.setDecimals(resNative.decimals) - - const ethersSigner = ethers.provider.getSigner() - testEnv.paymaster = await new TokenPaymaster__factory(ethersSigner).deploy( - testEnv.token.address, - entryPoint.address, - AddressZero, - testEnv.owner, // cannot approve to AddressZero - testEnv.tokenPaymasterConfig, - getOracleConfig({ - tokenToNativeOracle: false, - tokenOracleReverse: false, - nativeOracleReverse: false - }), - testEnv.uniswapHelperConfig, - testEnv.owner - ) - // note: oracle decimals are same and cancel each other out - testEnv.expectedPrice = - priceDenominator - .mul(resToken.answer) - .div(resNative.answer) - .toString() - - testEnv.expectedTokensPerEtherCalculated = - BigNumber - .from(parseEther('1')) - .mul(priceDenominator.toString()) - .div(testEnv.expectedPrice) - .toString() - }) - - testOracleFiguredPriceOut() - }) - - // TODO: these oracle types are not common but we probably want to support in any case - describe.skip('with two-hops price TOK/USD and ETH/USD', () => {}) - describe.skip('with two-hops price TOK/USD and USD/ETH', () => {}) - describe.skip('with two-hops price USD/TOK and ETH/USD', () => {}) -}) diff --git a/test/samples/TokenPaymaster.test.ts b/test/samples/TokenPaymaster.test.ts deleted file mode 100644 index 840521bc..00000000 --- a/test/samples/TokenPaymaster.test.ts +++ /dev/null @@ -1,507 +0,0 @@ -import { ContractReceipt, ContractTransaction, Wallet, utils, BigNumber } from 'ethers' -import { hexlify, hexZeroPad, Interface, parseEther } from 'ethers/lib/utils' -import { assert, expect } from 'chai' -import { ethers } from 'hardhat' - -import { - EntryPoint, - EntryPoint__factory, - SimpleAccount, - SimpleAccountFactory, - SimpleAccountFactory__factory, - TestERC20, - TestERC20__factory, - TestOracle2, - TestOracle2__factory, - TestUniswap, - TestUniswap__factory, - TestWrappedNativeToken, - TestWrappedNativeToken__factory, - TokenPaymaster, - TokenPaymaster__factory -} from '../../typechain' -import { - OracleHelper as OracleHelperNamespace, - UniswapHelper as UniswapHelperNamespace -} from '../../typechain/contracts/samples/TokenPaymaster' -import { - checkForGeth, - createAccount, - createAccountOwner, - decodeRevertReason, - deployEntryPoint, - fund, objdump -} from '../testutils' - -import { fillUserOp, packUserOp, signUserOp } from '../UserOp' - -const priceDenominator = BigNumber.from(10).pow(26) - -function uniq (arr: any[]): any[] { - // remove items with duplicate "name" attribute - return Object.values(arr.reduce((set, item) => ({ ...set, [item.name]: item }), {})) -} - -describe('TokenPaymaster', function () { - if (process.env.COVERAGE != null) { - return - } - - const minEntryPointBalance = 1e17.toString() - const initialPriceToken = 100000000 // USD per TOK - const initialPriceEther = 500000000 // USD per ETH - const ethersSigner = ethers.provider.getSigner() - const beneficiaryAddress = '0x'.padEnd(42, '1') - const testInterface = new Interface( - uniq([ - ...TestUniswap__factory.abi, - ...TestERC20__factory.abi, - ...TokenPaymaster__factory.abi, - ...EntryPoint__factory.abi - ]) - ) - - let chainId: number - let testUniswap: TestUniswap - let entryPoint: EntryPoint - let accountOwner: Wallet - let tokenOracle: TestOracle2 - let nativeAssetOracle: TestOracle2 - let account: SimpleAccount - let factory: SimpleAccountFactory - let paymasterAddress: string - let paymaster: TokenPaymaster - let paymasterOwner: string - let callData: string - let token: TestERC20 - let weth: TestWrappedNativeToken - - before(async function () { - entryPoint = await deployEntryPoint() - weth = await new TestWrappedNativeToken__factory(ethersSigner).deploy() - testUniswap = await new TestUniswap__factory(ethersSigner).deploy(weth.address) - factory = await new SimpleAccountFactory__factory(ethersSigner).deploy(entryPoint.address) - - accountOwner = createAccountOwner() - chainId = (await accountOwner.provider.getNetwork()).chainId - const { proxy } = await createAccount(ethersSigner, await accountOwner.getAddress(), entryPoint.address, factory) - account = proxy - await fund(account) - await checkForGeth() - token = await new TestERC20__factory(ethersSigner).deploy(6) - nativeAssetOracle = await new TestOracle2__factory(ethersSigner).deploy(initialPriceEther, 8) - tokenOracle = await new TestOracle2__factory(ethersSigner).deploy(initialPriceToken, 8) - await weth.deposit({ value: parseEther('1') }) - await weth.transfer(testUniswap.address, parseEther('1')) - paymasterOwner = await ethersSigner.getAddress() - const tokenPaymasterConfig: TokenPaymaster.TokenPaymasterConfigStruct = { - priceMaxAge: 86400, - refundPostopCost: 40000, - minEntryPointBalance, - priceMarkup: priceDenominator.mul(15).div(10) // +50% - } - - const oracleHelperConfig: OracleHelperNamespace.OracleHelperConfigStruct = { - cacheTimeToLive: 0, - maxOracleRoundAge: 0, - nativeOracle: nativeAssetOracle.address, - nativeOracleReverse: false, - priceUpdateThreshold: priceDenominator.mul(12).div(100).toString(), // 20% - tokenOracle: tokenOracle.address, - tokenOracleReverse: false, - tokenToNativeOracle: false - } - - const uniswapHelperConfig: UniswapHelperNamespace.UniswapHelperConfigStruct = { - minSwapAmount: 1, - slippage: 5, - uniswapPoolFee: 3 - } - - paymaster = await new TokenPaymaster__factory(ethersSigner).deploy( - token.address, - entryPoint.address, - weth.address, - testUniswap.address, - tokenPaymasterConfig, - oracleHelperConfig, - uniswapHelperConfig, - paymasterOwner - ) - paymasterAddress = paymaster.address - - await token.transfer(paymaster.address, 100) - await paymaster.updateCachedPrice(true) - await entryPoint.depositTo(paymaster.address, { value: parseEther('1000') }) - await paymaster.addStake(1, { value: parseEther('2') }) - - callData = await account.populateTransaction.execute(accountOwner.address, 0, '0x').then(tx => tx.data!) - }) - - it('Only owner should withdraw eth from paymaster to destination', async function () { - const recipient = accountOwner.address - const amount = 2e18.toString() - const balanceBefore = await ethers.provider.getBalance(paymasterAddress) - await fund(paymasterAddress, '2') - const balanceAfter = await ethers.provider.getBalance(paymasterAddress) - assert.equal(balanceBefore.add(BigNumber.from(amount)).toString(), balanceAfter.toString()) - - const impersonatedSigner = await ethers.getImpersonatedSigner('0x1234567890123456789012345678901234567890') - const paymasterDifferentSigner = TokenPaymaster__factory.connect(paymasterAddress, impersonatedSigner) - - // should revert for non owner - await expect(paymasterDifferentSigner.withdrawEth(paymasterOwner, amount)).to.be.revertedWith('OwnableUnauthorizedAccount') - - // should revert if the transfer fails - await expect(paymaster.withdrawEth(recipient, BigNumber.from(amount).mul(2))).to.be.revertedWith('withdraw failed') - - const recipientBalanceBefore = await ethers.provider.getBalance(recipient) - await paymaster.withdrawEth(recipient, balanceAfter) - const recipientBalanceAfter = await ethers.provider.getBalance(recipient) - assert.equal(recipientBalanceBefore.add(BigNumber.from(amount)).toString(), recipientBalanceAfter.toString()) - }) - - it('paymaster should reject if postOpGaSLimit is too low', async () => { - const snapshot = await ethers.provider.send('evm_snapshot', []) - const config = await paymaster.tokenPaymasterConfig() - let op = await fillUserOp({ - sender: account.address, - paymaster: paymasterAddress, - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: config.refundPostopCost - 1, // too low - callData - }, entryPoint) - op = signUserOp(op, accountOwner, entryPoint.address, chainId) - const opPacked = packUserOp(op) - // await expect( - expect(await entryPoint.handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) - .catch(e => decodeRevertReason(e))) - .to.match(/TPM: postOpGasLimit too low/) - - await ethers.provider.send('evm_revert', [snapshot]) - }) - - it('paymaster should reject if account does not have enough tokens or allowance', async () => { - const snapshot = await ethers.provider.send('evm_snapshot', []) - let op = await fillUserOp({ - sender: account.address, - paymaster: paymasterAddress, - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: 3e5, - callData - }, entryPoint) - op = signUserOp(op, accountOwner, entryPoint.address, chainId) - const opPacked = packUserOp(op) - // await expect( - expect(await entryPoint.handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) - .catch(e => decodeRevertReason(e))) - .to.match(/FailedOpWithRevert\(0,"AA33 reverted",ERC20InsufficientAllowance/) - - await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - - expect(await entryPoint.handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) - .catch(e => decodeRevertReason(e))) - .to.match(/FailedOpWithRevert\(0,"AA33 reverted",ERC20InsufficientBalance/) - - await ethers.provider.send('evm_revert', [snapshot]) - }) - - it('should be able to sponsor the UserOp while charging correct amount of ERC-20 tokens', async () => { - const snapshot = await ethers.provider.send('evm_snapshot', []) - await token.transfer(account.address, parseEther('1')) - await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - - let op = await fillUserOp({ - sender: account.address, - paymaster: paymasterAddress, - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: 3e5, - callData - }, entryPoint) - op = signUserOp(op, accountOwner, entryPoint.address, chainId) - const opPacked = packUserOp(op) - // for simpler 'gasPrice()' calculation - await ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', [utils.hexlify(op.maxFeePerGas)]) - const tx = await entryPoint - .handleOps([opPacked], beneficiaryAddress, { - gasLimit: 3e7, - maxFeePerGas: op.maxFeePerGas, - maxPriorityFeePerGas: op.maxFeePerGas - } - ) - .then(async tx => await tx.wait()) - - const decodedLogs = tx.logs.map(it => { - return testInterface.parseLog(it) - }) - const preChargeTokens = decodedLogs[0].args.value - const refundTokens = decodedLogs[2].args.value - const actualTokenChargeEvents = preChargeTokens.sub(refundTokens) - const actualTokenCharge = decodedLogs[3].args.actualTokenCharge - const actualTokenPriceWithMarkup = decodedLogs[3].args.actualTokenPriceWithMarkup - const actualGasCostPaymaster = decodedLogs[3].args.actualGasCost - const actualGasCostEntryPoint = decodedLogs[4].args.actualGasCost - const addedPostOpCost = BigNumber.from(op.maxFeePerGas).mul(40000) - - // note: as price is in ether-per-token, and we want more tokens, increasing it means dividing it by markup - const expectedTokenPriceWithMarkup = priceDenominator - .mul(initialPriceToken).div(initialPriceEther) // expectedTokenPrice of 0.2 as BigNumber - .mul(10).div(15) // added 150% priceMarkup - const expectedTokenCharge = actualGasCostPaymaster.add(addedPostOpCost).mul(priceDenominator).div(expectedTokenPriceWithMarkup) - const postOpGasCost = actualGasCostEntryPoint.sub(actualGasCostPaymaster) - assert.equal(decodedLogs.length, 5) - assert.equal(decodedLogs[4].args.success, true) - assert.equal(actualTokenChargeEvents.toString(), actualTokenCharge.toString()) - assert.equal(actualTokenChargeEvents.toString(), expectedTokenCharge.toString()) - assert.equal(actualTokenPriceWithMarkup.toString(), expectedTokenPriceWithMarkup.toString()) - assert.closeTo(postOpGasCost.div(tx.effectiveGasPrice).toNumber(), 50000, 20000) - await ethers.provider.send('evm_revert', [snapshot]) - }) - - it('should update cached token price if the change is above configured percentage', async function () { - const snapshot = await ethers.provider.send('evm_snapshot', []) - await token.transfer(account.address, parseEther('1')) - await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - await tokenOracle.setPrice(initialPriceToken * 5) - await nativeAssetOracle.setPrice(initialPriceEther * 10) - - let op = await fillUserOp({ - sender: account.address, - paymaster: paymasterAddress, - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: 3e5, - callData - }, entryPoint) - op = signUserOp(op, accountOwner, entryPoint.address, chainId) - const opPacked = packUserOp(op) - const tx: ContractTransaction = await entryPoint - .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) - const receipt: ContractReceipt = await tx.wait() - const block = await ethers.provider.getBlock(receipt.blockHash) - - const decodedLogs = receipt.logs.map(it => { - return testInterface.parseLog(it) - }) - - const oldExpectedPrice = priceDenominator.mul(initialPriceToken).div(initialPriceEther) - const newExpectedPrice = oldExpectedPrice.div(2) // ether DOUBLED in price relative to token - const oldExpectedPriceWithMarkup = oldExpectedPrice.mul(10).div(15) - const newExpectedPriceWithMarkup = oldExpectedPriceWithMarkup.div(2) - - const actualTokenPriceWithMarkup = decodedLogs[4].args.actualTokenPriceWithMarkup - assert.equal(actualTokenPriceWithMarkup.toString(), newExpectedPriceWithMarkup.toString()) - await expect(tx).to - .emit(paymaster, 'TokenPriceUpdated') - .withArgs(newExpectedPrice, oldExpectedPrice, block.timestamp) - - await ethers.provider.send('evm_revert', [snapshot]) - }) - - it('should use token price supplied by the client if it is better than cached', async function () { - const snapshot = await ethers.provider.send('evm_snapshot', []) - await token.transfer(account.address, parseEther('1')) - await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - - const currentCachedPrice = await paymaster.cachedPrice() - assert.equal((currentCachedPrice as any) / (priceDenominator as any), 0.2) - const overrideTokenPrice = priceDenominator.mul(132).div(1000) - - let op = await fillUserOp({ - sender: account.address, - paymaster: paymasterAddress, - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: 3e5, - paymasterData: hexZeroPad(hexlify(overrideTokenPrice), 32), - callData - }, entryPoint) - op = signUserOp(op, accountOwner, entryPoint.address, chainId) - const opPacked = packUserOp(op) - - // for simpler 'gasPrice()' calculation - await ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', [utils.hexlify(op.maxFeePerGas)]) - const tx = await entryPoint - .handleOps([opPacked], beneficiaryAddress, { - gasLimit: 1e7, - maxFeePerGas: op.maxFeePerGas, - maxPriorityFeePerGas: op.maxFeePerGas - }) - .then(async tx => await tx.wait()) - - const decodedLogs = tx.logs.map(it => { - return testInterface.parseLog(it) - }) - - const preChargeTokens = decodedLogs[0].args.value - const requiredGas = BigNumber.from(op.callGasLimit).add(BigNumber.from(op.verificationGasLimit).add(BigNumber.from(op.paymasterVerificationGasLimit))).add(BigNumber.from(op.paymasterPostOpGasLimit)).add(op.preVerificationGas).add(40000 /* REFUND_POSTOP_COST */) - const requiredPrefund = requiredGas.mul(op.maxFeePerGas) - const preChargeTokenPrice = requiredPrefund.mul(priceDenominator).div(preChargeTokens) - - // TODO: div 1e10 to hide rounding errors. look into it - 1e10 is too much. - assert.equal(preChargeTokenPrice.div(1e10).toString(), overrideTokenPrice.div(1e10).toString()) - await ethers.provider.send('evm_revert', [snapshot]) - }) - - it('should use cached token price if the one supplied by the client is worse', async function () { - const snapshot = await ethers.provider.send('evm_snapshot', []) - await token.transfer(account.address, parseEther('1')) - await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - - const currentCachedPrice = await paymaster.cachedPrice() - assert.equal((currentCachedPrice as any) / (priceDenominator as any), 0.2) - // note: higher number is lower token price - const overrideTokenPrice = priceDenominator.mul(50) - let op = await fillUserOp({ - sender: account.address, - maxFeePerGas: 1000000000, - paymaster: paymasterAddress, - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: 3e5, - paymasterData: hexZeroPad(hexlify(overrideTokenPrice), 32), - callData - }, entryPoint) - op = signUserOp(op, accountOwner, entryPoint.address, chainId) - const opPacked = packUserOp(op) - - // for simpler 'gasPrice()' calculation - await ethers.provider.send('hardhat_setNextBlockBaseFeePerGas', [utils.hexlify(op.maxFeePerGas)]) - const tx = await entryPoint - .handleOps([opPacked], beneficiaryAddress, { - gasLimit: 1e7, - maxFeePerGas: op.maxFeePerGas, - maxPriorityFeePerGas: op.maxFeePerGas - }) - .then(async tx => await tx.wait()) - - const decodedLogs = tx.logs.map(it => { - return testInterface.parseLog(it) - }) - - const preChargeTokens = decodedLogs[0].args.value - const requiredGas = BigNumber.from(op.callGasLimit).add(BigNumber.from(op.verificationGasLimit).add(BigNumber.from(op.paymasterVerificationGasLimit))).add(BigNumber.from(op.paymasterPostOpGasLimit)).add(op.preVerificationGas).add(40000 /* REFUND_POSTOP_COST */) - const requiredPrefund = requiredGas.mul(op.maxFeePerGas) - const preChargeTokenPrice = requiredPrefund.mul(priceDenominator).div(preChargeTokens) - - assert.equal(preChargeTokenPrice.toString(), currentCachedPrice.mul(10).div(15).toString()) - await ethers.provider.send('evm_revert', [snapshot]) - }) - - it('should charge the overdraft tokens if the pre-charge ended up lower than the final transaction cost', async function () { - const snapshot = await ethers.provider.send('evm_snapshot', []) - await token.transfer(account.address, await token.balanceOf(await ethersSigner.getAddress())) - await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - - // Ether price increased 100 times! - await tokenOracle.setPrice(initialPriceToken) - await nativeAssetOracle.setPrice(initialPriceEther * 100) - // Cannot happen too fast though - await ethers.provider.send('evm_increaseTime', [200]) - - let op = await fillUserOp({ - sender: account.address, - paymaster: paymasterAddress, - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: 3e5, - callData - }, entryPoint) - op = signUserOp(op, accountOwner, entryPoint.address, chainId) - const opPacked = packUserOp(op) - const tx = await entryPoint - .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) - .then(async tx => await tx.wait()) - - const decodedLogs = tx.logs.map(it => { - return testInterface.parseLog(it) - }) - - const preChargeTokens = decodedLogs[0].args.value - const overdraftTokens = decodedLogs[3].args.value - const actualTokenCharge = decodedLogs[4].args.actualTokenCharge - // Checking that both 'Transfers' are from account to Paymaster - assert.equal(decodedLogs[0].args.from, decodedLogs[3].args.from) - assert.equal(decodedLogs[0].args.to, decodedLogs[3].args.to) - - assert.equal(preChargeTokens.add(overdraftTokens).toString(), actualTokenCharge.toString()) - - const userOpSuccess = decodedLogs[5].args.success - assert.equal(userOpSuccess, true) - await ethers.provider.send('evm_revert', [snapshot]) - }) - - it('should revert in the first postOp run if the pre-charge ended up lower than the final transaction cost but the client has no tokens to cover the overdraft', async function () { - const snapshot = await ethers.provider.send('evm_snapshot', []) - - // Make sure account has small amount of tokens - await token.transfer(account.address, parseEther('0.01')) - await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - - // Ether price increased 100 times! - await tokenOracle.setPrice(initialPriceToken) - await nativeAssetOracle.setPrice(initialPriceEther * 100) - // Cannot happen too fast though - await ethers.provider.send('evm_increaseTime', [200]) - - // Withdraw most of the tokens the account hs inside the inner transaction - const withdrawTokensCall = await token.populateTransaction.transfer(token.address, parseEther('0.009')).then(tx => tx.data!) - const callData = await account.populateTransaction.execute(token.address, 0, withdrawTokensCall).then(tx => tx.data!) - - let op = await fillUserOp({ - sender: account.address, - paymaster: paymasterAddress, - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: 3e5, - callData - }, entryPoint) - op = signUserOp(op, accountOwner, entryPoint.address, chainId) - const opPacked = packUserOp(op) - const tx = await entryPoint - .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) - .then(async tx => await tx.wait()) - - const decodedLogs = tx.logs.map(it => { - return testInterface.parseLog(it) - }) - console.log(decodedLogs.map((e: any) => ({ ev: e.name, ...objdump(e.args!) }))) - - const postOpRevertReason = decodeRevertReason(decodedLogs[2].args.revertReason) - assert.include(postOpRevertReason, 'PostOpReverted(ERC20InsufficientBalance') - const userOpSuccess = decodedLogs[3].args.success - assert.equal(userOpSuccess, false) - assert.equal(decodedLogs.length, 4) - await ethers.provider.send('evm_revert', [snapshot]) - }) - - it('should swap tokens for ether if it falls below configured value and deposit it', async function () { - await token.transfer(account.address, await token.balanceOf(await ethersSigner.getAddress())) - await token.sudoApprove(account.address, paymaster.address, ethers.constants.MaxUint256) - - const depositInfo = await entryPoint.deposits(paymaster.address) - await paymaster.withdrawTo(account.address, depositInfo.deposit) - - // deposit exactly the minimum amount so the next UserOp makes it go under - await entryPoint.depositTo(paymaster.address, { value: minEntryPointBalance }) - - let op = await fillUserOp({ - sender: account.address, - paymaster: paymasterAddress, - paymasterVerificationGasLimit: 3e5, - paymasterPostOpGasLimit: 3e5, - callData - }, entryPoint) - op = signUserOp(op, accountOwner, entryPoint.address, chainId) - const opPacked = packUserOp(op) - const tx = await entryPoint - .handleOps([opPacked], beneficiaryAddress, { gasLimit: 1e7 }) - .then(async tx => await tx.wait()) - const decodedLogs = tx.logs.map(it => { - return testInterface.parseLog(it) - }) - - // note: it is hard to deploy Uniswap on hardhat - so stubbing it for the unit test - assert.equal(decodedLogs[4].name, 'StubUniswapExchangeEvent') - assert.equal(decodedLogs[8].name, 'Received') - assert.equal(decodedLogs[9].name, 'Deposited') - const deFactoExchangeRate = decodedLogs[4].args.amountOut.toString() / decodedLogs[4].args.amountIn.toString() - const expectedPrice = initialPriceToken / initialPriceEther - assert.closeTo(deFactoExchangeRate, expectedPrice, 0.001) - }) -}) diff --git a/test/verifying_paymaster.test.ts b/test/verifying_paymaster.test.ts deleted file mode 100644 index 49a449a7..00000000 --- a/test/verifying_paymaster.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Wallet } from 'ethers' -import { ethers } from 'hardhat' -import { expect } from 'chai' -import { - EntryPoint, - SimpleAccount, - VerifyingPaymaster, - VerifyingPaymaster__factory -} from '../typechain' -import { - AddressZero, - createAccount, - createAccountOwner, createAddress, decodeRevertReason, - deployEntryPoint, packPaymasterData, parseValidationData -} from './testutils' -import { DefaultsForUserOp, fillAndSign, fillSignAndPack, packUserOp, simulateValidation } from './UserOp' -import { arrayify, defaultAbiCoder, hexConcat, parseEther } from 'ethers/lib/utils' -import { PackedUserOperation } from './UserOperation' - -const MOCK_VALID_UNTIL = '0x00000000deadbeef' -const MOCK_VALID_AFTER = '0x0000000000001234' -const MOCK_SIG = '0x1234' - -describe('EntryPoint with VerifyingPaymaster', function () { - let entryPoint: EntryPoint - let accountOwner: Wallet - const ethersSigner = ethers.provider.getSigner() - let account: SimpleAccount - let offchainSigner: Wallet - - let paymaster: VerifyingPaymaster - before(async function () { - this.timeout(20000) - entryPoint = await deployEntryPoint() - - offchainSigner = createAccountOwner() - accountOwner = createAccountOwner() - - paymaster = await new VerifyingPaymaster__factory(ethersSigner).deploy(entryPoint.address, offchainSigner.address) - await paymaster.addStake(1, { value: parseEther('2') }) - await entryPoint.depositTo(paymaster.address, { value: parseEther('1') }); - ({ proxy: account } = await createAccount(ethersSigner, accountOwner.address, entryPoint.address)) - }) - - describe('#parsePaymasterAndData', () => { - it('should parse data properly', async () => { - const paymasterAndData = packPaymasterData( - paymaster.address, - DefaultsForUserOp.paymasterVerificationGasLimit, - DefaultsForUserOp.paymasterPostOpGasLimit, - hexConcat([ - defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), MOCK_SIG - ]) - ) - console.log(paymasterAndData) - const res = await paymaster.parsePaymasterAndData(paymasterAndData) - // console.log('MOCK_VALID_UNTIL, MOCK_VALID_AFTER', MOCK_VALID_UNTIL, MOCK_VALID_AFTER) - // console.log('validUntil after', res.validUntil, res.validAfter) - // console.log('MOCK SIG', MOCK_SIG) - // console.log('sig', res.signature) - expect(res.validUntil).to.be.equal(ethers.BigNumber.from(MOCK_VALID_UNTIL)) - expect(res.validAfter).to.be.equal(ethers.BigNumber.from(MOCK_VALID_AFTER)) - expect(res.signature).equal(MOCK_SIG) - }) - }) - - describe('#validatePaymasterUserOp', () => { - it('should reject on no signature', async () => { - const userOp = await fillSignAndPack({ - sender: account.address, - paymaster: paymaster.address, - paymasterData: hexConcat([defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x1234']) - }, accountOwner, entryPoint) - expect(await simulateValidation(userOp, entryPoint.address) - .catch(e => decodeRevertReason(e))) - .to.include('invalid signature length in paymasterAndData') - }) - - it('should reject on invalid signature', async () => { - const userOp = await fillSignAndPack({ - sender: account.address, - paymaster: paymaster.address, - paymasterData: hexConcat( - [defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x' + '00'.repeat(65)]) - }, accountOwner, entryPoint) - expect(await simulateValidation(userOp, entryPoint.address) - .catch(e => decodeRevertReason(e))) - .to.include('ECDSAInvalidSignature') - }) - - describe('with wrong signature', () => { - let wrongSigUserOp: PackedUserOperation - const beneficiaryAddress = createAddress() - before(async () => { - const sig = await offchainSigner.signMessage(arrayify('0xdead')) - wrongSigUserOp = await fillSignAndPack({ - sender: account.address, - paymaster: paymaster.address, - paymasterData: hexConcat([defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), sig]) - }, accountOwner, entryPoint) - }) - - it('should return signature error (no revert) on wrong signer signature', async () => { - const ret = await simulateValidation(wrongSigUserOp, entryPoint.address) - expect(parseValidationData(ret.returnInfo.paymasterValidationData).aggregator).to.match(/0x0*1$/) - }) - - it('handleOp revert on signature failure in handleOps', async () => { - await expect(entryPoint.estimateGas.handleOps([wrongSigUserOp], beneficiaryAddress)).to.revertedWith('AA34 signature error') - }) - }) - - it('succeed with valid signature', async () => { - const userOp1 = await fillAndSign({ - sender: account.address, - paymaster: paymaster.address, - paymasterData: hexConcat( - [defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), '0x' + '00'.repeat(65)]) - }, accountOwner, entryPoint) - const hash = await paymaster.getHash(packUserOp(userOp1), MOCK_VALID_UNTIL, MOCK_VALID_AFTER) - const sig = await offchainSigner.signMessage(arrayify(hash)) - const userOp = await fillSignAndPack({ - ...userOp1, - paymaster: paymaster.address, - paymasterData: hexConcat([defaultAbiCoder.encode(['uint48', 'uint48'], [MOCK_VALID_UNTIL, MOCK_VALID_AFTER]), sig]) - }, accountOwner, entryPoint) - const res = await simulateValidation(userOp, entryPoint.address) - const validationData = parseValidationData(res.returnInfo.paymasterValidationData) - expect(validationData).to.eql({ - aggregator: AddressZero, - validAfter: parseInt(MOCK_VALID_AFTER), - validUntil: parseInt(MOCK_VALID_UNTIL) - }) - }) - }) -})