diff --git a/.github/workflows/check_branch_name.yml b/.github/workflows/check_branch_name.yml new file mode 100644 index 0000000..ffa9904 --- /dev/null +++ b/.github/workflows/check_branch_name.yml @@ -0,0 +1,18 @@ +name: Check Branch Name +on: + create: + +jobs: + check-branch-name: + runs-on: ubuntu-latest + if: github.event.ref_type == 'branch' && !contains(github.ref, 'refs/heads/main') && !contains(github.ref, 'refs/heads/develop') + steps: + - name: Check Branch Name + run: | + BRANCH_PREFIX_REGEX="^(features/|fixes/|releases/).+" + BRANCH_NAME=${GITHUB_REF#refs/heads/} + if [[ ! $BRANCH_NAME =~ $BRANCH_PREFIX_REGEX ]]; then + echo "Invalid branch name: $BRANCH_NAME" + echo "Branch name must start with 'features/', 'fixes/', or 'releases/'." + exit 1 + fi diff --git a/.gitignore b/.gitignore index 0028d08..79807b2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ forge-cache/ typechain-types typechain typings +yarn-error.log #Hardhat files cache diff --git a/DerivedPriceFeedFlat.sol b/DerivedPriceFeedFlat.sol deleted file mode 100644 index d73d003..0000000 --- a/DerivedPriceFeedFlat.sol +++ /dev/null @@ -1,149 +0,0 @@ -// Sources flattened with hardhat v2.14.1 https://hardhat.org - -// File @chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol@v0.6.1 - -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface AggregatorV3Interface { - function decimals() external view returns (uint8); - - function description() external view returns (string memory); - - function version() external view returns (uint256); - - function getRoundData(uint80 _roundId) - external - view - returns ( - uint80 roundId, - int256 answer, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ); - - function latestRoundData() - external - view - returns ( - uint80 roundId, - int256 answer, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ); -} - - -// File contracts/token/oracles/DerivedPriceFeed.sol - -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -error MismatchInBaseAndQuoteDecimals(); -error InvalidPriceFromRound(); -error LatestRoundIncomplete(); -error PriceFeedStale(); -error OracleAddressCannotBeZero(); - -contract DerivedPriceFeed { - // price is native-per-dollar - AggregatorV3Interface internal nativeOracle; - // price is tokens-per-dollar - AggregatorV3Interface internal tokenOracle; - - string internal DESCRIPTION; - - constructor( - address _nativeOracleAddress, - address _tokenOracleAddress, - string memory _description - ) { - if (_nativeOracleAddress == address(0)) - revert OracleAddressCannotBeZero(); - if (_tokenOracleAddress == address(0)) - revert OracleAddressCannotBeZero(); - nativeOracle = AggregatorV3Interface(_nativeOracleAddress); - tokenOracle = AggregatorV3Interface(_tokenOracleAddress); - - // If either of the base or quote price feeds have mismatch in decimal then it could be a problem, so throw! - uint8 decimals1 = nativeOracle.decimals(); - uint8 decimals2 = tokenOracle.decimals(); - if (decimals1 != decimals2) revert MismatchInBaseAndQuoteDecimals(); - - DESCRIPTION = _description; - } - - function decimals() public view returns (uint8) { - return 18; - } - - function description() public view returns (string memory) { - return DESCRIPTION; - } - - function validateRound( - uint80 roundId, - int256 price, - uint256 updatedAt, - uint80 answeredInRound, - uint256 staleFeedThreshold - ) internal view { - if (price <= 0) revert InvalidPriceFromRound(); - // 2 days old price is considered stale since the price is updated every 24 hours - if (updatedAt < block.timestamp - staleFeedThreshold) - revert PriceFeedStale(); - if (updatedAt == 0) revert LatestRoundIncomplete(); - if (answeredInRound < roundId) revert PriceFeedStale(); - } - - function getThePrice() public view returns (int) { - /** - * Returns the latest price of price feed 1 - */ - - ( - uint80 roundID1, - int256 price1, - , - uint256 updatedAt1, - uint80 answeredInRound1 - ) = nativeOracle.latestRoundData(); - - // By default 2 days old price is considered stale BUT it may vary per price feed - // comapred to stable coin feeds this may have different heartbeat - validateRound( - roundID1, - price1, - updatedAt1, - answeredInRound1, - 60 * 60 * 24 * 2 - ); - - /** - * Returns the latest price of price feed 2 - */ - - ( - uint80 roundID2, - int256 price2, - , - uint256 updatedAt2, - uint80 answeredInRound2 - ) = tokenOracle.latestRoundData(); - - // By default 2 days old price is considered stale BUT it may vary per price feed - validateRound( - roundID2, - price2, - updatedAt2, - answeredInRound2, - 60 * 60 * 24 * 2 - ); - - // Always using decimals 18 for derived price feeds - int token_native = (price2 * (10 ** 18)) / price1; - return token_native; - } -} diff --git a/contracts/BasePaymaster.sol b/contracts/BasePaymaster.sol index 89a0f1f..c63e2c3 100644 --- a/contracts/BasePaymaster.sol +++ b/contracts/BasePaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; +pragma solidity ^0.8.20; /* solhint-disable reason-string */ diff --git a/contracts/common/Errors.sol b/contracts/common/Errors.sol index 2b758c2..2793af8 100644 --- a/contracts/common/Errors.sol +++ b/contracts/common/Errors.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-only -pragma solidity 0.8.17; +pragma solidity ^0.8.20; contract BasePaymasterErrors { /** @@ -20,6 +20,11 @@ contract VerifyingPaymasterErrors { */ error VerifyingSignerCannotBeZero(); + /** + * @notice Throws when the fee collector address provided is address(0) + */ + error FeeCollectorCannotBeZero(); + /** * @notice Throws when the paymaster address provided is address(0) */ diff --git a/contracts/deployer/Create3.sol b/contracts/deployer/Create3.sol index 89f1168..7d32989 100644 --- a/contracts/deployer/Create3.sol +++ b/contracts/deployer/Create3.sol @@ -1,5 +1,5 @@ //SPDX-License-Identifier: Unlicense -pragma solidity 0.8.17; +pragma solidity ^0.8.20; /** @title A library for deploying contracts EIP-3171 style. diff --git a/contracts/deployer/Deployer.sol b/contracts/deployer/Deployer.sol index 7571f00..a5b8338 100644 --- a/contracts/deployer/Deployer.sol +++ b/contracts/deployer/Deployer.sol @@ -1,5 +1,5 @@ //SPDX-License-Identifier: Unlicense -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "./Create3.sol"; diff --git a/contracts/interfaces/IWETH9.sol b/contracts/interfaces/IWETH9.sol index 26759eb..6f40ece 100644 --- a/contracts/interfaces/IWETH9.sol +++ b/contracts/interfaces/IWETH9.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/contracts/interfaces/paymasters/IVerifyingSingletonPaymaster.sol b/contracts/interfaces/paymasters/IVerifyingSingletonPaymaster.sol new file mode 100644 index 0000000..86825f7 --- /dev/null +++ b/contracts/interfaces/paymasters/IVerifyingSingletonPaymaster.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IVerifyingSingletonPaymaster { + event EPGasOverheadChanged( + uint256 indexed _oldValue, + uint256 indexed _newValue + ); + + event FixedPriceMarkupChanged( + uint32 indexed _oldValue, + uint32 indexed _newValue + ); + + event VerifyingSignerChanged( + address indexed _oldSigner, + address indexed _newSigner, + address indexed _actor + ); + + event FeeCollectorChanged( + address indexed _oldFeeCollector, + address indexed _newFeeCollector, + address indexed _actor + ); + event GasDeposited(address indexed _paymasterId, uint256 indexed _value); + event GasWithdrawn( + address indexed _paymasterId, + address indexed _to, + uint256 indexed _value + ); + event GasBalanceDeducted( + address indexed _paymasterId, + uint256 indexed _charge + ); + event PremiumCollected( + address indexed _paymasterId, + uint256 indexed _premium + ); + + /** + * @dev Returns the current balance of the paymasterId(aka fundingId) + * @param paymasterId The address of the paymasterId + */ + function getBalance( + address paymasterId + ) external view returns (uint256 balance); + + /** + * @dev updates the verifyingSigner address + * @param _newVerifyingSigner The new verifyingSigner address + */ + function setSigner(address _newVerifyingSigner) external payable; + + /** + * @dev updates the postOp + unacocunted gas overhead + * @param value The new value + */ + function setUnaccountedEPGasOverhead(uint256 value) external payable; +} diff --git a/contracts/libs/MathLib.sol b/contracts/libs/MathLib.sol new file mode 100644 index 0000000..87480fb --- /dev/null +++ b/contracts/libs/MathLib.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.20; + +library MathLib { + function minuint256( + uint256 a, + uint256 b + ) internal pure returns (uint256 result) { + assembly { + result := xor(b, mul(xor(b, a), gt(a, b))) + } + } + + function maxuint256( + uint256 a, + uint256 b + ) internal pure returns (uint256 result) { + assembly { + result := xor(a, mul(xor(a, b), gt(b, a))) + } + } + + function minuint32( + uint32 a, + uint32 b + ) internal pure returns (uint32 result) { + assembly { + result := xor(b, mul(xor(b, a), gt(a, b))) + } + } + + function maxuint32( + uint32 a, + uint32 b + ) internal pure returns (uint32 result) { + assembly { + result := xor(a, mul(xor(a, b), gt(b, a))) + } + } +} diff --git a/contracts/references/AdvancedVerifyingPaymaster.sol b/contracts/references/AdvancedVerifyingPaymaster.sol index 9872277..b5d8e5b 100644 --- a/contracts/references/AdvancedVerifyingPaymaster.sol +++ b/contracts/references/AdvancedVerifyingPaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; diff --git a/contracts/references/infinitism/OracleHelper.sol b/contracts/references/infinitism/OracleHelper.sol index 8fccf45..5d16ca3 100644 --- a/contracts/references/infinitism/OracleHelper.sol +++ b/contracts/references/infinitism/OracleHelper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; +pragma solidity ^0.8.20; /* solhint-disable not-rely-on-time */ diff --git a/contracts/references/infinitism/SampleTokenPaymaster.sol b/contracts/references/infinitism/SampleTokenPaymaster.sol index b227c2f..84162c2 100644 --- a/contracts/references/infinitism/SampleTokenPaymaster.sol +++ b/contracts/references/infinitism/SampleTokenPaymaster.sol @@ -13,6 +13,8 @@ import "./OracleHelper.sol"; // OG GSN same correlation https://github.com/opengsn/gsn/blob/master/packages/paymasters/contracts/TokenPaymaster.sol +// TODO: Update with latest contract changes and make required modifications / variations + // TODO: note https://github.com/pimlicolabs/erc20-paymaster-contracts/issues/10 // TODO: set a hard limit on how much gas a single user op may cost (postOp to fix the price) /// @title Sample ERC-20 Token Paymaster for ERC-4337 diff --git a/contracts/references/infinitism/UniswapHelper.sol b/contracts/references/infinitism/UniswapHelper.sol index 6c0a19e..77c5c29 100644 --- a/contracts/references/infinitism/UniswapHelper.sol +++ b/contracts/references/infinitism/UniswapHelper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; +pragma solidity ^0.8.20; /* solhint-disable not-rely-on-time */ diff --git a/contracts/references/pimlico/IOracle.sol b/contracts/references/pimlico/IOracle.sol index 43a4c5b..5d49f7a 100644 --- a/contracts/references/pimlico/IOracle.sol +++ b/contracts/references/pimlico/IOracle.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.20; interface IOracle { function decimals() external view returns (uint8); diff --git a/contracts/references/pimlico/PimlicoERC20Paymaster.sol b/contracts/references/pimlico/PimlicoERC20Paymaster.sol index e82a371..601c82c 100644 --- a/contracts/references/pimlico/PimlicoERC20Paymaster.sol +++ b/contracts/references/pimlico/PimlicoERC20Paymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; +pragma solidity ^0.8.20; // Import the required libraries and contracts import "@account-abstraction/contracts/core/BasePaymaster.sol"; diff --git a/contracts/references/soul/IPriceOracle.sol b/contracts/references/soul/IPriceOracle.sol index f2a6c63..27e2e1a 100644 --- a/contracts/references/soul/IPriceOracle.sol +++ b/contracts/references/soul/IPriceOracle.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; interface IPriceOracle { function exchangePrice( diff --git a/contracts/references/soul/ITokenPaymaster.sol b/contracts/references/soul/ITokenPaymaster.sol index 4ae128b..64fdc06 100644 --- a/contracts/references/soul/ITokenPaymaster.sol +++ b/contracts/references/soul/ITokenPaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; import "@account-abstraction/contracts/interfaces/IPaymaster.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; diff --git a/contracts/references/soul/PriceOracle.sol b/contracts/references/soul/PriceOracle.sol index 3ca70d7..44b6b4b 100644 --- a/contracts/references/soul/PriceOracle.sol +++ b/contracts/references/soul/PriceOracle.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; import "./IPriceOracle.sol"; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; diff --git a/contracts/references/soul/TokenPaymaster.sol b/contracts/references/soul/TokenPaymaster.sol index 258ade5..00561c9 100644 --- a/contracts/references/soul/TokenPaymaster.sol +++ b/contracts/references/soul/TokenPaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; import "./ITokenPaymaster.sol"; diff --git a/contracts/test/accounts/BiconomyAccountFactory.sol b/contracts/test/accounts/BiconomyAccountFactory.sol index 9a3e532..4fefb0d 100644 --- a/contracts/test/accounts/BiconomyAccountFactory.sol +++ b/contracts/test/accounts/BiconomyAccountFactory.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity 0.8.17; // temp // Could also use published package or added submodule. +// temp import {SmartAccountFactory} from "@biconomy-devx/account-contracts-v2/contracts/smart-account/factory/SmartAccountFactory.sol"; contract BiconomyAccountFactory is SmartAccountFactory { diff --git a/contracts/test/accounts/SimpleAccount.sol b/contracts/test/accounts/SimpleAccount.sol index 6674984..3bc4476 100644 --- a/contracts/test/accounts/SimpleAccount.sol +++ b/contracts/test/accounts/SimpleAccount.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; +pragma solidity ^0.8.20; /* solhint-disable avoid-low-level-calls */ /* solhint-disable no-inline-assembly */ diff --git a/contracts/test/dex/UniV3Integration.sol b/contracts/test/dex/UniV3Integration.sol index e1dfc56..311d146 100644 --- a/contracts/test/dex/UniV3Integration.sol +++ b/contracts/test/dex/UniV3Integration.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; diff --git a/contracts/test/dex/UniswapV3SwapExamples.sol b/contracts/test/dex/UniswapV3SwapExamples.sol index 61ba67b..f182eab 100644 --- a/contracts/test/dex/UniswapV3SwapExamples.sol +++ b/contracts/test/dex/UniswapV3SwapExamples.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; // swap reference: https://solidity-by-example.org/defi/uniswap-v3-swap/ for adding tests // https://docs.uniswap.org/contracts/v3/reference/periphery/SwapRouter diff --git a/contracts/test/helpers/MockChainlinkAggregator.sol b/contracts/test/helpers/MockChainlinkAggregator.sol index b1f57ff..3c1b9e3 100644 --- a/contracts/test/helpers/MockChainlinkAggregator.sol +++ b/contracts/test/helpers/MockChainlinkAggregator.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; import "../../token/oracles/IOracleAggregator.sol"; diff --git a/contracts/test/helpers/MockPriceFeed.sol b/contracts/test/helpers/MockPriceFeed.sol index 493335b..c0a8472 100644 --- a/contracts/test/helpers/MockPriceFeed.sol +++ b/contracts/test/helpers/MockPriceFeed.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; diff --git a/contracts/test/helpers/MockStalePriceFeed.sol b/contracts/test/helpers/MockStalePriceFeed.sol index 6be8c65..1dd92e4 100644 --- a/contracts/test/helpers/MockStalePriceFeed.sol +++ b/contracts/test/helpers/MockStalePriceFeed.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; diff --git a/contracts/test/helpers/MockToken.sol b/contracts/test/helpers/MockToken.sol index 876f755..ba8b7e3 100644 --- a/contracts/test/helpers/MockToken.sol +++ b/contracts/test/helpers/MockToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/contracts/token/BICOPaymaster.sol b/contracts/token/BICOPaymaster.sol new file mode 100644 index 0000000..e69de29 diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 62f5efe..1d4bb5e 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; @@ -194,7 +194,6 @@ contract BiconomyTokenPaymaster is function setUnaccountedEPGasOverhead( uint256 _newOverheadCost ) external payable onlyOwner { - // review if this could be high value in case of arbitrum if (_newOverheadCost > 200000) revert CannotBeUnrealisticValue(); uint256 oldValue = UNACCOUNTED_COST; assembly ("memory-safe") { diff --git a/contracts/token/BiconomyTokenPaymasterV2.sol b/contracts/token/BiconomyTokenPaymasterV2.sol new file mode 100644 index 0000000..dadaccd --- /dev/null +++ b/contracts/token/BiconomyTokenPaymasterV2.sol @@ -0,0 +1,632 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; +import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; +import {UserOperationLib} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {BasePaymaster} from "../BasePaymaster.sol"; +import {IOracleAggregator} from "./oracles/IOracleAggregator.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@account-abstraction/contracts/core/Helpers.sol" as Helpers; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import "../utils/SafeTransferLib.sol"; +import {TokenPaymasterErrors} from "./TokenPaymasterErrors.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +// Biconomy Token Paymaster +/** + * A token-based paymaster that allows user to pay gas fee in ERC20 tokens. The paymaster owner chooses which tokens to accept. + * The payment manager (usually the owner) first deposits native gas into the EntryPoint. Then, for each transaction, it takes the gas fee from the user's ERC20 token balance. + * The manager must convert these collected tokens back to native gas and deposit it into the EntryPoint to keep the system running. + * It is an extension of VerifyingPaymaster which trusts external signer to authorize the transaction, but also with an ability to withdraw tokens. + * + * The validatePaymasterUserOp function does not interact with external contracts but uses an externally provided exchange rate. + * Based on the exchangeRate and requiredPrefund amount, the validation method checks if the user's account has enough token balance. This is done by only looking at the referenced storage. + * All Withdrawn tokens are sent to a dynamic fee receiver address. + * + * Optionally a safe guard deposit may be used in future versions. + */ +contract BiconomyTokenPaymasterV2 is + BasePaymaster, + ReentrancyGuard, + TokenPaymasterErrors +{ + using ECDSA for bytes32; + using Address for address; + using UserOperationLib for UserOperation; + + /** + * price source can be off-chain calculation or oracles + * for oracle based it can be based on chainlink feeds or TWAP oracles + * for ORACLE_BASED oracle aggregator address has to be passed in paymasterAndData + */ + enum ExchangeRateSource { + EXTERNAL_EXCHANGE_RATE, + ORACLE_BASED + } + + // 1. use mode and based on mode treat uint256 fee sent either as priceMarkup or flatFee + // 2. (no mode required) add extra value in paymasterandData so uint32 markup and uint224 flatFee both can be parsed + // 3. (no mode required) without extra value treat uint256 as packed uint32uint224 and use values accordingly + /*enum FeePremiumMode { + PERCENTAGE, + FLAT + }*/ + + /// @notice All 'price' variable coming from outside are expected to be multiple of 1e6, and in actual calculation, + /// final value is divided by PRICE_DENOMINATOR to avoid rounding up. + uint32 private constant PRICE_DENOMINATOR = 1e6; + + // Gas used in EntryPoint._handlePostOp() method (including this#postOp() call) + uint256 public UNACCOUNTED_COST = 45000; // TBD + + // Always rely on verifyingSigner.. + address public verifyingSigner; + + // receiver of withdrawn fee tokens + address public feeReceiver; + + // paymasterAndData: concat of [paymasterAddress(address), priceSource(enum 1 byte), abi.encode(validUntil, validAfter, feeToken, oracleAggregator, exchangeRate, priceMarkup): makes up 32*6 bytes, signature] + // PND offset is used to indicate offsets to decode, used along with Signature offset + uint256 private constant VALID_PND_OFFSET = 21; + + uint256 private constant SIGNATURE_OFFSET = 213; + + address private constant NATIVE_ADDRESS = + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + /** + * Designed to enable the community to track change in storage variable UNACCOUNTED_COST which is used + * to maintain gas execution cost which can't be calculated within contract*/ + event EPGasOverheadChanged( + uint256 indexed _oldOverheadCost, + uint256 indexed _newOverheadCost, + address indexed _actor + ); + + /** + * Designed to enable the community to track change in storage variable verifyingSigner which is used + * to authorize any operation for this paymaster (validation stage) and provides signature*/ + event VerifyingSignerChanged( + address indexed _oldSigner, + address indexed _newSigner, + address indexed _actor + ); + + /** + * Designed to enable the community to track change in storage variable feeReceiver which is an address (self or other SCW/EOA) + * responsible for collecting all the tokens being withdrawn as fees*/ + event FeeReceiverChanged( + address indexed _oldfeeReceiver, + address indexed _newfeeReceiver, + address indexed _actor + ); + + /** + * Designed to enable tracking how much fees were charged from the sender and in which ERC20 token + * More information can be emitted like exchangeRate used, what was the source of exchangeRate etc*/ + // priceMarkup = Multiplier value to calculate markup, 1e6 means 1x multiplier = No markup + event TokenPaymasterOperation( + address indexed sender, + address indexed token, + uint256 indexed totalCharge, + address oracleAggregator, + uint32 priceMarkup, + bytes32 userOpHash, + uint256 exchangeRate, + ExchangeRateSource priceSource + ); + + /** + * Notify in case paymaster failed to withdraw tokens from sender + */ + event TokenPaymentDue( + address indexed token, + address indexed account, + uint256 indexed charge + ); + + event Received(address indexed sender, uint256 value); + + constructor( + address _owner, + IEntryPoint _entryPoint, + address _verifyingSigner + ) payable BasePaymaster(_owner, _entryPoint) { + if (_owner == address(0)) revert OwnerCannotBeZero(); + if (address(_entryPoint) == address(0)) revert EntryPointCannotBeZero(); + if (_verifyingSigner == address(0)) + revert VerifyingSignerCannotBeZero(); + assembly ("memory-safe") { + sstore(verifyingSigner.slot, _verifyingSigner) + sstore(feeReceiver.slot, address()) // initialize with self (could also be _owner) + } + } + + /** + * @dev Set a new verifying signer address. + * Can only be called by the owner of the contract. + * @param _newVerifyingSigner The new address to be set as the verifying signer. + * @notice If _newVerifyingSigner is set to zero address, it will revert with an error. + * After setting the new signer address, it will emit an event VerifyingSignerChanged. + */ + function setVerifyingSigner( + address _newVerifyingSigner + ) external payable onlyOwner { + if (_newVerifyingSigner == address(0)) + revert VerifyingSignerCannotBeZero(); + address oldSigner = verifyingSigner; + assembly ("memory-safe") { + sstore(verifyingSigner.slot, _newVerifyingSigner) + } + emit VerifyingSignerChanged(oldSigner, _newVerifyingSigner, msg.sender); + } + + // marked for removal + /** + * @dev Set a new fee receiver. + * Can only be called by the owner of the contract. + * @param _newFeeReceiver The new address to be set as the address of new fee receiver. + * @notice If _newFeeReceiver is set to zero address, it will revert with an error. + * After setting the new address, it will emit an event FeeReceiverChanged. + */ + function setFeeReceiver( + address _newFeeReceiver + ) external payable onlyOwner { + if (_newFeeReceiver == address(0)) revert FeeReceiverCannotBeZero(); + address oldFeeReceiver = feeReceiver; + assembly ("memory-safe") { + sstore(feeReceiver.slot, _newFeeReceiver) + } + emit FeeReceiverChanged(oldFeeReceiver, _newFeeReceiver, msg.sender); + } + + /** + * @dev Set a new overhead for unaccounted cost + * Can only be called by the owner of the contract. + * @param _newOverheadCost The new value to be set as the gas cost overhead. + * @notice If _newOverheadCost is set to very high value, it will revert with an error. + * After setting the new value, it will emit an event EPGasOverheadChanged. + */ + function setUnaccountedEPGasOverhead( + uint256 _newOverheadCost + ) external payable onlyOwner { + // review if this could be high value in case of arbitrum + if (_newOverheadCost > 200000) revert CannotBeUnrealisticValue(); + uint256 oldValue = UNACCOUNTED_COST; + assembly ("memory-safe") { + sstore(UNACCOUNTED_COST.slot, _newOverheadCost) + } + emit EPGasOverheadChanged(oldValue, _newOverheadCost, msg.sender); + } + + /** + * Add a deposit in native currency for this paymaster, used for paying for transaction fees. + * This is ideally done by the entity who is managing the received ERC20 gas tokens. + */ + function deposit() public payable virtual override nonReentrant { + IEntryPoint(entryPoint).depositTo{value: msg.value}(address(this)); + } + + /** + * @dev Withdraws the specified amount of gas tokens from the paymaster's balance and transfers them to the specified address. + * @param withdrawAddress The address to which the gas tokens should be transferred. + * @param amount The amount of gas tokens to withdraw. + */ + function withdrawTo( + address payable withdrawAddress, + uint256 amount + ) public override onlyOwner nonReentrant { + if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress(); + entryPoint.withdrawTo(withdrawAddress, amount); + } + + /** + * @dev Returns the exchange price of the token in wei. + * @param _token ERC20 token address + * @param _oracleAggregator oracle aggregator address + */ + function exchangePrice( + address _token, + address _oracleAggregator + ) internal view virtual returns (uint256) { + try + IOracleAggregator(_oracleAggregator).getTokenValueOfOneNativeToken( + _token + ) + returns (uint256 exchangeRate) { + return exchangeRate; + } catch { + return 0; + } + } + + /** + * @dev pull tokens out of paymaster in case they were sent to the paymaster at any point. + * @param token the token deposit to withdraw + * @param target address to send to + * @param amount amount to withdraw + */ + function withdrawERC20( + IERC20 token, + address target, + uint256 amount + ) public payable onlyOwner nonReentrant { + _withdrawERC20(token, target, amount); + } + + /** + * @dev pull tokens out of paymaster in case they were sent to the paymaster at any point. + * @param token the token deposit to withdraw + * @param target address to send to + */ + function withdrawERC20Full( + IERC20 token, + address target + ) public payable onlyOwner nonReentrant { + uint256 amount = token.balanceOf(address(this)); + _withdrawERC20(token, target, amount); + } + + /** + * @dev pull multiple tokens out of paymaster in case they were sent to the paymaster at any point. + * @param token the tokens deposit to withdraw + * @param target address to send to + * @param amount amounts to withdraw + */ + function withdrawMultipleERC20( + IERC20[] calldata token, + address target, + uint256[] calldata amount + ) public payable onlyOwner nonReentrant { + if (token.length != amount.length) + revert TokensAndAmountsLengthMismatch(); + unchecked { + for (uint256 i; i < token.length; ) { + _withdrawERC20(token[i], target, amount[i]); + ++i; + } + } + } + + /** + * @dev pull multiple tokens out of paymaster in case they were sent to the paymaster at any point. + * @param token the tokens deposit to withdraw + * @param target address to send to + */ + function withdrawMultipleERC20Full( + IERC20[] calldata token, + address target + ) public payable onlyOwner nonReentrant { + unchecked { + for (uint256 i; i < token.length; ) { + uint256 amount = token[i].balanceOf(address(this)); + _withdrawERC20(token[i], target, amount); + ++i; + } + } + } + + /** + * @dev pull native tokens out of paymaster in case they were sent to the paymaster at any point + * @param dest address to send to + */ + function withdrawAllNative( + address dest + ) public payable onlyOwner nonReentrant { + uint256 _balance = address(this).balance; + if (_balance == 0) revert NativeTokenBalanceZero(); + if (dest == address(0)) revert CanNotWithdrawToZeroAddress(); + bool success; + assembly ("memory-safe") { + success := call(gas(), dest, _balance, 0, 0, 0, 0) + } + if (!success) revert NativeTokensWithdrawalFailed(); + } + + /** + * @dev 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. + * @notice That this signature covers all fields of the UserOperation, except the "paymasterAndData", + * which will carry the signature itself. + * @return hash we're going to sign off-chain (and validate on-chain) + */ + function getHash( + UserOperation calldata userOp, + ExchangeRateSource priceSource, + uint48 validUntil, + uint48 validAfter, + address feeToken, + address oracleAggregator, + uint256 exchangeRate, + uint32 priceMarkup + ) public view returns (bytes32) { + //can't use userOp.hash(), since it contains also the paymasterAndData itself. + return + keccak256( + abi.encode( + userOp.getSender(), + userOp.nonce, + keccak256(userOp.initCode), + keccak256(userOp.callData), + userOp.callGasLimit, + userOp.verificationGasLimit, + userOp.preVerificationGas, + userOp.maxFeePerGas, + userOp.maxPriorityFeePerGas, + block.chainid, + address(this), + priceSource, + validUntil, + validAfter, + feeToken, + oracleAggregator, + exchangeRate, + priceMarkup + ) + ); + } + + function parsePaymasterAndData( + bytes calldata paymasterAndData + ) + public + pure + returns ( + ExchangeRateSource priceSource, + uint48 validUntil, + uint48 validAfter, + address feeToken, + address oracleAggregator, + uint256 exchangeRate, + uint32 priceMarkup, + bytes calldata signature + ) + { + // paymasterAndData.length should be at least SIGNATURE_OFFSET + 65 (checked separate) + require( + paymasterAndData.length >= SIGNATURE_OFFSET, + "BTPM: Invalid length for paymasterAndData" + ); + priceSource = ExchangeRateSource( + uint8( + bytes1(paymasterAndData[VALID_PND_OFFSET - 1:VALID_PND_OFFSET]) + ) + ); + ( + validUntil, + validAfter, + feeToken, + oracleAggregator, + exchangeRate, + priceMarkup + ) = abi.decode( + paymasterAndData[VALID_PND_OFFSET:SIGNATURE_OFFSET], + (uint48, uint48, address, address, uint256, uint32) + ); + signature = paymasterAndData[SIGNATURE_OFFSET:]; + } + + function _getRequiredPrefund( + UserOperation calldata userOp + ) internal view returns (uint256 requiredPrefund) { + unchecked { + uint256 requiredGas = userOp.callGasLimit + + userOp.verificationGasLimit + + userOp.preVerificationGas + + UNACCOUNTED_COST; + + requiredPrefund = requiredGas * userOp.maxFeePerGas; + } + } + + /** + * @dev Verify that an external signer signed the paymaster data of a user operation. + * The paymaster data is expected to be the paymaster address, request data and a signature over the entire request parameters. + * paymasterAndData: hexConcat([paymasterAddress, priceSource, abi.encode(validUntil, validAfter, feeToken, oracleAggregator, exchangeRate, priceMarkup), signature]) + * @param userOp The UserOperation struct that represents the current user operation. + * userOpHash The hash of the UserOperation struct. + * @param requiredPreFund The required amount of pre-funding for the paymaster. + * @return context A context string returned by the entry point after successful validation. + * @return validationData An integer returned by the entry point after successful validation. + */ + function _validatePaymasterUserOp( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 requiredPreFund + ) + internal + view + override + returns (bytes memory context, uint256 validationData) + { + (requiredPreFund); + // verificationGasLimit is dual-purposed, as gas limit for postOp. make sure it is high enough + // make sure that verificationGasLimit is high enough to handle postOp + require( + userOp.verificationGasLimit > UNACCOUNTED_COST, + "BTPM: gas too low for postOp" + ); + + // review: in this method try to resolve stack too deep (though via-ir is good enough) + ( + ExchangeRateSource priceSource, + uint48 validUntil, + uint48 validAfter, + address feeToken, + address oracleAggregator, + uint256 exchangeRate, + uint32 priceMarkup, + bytes calldata signature + ) = parsePaymasterAndData(userOp.paymasterAndData); + + // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA" + require( + signature.length == 65, + "BTPM: invalid signature length in paymasterAndData" + ); + + bytes32 _hash = getHash( + userOp, + priceSource, + validUntil, + validAfter, + feeToken, + oracleAggregator, + exchangeRate, + priceMarkup + ).toEthSignedMessageHash(); + + context = ""; + + //don't revert on signature failure: return SIG_VALIDATION_FAILED + if (verifyingSigner != _hash.recover(signature)) { + // empty context and sigFailed true + return ( + context, + Helpers._packValidationData(true, validUntil, validAfter) + ); + } + + address account = userOp.getSender(); + + // This model assumes irrespective of priceSource exchangeRate is always sent from outside + // for below checks you would either need maxCost or some exchangeRate + + uint256 btpmRequiredPrefund = _getRequiredPrefund(userOp); + + uint256 tokenRequiredPreFund = (btpmRequiredPrefund * exchangeRate) / + 10 ** 18; + require( + tokenRequiredPreFund != 0, + "BTPM: calculated token charge invalid" + ); + require(priceMarkup <= 2e6, "BTPM: price markup percentage too high"); + require(priceMarkup >= 1e6, "BTPM: price markup percentage too low"); + require( + IERC20(feeToken).balanceOf(account) >= + ((tokenRequiredPreFund * priceMarkup) / PRICE_DENOMINATOR), + "BTPM: account does not have enough token balance" + ); + + context = abi.encode( + account, + feeToken, + oracleAggregator, + priceSource, + exchangeRate, + priceMarkup, + userOpHash + ); + + return ( + context, + Helpers._packValidationData(false, validUntil, validAfter) + ); + } + + /** + * @dev Executes the paymaster's payment conditions + * @param mode tells whether the op succeeded, reverted, or if the op succeeded but cause the postOp to revert + * @param context payment conditions signed by the paymaster in `validatePaymasterUserOp` + * @param actualGasCost amount to be paid to the entry point in wei + */ + function _postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost + ) internal virtual override { + ( + address account, + IERC20 feeToken, + address oracleAggregator, + ExchangeRateSource priceSource, + uint256 exchangeRate, + uint32 priceMarkup, + bytes32 userOpHash + ) = abi.decode( + context, + ( + address, + IERC20, + address, + ExchangeRateSource, + uint256, + uint32, + bytes32 + ) + ); + + uint256 effectiveExchangeRate = exchangeRate; + + if ( + priceSource == ExchangeRateSource.ORACLE_BASED && + oracleAggregator != address(NATIVE_ADDRESS) && + oracleAggregator != address(0) + ) { + uint256 result = exchangePrice(address(feeToken), oracleAggregator); + if (result != 0) effectiveExchangeRate = result; + } + + // We could either touch the state for BASEFEE and calculate based on maxPriorityFee passed (to be added in context along with maxFeePerGas) or just use tx.gasprice + uint256 charge; // Final amount to be charged from user account + { + uint256 actualTokenCost = ((actualGasCost + + (UNACCOUNTED_COST * tx.gasprice)) * effectiveExchangeRate) / + 1e18; + charge = ((actualTokenCost * priceMarkup) / PRICE_DENOMINATOR); + } + + if (mode != PostOpMode.postOpReverted) { + SafeTransferLib.safeTransferFrom( + address(feeToken), + account, + feeReceiver, + charge + ); + emit TokenPaymasterOperation( + account, + address(feeToken), + charge, + oracleAggregator, + priceMarkup, + userOpHash, + effectiveExchangeRate, + priceSource + ); + } else { + // In case transferFrom failed in first handlePostOp call, attempt to charge the tokens again + bytes memory _data = abi.encodeWithSelector( + feeToken.transferFrom.selector, + account, + feeReceiver, + charge + ); + (bool success, bytes memory returndata) = address(feeToken).call( + _data + ); + if (!success) { + // In case above transferFrom failed, pay with deposit / notify at least + // Sender could be banned indefinitely or for certain period + emit TokenPaymentDue(address(feeToken), account, charge); + // Do nothing else here to not revert the whole bundle and harm reputation + } + } + } + + function _withdrawERC20( + IERC20 token, + address target, + uint256 amount + ) private { + if (target == address(0)) revert CanNotWithdrawToZeroAddress(); + SafeTransferLib.safeTransfer(address(token), target, amount); + } + + receive() external payable { + emit Received(msg.sender, msg.value); + } +} diff --git a/contracts/token/TokenPaymasterErrors.sol b/contracts/token/TokenPaymasterErrors.sol index 6e3e7d8..c6df0ac 100644 --- a/contracts/token/TokenPaymasterErrors.sol +++ b/contracts/token/TokenPaymasterErrors.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-only -pragma solidity 0.8.17; +pragma solidity ^0.8.20; contract TokenPaymasterErrors { /** diff --git a/contracts/token/USDCPaymaster.sol b/contracts/token/USDCPaymaster.sol new file mode 100644 index 0000000..e69de29 diff --git a/contracts/token/adapters/IUniversalRouter.sol b/contracts/token/adapters/IUniversalRouter.sol index 9e399f7..11e9ef8 100644 --- a/contracts/token/adapters/IUniversalRouter.sol +++ b/contracts/token/adapters/IUniversalRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; diff --git a/contracts/token/adapters/UniswapExample.sol b/contracts/token/adapters/UniswapExample.sol index 3672698..54aaad2 100644 --- a/contracts/token/adapters/UniswapExample.sol +++ b/contracts/token/adapters/UniswapExample.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; -pragma abicoder v2; +pragma solidity ^0.8.20; import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import "@uniswap/v3-periphery/contracts/interfaces/IQuoter.sol"; diff --git a/contracts/token/feemanager/FeeManager.sol b/contracts/token/feemanager/FeeManager.sol index bbb205c..3d6d183 100644 --- a/contracts/token/feemanager/FeeManager.sol +++ b/contracts/token/feemanager/FeeManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; diff --git a/contracts/token/oracles/ChainlinkOracleAggregator.sol b/contracts/token/oracles/ChainlinkOracleAggregator.sol index ff2a4c9..bffccdf 100644 --- a/contracts/token/oracles/ChainlinkOracleAggregator.sol +++ b/contracts/token/oracles/ChainlinkOracleAggregator.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; import "./IOracleAggregator.sol"; diff --git a/contracts/token/oracles/DerivedPriceFeed.sol b/contracts/token/oracles/DerivedPriceFeed.sol index f2b852e..8321b51 100644 --- a/contracts/token/oracles/DerivedPriceFeed.sol +++ b/contracts/token/oracles/DerivedPriceFeed.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; diff --git a/contracts/token/oracles/FeedInterface.sol b/contracts/token/oracles/FeedInterface.sol index 2915884..1a59cc6 100644 --- a/contracts/token/oracles/FeedInterface.sol +++ b/contracts/token/oracles/FeedInterface.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.20; /** */ diff --git a/contracts/token/oracles/IOracleAggregator.sol b/contracts/token/oracles/IOracleAggregator.sol index ab04874..e3492b6 100644 --- a/contracts/token/oracles/IOracleAggregator.sol +++ b/contracts/token/oracles/IOracleAggregator.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; interface IOracleAggregator { function getTokenValueOfOneNativeToken( diff --git a/contracts/utils/LibAddress.sol b/contracts/utils/LibAddress.sol index 5be2be0..30afb10 100644 --- a/contracts/utils/LibAddress.sol +++ b/contracts/utils/LibAddress.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.17; +pragma solidity ^0.8.20; library LibAddress { /** diff --git a/contracts/verifying/PaymasterHelpers.sol b/contracts/verifying/PaymasterHelpers.sol index 5e3aaa7..0af652e 100644 --- a/contracts/verifying/PaymasterHelpers.sol +++ b/contracts/verifying/PaymasterHelpers.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; diff --git a/contracts/verifying/VerifyingSingletonPaymaster.sol b/contracts/verifying/VerifyingSingletonPaymaster.sol index 8e4d201..0e365c4 100644 --- a/contracts/verifying/VerifyingSingletonPaymaster.sol +++ b/contracts/verifying/VerifyingSingletonPaymaster.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.17; +pragma solidity ^0.8.20; /* solhint-disable reason-string */ /* solhint-disable no-inline-assembly */ @@ -99,7 +99,7 @@ contract VerifyingSingletonPaymaster is @dev Override the default implementation. */ function deposit() public payable virtual override { - revert("user DepositFor instead"); + revert("use depositFor() instead"); } /** diff --git a/contracts/verifying/VerifyingSingletonPaymasterV2.sol b/contracts/verifying/VerifyingSingletonPaymasterV2.sol new file mode 100644 index 0000000..d0c6d88 --- /dev/null +++ b/contracts/verifying/VerifyingSingletonPaymasterV2.sol @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.20; + +/* solhint-disable reason-string */ +/* solhint-disable no-inline-assembly */ +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {UserOperation, UserOperationLib} from "@account-abstraction/contracts/interfaces/UserOperation.sol"; +import "../BasePaymaster.sol"; +import {VerifyingPaymasterErrors} from "../common/Errors.sol"; +import {MathLib} from "../libs/MathLib.sol"; +import {IVerifyingSingletonPaymaster} from "../interfaces/paymasters/IVerifyingSingletonPaymaster.sol"; + +contract VerifyingSingletonPaymasterV2 is + BasePaymaster, + ReentrancyGuard, + VerifyingPaymasterErrors, + IVerifyingSingletonPaymaster +{ + using ECDSA for bytes32; + using UserOperationLib for UserOperation; + + uint32 private constant PRICE_DENOMINATOR = 1e6; + + // paymasterAndData: concat of [paymasterAddress(address), abi.encode(paymasterId, validUntil, validAfter, priceMarkup): makes up 32*4 bytes, signature] + uint256 private constant VALID_PND_OFFSET = 20; + + uint256 private constant SIGNATURE_OFFSET = 148; + + // Gas used in EntryPoint._handlePostOp() method (including this#postOp() call) + uint256 private unaccountedEPGasOverhead; + + uint32 private fixedPriceMarkup; + + mapping(address => uint256) public paymasterIdBalances; + + address public verifyingSigner; + + address public feeCollector; + + constructor( + address _owner, + IEntryPoint _entryPoint, + address _verifyingSigner, + address _feeCollector + ) payable BasePaymaster(_owner, _entryPoint) { + if (address(_entryPoint) == address(0)) revert EntryPointCannotBeZero(); + if (_verifyingSigner == address(0)) + revert VerifyingSignerCannotBeZero(); + if (_feeCollector == address(0)) revert FeeCollectorCannotBeZero(); + assembly { + sstore(verifyingSigner.slot, _verifyingSigner) + sstore(feeCollector.slot, _feeCollector) + } + unaccountedEPGasOverhead = 12000; + fixedPriceMarkup = 1100000; // 10% + } + + /** + * @dev Add a deposit for this paymaster and given paymasterId (Dapp Depositor address), used for paying for transaction fees + * @param paymasterId dapp identifier for which deposit is being made + */ + function depositFor(address paymasterId) external payable nonReentrant { + if (paymasterId == address(0)) revert PaymasterIdCannotBeZero(); + if (msg.value == 0) revert DepositCanNotBeZero(); + paymasterIdBalances[paymasterId] = + paymasterIdBalances[paymasterId] + + msg.value; + entryPoint.depositTo{value: msg.value}(address(this)); + emit GasDeposited(paymasterId, msg.value); + } + + /** + * @dev get the current deposit for paymasterId (Dapp Depositor address) + * @param paymasterId dapp identifier + */ + function getBalance( + address paymasterId + ) external view returns (uint256 balance) { + balance = paymasterIdBalances[paymasterId]; + } + + /** + @dev Override the default implementation. + */ + function deposit() public payable virtual override { + revert("Use depositFor() instead"); + } + + /** + * @dev Withdraws the specified amount of gas tokens from the paymaster's balance and transfers them to the specified address. + * @param withdrawAddress The address to which the gas tokens should be transferred. + * @param amount The amount of gas tokens to withdraw. + */ + function withdrawTo( + address payable withdrawAddress, + uint256 amount + ) public override nonReentrant { + if (withdrawAddress == address(0)) revert CanNotWithdrawToZeroAddress(); + uint256 currentBalance = paymasterIdBalances[msg.sender]; + if (amount > currentBalance) + revert InsufficientBalance(amount, currentBalance); + paymasterIdBalances[msg.sender] = currentBalance - amount; + entryPoint.withdrawTo(withdrawAddress, amount); + emit GasWithdrawn(msg.sender, withdrawAddress, amount); + } + + /** + * @dev Set a new verifying signer address. + * Can only be called by the owner of the contract. + * @param _newVerifyingSigner The new address to be set as the verifying signer. + * @notice If _newVerifyingSigner is set to zero address, it will revert with an error. + * After setting the new signer address, it will emit an event VerifyingSignerChanged. + */ + function setSigner( + address _newVerifyingSigner + ) external payable override onlyOwner { + if (_newVerifyingSigner == address(0)) + revert VerifyingSignerCannotBeZero(); + address oldSigner = verifyingSigner; + assembly { + sstore(verifyingSigner.slot, _newVerifyingSigner) + } + emit VerifyingSignerChanged(oldSigner, _newVerifyingSigner, msg.sender); + } + + /** + * @dev Set a new fee collector address. + * Can only be called by the owner of the contract. + * @param _newFeeCollector The new address to be set as the fee collector. + * @notice If _newFeeCollector is set to zero address, it will revert with an error. + * After setting the new fee collector address, it will emit an event FeeCollectorChanged. + */ + function setFeeCollector( + address _newFeeCollector + ) external payable onlyOwner { + if (_newFeeCollector == address(0)) revert FeeCollectorCannotBeZero(); + address oldFeeCollector = feeCollector; + assembly { + sstore(feeCollector.slot, _newFeeCollector) + } + emit FeeCollectorChanged(oldFeeCollector, _newFeeCollector, msg.sender); + } + + /** + * @dev Set a new unaccountedEPGasOverhead value. + * @param value The new value to be set as the unaccountedEPGasOverhead. + * @notice only to be called by the owner of the contract. + */ + function setUnaccountedEPGasOverhead( + uint256 value + ) external payable onlyOwner { + require(value <= 200000, "Gas overhead too high"); + uint256 oldValue = unaccountedEPGasOverhead; + unaccountedEPGasOverhead = value; + emit EPGasOverheadChanged(oldValue, value); + } + + /** + * @dev Set a new fixedPriceMarkup value. + * @param _markup The new value to be set as the fixedPriceMarkup. + * @notice only to be called by the owner of the contract. + * @notice The markup is in percentage, so 1100000 is 10%. + * @notice The markup can not be higher than 100% + */ + function setFixedPriceMarkup(uint32 _markup) external payable onlyOwner { + require(_markup <= PRICE_DENOMINATOR * 2, "Markup too high"); + uint32 oldValue = fixedPriceMarkup; + fixedPriceMarkup = _markup; + emit FixedPriceMarkupChanged(oldValue, _markup); + } + + /** + * @dev 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. + * @notice That this signature covers all fields of the UserOperation, except the "paymasterAndData", + * which will carry the signature itself. + * @return hash we're going to sign off-chain (and validate on-chain) + */ + function getHash( + UserOperation calldata userOp, + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 priceMarkup + ) public view returns (bytes32) { + //can't use userOp.hash(), since it contains also the paymasterAndData itself. + return + keccak256( + abi.encode( + userOp.getSender(), + userOp.nonce, + userOp.initCode, + userOp.callData, + userOp.callGasLimit, + userOp.verificationGasLimit, + userOp.preVerificationGas, + userOp.maxFeePerGas, + userOp.maxPriorityFeePerGas, + block.chainid, + address(this), + paymasterId, + validUntil, + validAfter, + priceMarkup + ) + ); + } + + // Note: do not use this in validation phase + function getGasPrice( + uint256 maxFeePerGas, + uint256 maxPriorityFeePerGas + ) internal view returns (uint256) { + if (maxFeePerGas == maxPriorityFeePerGas) { + //legacy mode (for networks that don't support basefee opcode) + return maxFeePerGas; + } + return + MathLib.minuint256( + maxFeePerGas, + maxPriorityFeePerGas + block.basefee + ); + } + + /** + * @dev Verify that an external signer signed the paymaster data of a user operation. + * The paymaster data is expected to be the paymaster and a signature over the entire request parameters. + * @param userOp The UserOperation struct that represents the current user operation. + * userOpHash The hash of the UserOperation struct. + * @param requiredPreFund The required amount of pre-funding for the paymaster. + * @return context A context string returned by the entry point after successful validation. + * @return validationData An integer returned by the entry point after successful validation. + */ + function _validatePaymasterUserOp( + UserOperation calldata userOp, + bytes32 /*userOpHash*/, + uint256 requiredPreFund + ) internal override returns (bytes memory context, uint256 validationData) { + ( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 priceMarkup, + bytes calldata signature + ) = parsePaymasterAndData(userOp.paymasterAndData); + + bytes32 hash = getHash( + userOp, + paymasterId, + validUntil, + validAfter, + priceMarkup + ); + uint256 sigLength = signature.length; + // we only "require" it here so that the revert reason on invalid signature will be of "VerifyingPaymaster", and not "ECDSA" + if (sigLength != 65) revert InvalidPaymasterSignatureLength(sigLength); + //don't revert on signature failure: return SIG_VALIDATION_FAILED + if ( + verifyingSigner != hash.toEthSignedMessageHash().recover(signature) + ) { + // empty context and sigFailed with time range provided + return (context, _packValidationData(true, validUntil, validAfter)); + } + + require(priceMarkup <= 2e6, "Verifying PM:high markup %"); + + uint32 dynamicMarkup = MathLib.maxuint32(priceMarkup, fixedPriceMarkup); + require(dynamicMarkup >= 1e6, "Verifying PM:low markup %"); + + uint256 effectiveCost = (requiredPreFund * dynamicMarkup) / + PRICE_DENOMINATOR; + + if (effectiveCost > paymasterIdBalances[paymasterId]) + revert InsufficientBalance( + effectiveCost, + paymasterIdBalances[paymasterId] + ); + + context = abi.encode( + paymasterId, + dynamicMarkup, + userOp.maxFeePerGas, + userOp.maxPriorityFeePerGas + ); + + return (context, _packValidationData(false, validUntil, validAfter)); + } + + function parsePaymasterAndData( + bytes calldata paymasterAndData + ) + public + pure + returns ( + address paymasterId, + uint48 validUntil, + uint48 validAfter, + uint32 priceMarkup, + bytes calldata signature + ) + { + (paymasterId, validUntil, validAfter, priceMarkup) = abi.decode( + paymasterAndData[VALID_PND_OFFSET:SIGNATURE_OFFSET], + (address, uint48, uint48, uint32) + ); + + signature = paymasterAndData[SIGNATURE_OFFSET:]; + } + + /** + * @dev Executes the paymaster's payment conditions + * @param mode tells whether the op succeeded, reverted, or if the op succeeded but cause the postOp to revert + * @param context payment conditions signed by the paymaster in `validatePaymasterUserOp` + * @param actualGasCost amount to be paid to the entry point in wei + */ + function _postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost + ) internal virtual override { + ( + address paymasterId, + uint32 dynamicMarkup, + uint256 maxFeePerGas, + uint256 maxPriorityFeePerGas + ) = abi.decode(context, (address, uint32, uint256, uint256)); + + uint256 effectiveGasPrice = getGasPrice( + maxFeePerGas, + maxPriorityFeePerGas + ); + + uint256 balToDeduct = actualGasCost + + unaccountedEPGasOverhead * + effectiveGasPrice; + + uint256 costIncludingPremium = (balToDeduct * dynamicMarkup) / + PRICE_DENOMINATOR; + + // deduct with premium + paymasterIdBalances[paymasterId] -= costIncludingPremium; + + uint256 actualPremium = costIncludingPremium - balToDeduct; + // "collect" premium + paymasterIdBalances[feeCollector] += actualPremium; + + emit GasBalanceDeducted(paymasterId, costIncludingPremium); + emit PremiumCollected(paymasterId, actualPremium); + } +} diff --git a/foundry.toml b/foundry.toml index 7f69fd6..fad669c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,7 @@ src = 'contracts' out = 'out' libs = ['node_modules', 'lib'] test = 'test' -cache_path = 'forge-cache' +cache_path = 'forge-cache' viaIr = true +evm_version = "paris" # See more config options https://book.getfoundry.sh/reference/config.html diff --git a/hardhat.config.ts b/hardhat.config.ts index 82b0711..5dc25c2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -27,6 +27,14 @@ const config: HardhatUserConfig = { viaIR: true, }, }, + { + version: "0.8.20", + settings: { + optimizer: { enabled: true, runs: 800 }, + evmVersion: "paris", + viaIR: true, + }, + }, ], }, networks: { diff --git a/test/TokenPaymaster.t.sol b/test/TokenPaymaster.t.sol index 67b747d..03f6486 100644 --- a/test/TokenPaymaster.t.sol +++ b/test/TokenPaymaster.t.sol @@ -26,13 +26,27 @@ import {EcdsaOwnershipRegistryModule} from "@biconomy-devx/account-contracts-v2/ import {MockToken} from "../contracts/test/helpers/MockToken.sol"; import {MockPriceFeed} from "../contracts/test/helpers/MockPriceFeed.sol"; - import {FeedInterface} from "../contracts/token/oracles/FeedInterface.sol"; error SetupIncomplete(); using ECDSA for bytes32; +interface ISmartAccount { + function executeCall( + address dest, + uint256 value, + bytes calldata func + ) external; +} + +interface ISmartAccountFactory { + function deployCounterFactualAccount( + address _owner, + uint256 _index + ) external returns (address proxy); +} + contract TokenPaymasterTest is Test { using stdStorage for StdStorage; @@ -79,13 +93,23 @@ contract TokenPaymasterTest is Test { counter = new TestCounter(); // setting price oracle for token - bytes memory _data = abi.encodeWithSelector(FeedInterface.getThePrice.selector); + bytes memory _data = abi.encodeWithSelector( + FeedInterface.getThePrice.selector + ); vm.prank(alice); // could also make a .call using selector and handle success - _oa1.setTokenOracle(address(usdc), address(usdcMaticFeed), 18, _data, true); + _oa1.setTokenOracle( + address(usdc), + address(usdcMaticFeed), + 18, + _data, + true + ); - uint256 priceToLog = _oa1.getTokenValueOfOneNativeToken((address(usdc))); + uint256 priceToLog = _oa1.getTokenValueOfOneNativeToken( + (address(usdc)) + ); console2.log(priceToLog); smartAccount = new SmartAccount(_ep); @@ -197,8 +221,8 @@ contract TokenPaymasterTest is Test { // WIP // TODO function testParsePaymasterData() public { - bytes memory paymasterAndData = - "0x0987404beb853f24f36c76c3e18adcad7ab44f930100000000000000000000000000000000000000000000000000000000deadbeef00000000000000000000000000000000000000000000000000000000000012340000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000007ed8428288323e8583defc90bfdf2dad91cff88900000000000000000000000000000000000000000000000000000000000d34e300000000000000000000000000000000000000000000000000000000000000001984ae5a976a7eb4ee0292a2fa344721f074ce31a90d3182318bdbcec8b447f55690ffa572e58e1f40e93d3e7060b0a20a8b3493226d269adf3cb4d467e9996d1c"; + bytes + memory paymasterAndData = "0x0987404beb853f24f36c76c3e18adcad7ab44f930100000000000000000000000000000000000000000000000000000000deadbeef00000000000000000000000000000000000000000000000000000000000012340000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000007ed8428288323e8583defc90bfdf2dad91cff88900000000000000000000000000000000000000000000000000000000000d34e300000000000000000000000000000000000000000000000000000000000000001984ae5a976a7eb4ee0292a2fa344721f074ce31a90d3182318bdbcec8b447f55690ffa572e58e1f40e93d3e7060b0a20a8b3493226d269adf3cb4d467e9996d1c"; bytes memory paymasterAndDataBytes = bytes(paymasterAndData); // [FAIL. Reason: Conversion into non-existent enum type] // _btpm.parsePaymasterAndData(abi.encodePacked(paymasterAndDataBytes)); @@ -207,8 +231,13 @@ contract TokenPaymasterTest is Test { // sanity check for everything works without paymaster function testCall() external { vm.deal(address(smartAccount), 1e18); - (UserOperation memory op, uint256 prefund) = - fillUserOp(smartAccount, keyUser, address(counter), 0, abi.encodeWithSelector(TestCounter.count.selector)); + (UserOperation memory op, uint256 prefund) = fillUserOp( + address(smartAccount), + keyUser, + address(counter), + 0, + abi.encodeWithSelector(TestCounter.count.selector) + ); op.signature = signUserOp(op, keyUser); UserOperation[] memory ops = new UserOperation[](1); ops[0] = op; @@ -220,9 +249,12 @@ contract TokenPaymasterTest is Test { vm.deal(address(smartAccount), 1e18); usdc.mint(address(smartAccount), 100e6); // 100 usdc; usdc.mint(address(_btpm), 100e6); // 100 usdc; - console2.log("paymaster balance before ", usdc.balanceOf(address(_btpm))); + console2.log( + "paymaster balance before ", + usdc.balanceOf(address(_btpm)) + ); (UserOperation memory op, uint256 prefund) = fillUserOp( - smartAccount, + address(smartAccount), keyUser, address(usdc), 0, @@ -230,7 +262,9 @@ contract TokenPaymasterTest is Test { ); bytes memory pmSig = signPaymasterSignature(op, keyVerifyingSigner); - BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster.ExchangeRateSource.ORACLE_BASED; + BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster + .ExchangeRateSource + .ORACLE_BASED; uint48 validUntil = 3735928559; uint48 validAfter = 4660; uint256 exchangeRate = 977100; @@ -239,7 +273,14 @@ contract TokenPaymasterTest is Test { op.paymasterAndData = abi.encodePacked( address(_btpm), priceSource, - abi.encode(validUntil, validAfter, address(usdc), address(_oa1), exchangeRate, priceMarkup), + abi.encode( + validUntil, + validAfter, + address(usdc), + address(_oa1), + exchangeRate, + priceMarkup + ), pmSig ); op.signature = signUserOp(op, keyUser); @@ -248,7 +289,10 @@ contract TokenPaymasterTest is Test { _ep.handleOps(ops, beneficiary); // todo // review fails to validate updated balances - console2.log("paymaster balance after ", usdc.balanceOf(address(_btpm))); + console2.log( + "paymaster balance after ", + usdc.balanceOf(address(_btpm)) + ); assertNotEq(usdc.balanceOf(address(smartAccount)), 100e6); } @@ -256,9 +300,12 @@ contract TokenPaymasterTest is Test { vm.deal(address(smartAccount), 1e18); usdc.mint(address(smartAccount), 100e6); // 100 usdc; usdc.mint(address(_btpm), 100e6); // 100 usdc; - console2.log("paymaster balance before ", usdc.balanceOf(address(_btpm))); + console2.log( + "paymaster balance before ", + usdc.balanceOf(address(_btpm)) + ); (UserOperation memory op, uint256 prefund) = fillUserOp( - smartAccount, + address(smartAccount), keyUser, address(usdc), 0, @@ -266,7 +313,9 @@ contract TokenPaymasterTest is Test { ); bytes memory pmSig = "0x1234"; - BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster.ExchangeRateSource.ORACLE_BASED; + BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster + .ExchangeRateSource + .ORACLE_BASED; uint48 validUntil = 3735928559; uint48 validAfter = 4660; uint256 exchangeRate = 977100; @@ -275,7 +324,14 @@ contract TokenPaymasterTest is Test { op.paymasterAndData = abi.encodePacked( address(_btpm), priceSource, - abi.encode(validUntil, validAfter, address(usdc), address(_oa1), exchangeRate, priceMarkup), + abi.encode( + validUntil, + validAfter, + address(usdc), + address(_oa1), + exchangeRate, + priceMarkup + ), pmSig ); op.signature = signUserOp(op, keyUser); @@ -295,9 +351,12 @@ contract TokenPaymasterTest is Test { vm.deal(address(smartAccount), 1e18); usdc.mint(address(smartAccount), 100e6); // 100 usdc; usdc.mint(address(_btpm), 100e6); // 100 usdc; - console2.log("paymaster balance before ", usdc.balanceOf(address(_btpm))); + console2.log( + "paymaster balance before ", + usdc.balanceOf(address(_btpm)) + ); (UserOperation memory op, uint256 prefund) = fillUserOp( - smartAccount, + address(smartAccount), keyUser, address(usdc), 0, @@ -316,9 +375,12 @@ contract TokenPaymasterTest is Test { vm.deal(address(smartAccount), 1e18); usdc.mint(address(smartAccount), 100e6); // 100 usdc; usdc.mint(address(_btpm), 100e6); // 100 usdc; - console2.log("paymaster balance before ", usdc.balanceOf(address(_btpm))); + console2.log( + "paymaster balance before ", + usdc.balanceOf(address(_btpm)) + ); (UserOperation memory op, uint256 prefund) = fillUserOp( - smartAccount, + address(smartAccount), keyUser, address(usdc), 0, @@ -332,7 +394,9 @@ contract TokenPaymasterTest is Test { ops[0] = op; vm.expectRevert( abi.encodeWithSelector( - IEntryPoint.FailedOp.selector, uint256(0), "AA33 reverted: BTPM: Invalid length for paymasterAndData" + IEntryPoint.FailedOp.selector, + uint256(0), + "AA33 reverted: BTPM: Invalid length for paymasterAndData" ) ); _ep.handleOps(ops, beneficiary); @@ -342,18 +406,23 @@ contract TokenPaymasterTest is Test { vm.deal(address(smartAccount), 1e18); usdc.mint(address(smartAccount), 100e6); // 100 usdc; usdc.mint(address(_btpm), 100e6); // 100 usdc; - console2.log("paymaster balance before ", usdc.balanceOf(address(_btpm))); + console2.log( + "paymaster balance before ", + usdc.balanceOf(address(_btpm)) + ); (UserOperation memory op, uint256 prefund) = fillUserOp( - smartAccount, + address(smartAccount), keyUser, address(usdc), 0, abi.encodeWithSelector(ERC20.approve.selector, address(_btpm), 10e6) ); - bytes memory pmSig = - "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + bytes + memory pmSig = "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster.ExchangeRateSource.ORACLE_BASED; + BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster + .ExchangeRateSource + .ORACLE_BASED; uint48 validUntil = 3735928559; uint48 validAfter = 4660; uint256 exchangeRate = 977100; @@ -362,7 +431,14 @@ contract TokenPaymasterTest is Test { op.paymasterAndData = abi.encodePacked( address(_btpm), priceSource, - abi.encode(validUntil, validAfter, address(usdc), address(_oa1), exchangeRate, priceMarkup), + abi.encode( + validUntil, + validAfter, + address(usdc), + address(_oa1), + exchangeRate, + priceMarkup + ), pmSig ); op.signature = signUserOp(op, keyUser); @@ -384,9 +460,12 @@ contract TokenPaymasterTest is Test { vm.deal(address(smartAccount), 1e18); usdc.mint(address(smartAccount), 100e6); // 100 usdc; usdc.mint(address(_btpm), 100e6); // 100 usdc; - console2.log("paymaster balance before ", usdc.balanceOf(address(_btpm))); + console2.log( + "paymaster balance before ", + usdc.balanceOf(address(_btpm)) + ); (UserOperation memory op, uint256 prefund) = fillUserOp( - smartAccount, + address(smartAccount), keyUser, address(usdc), 0, @@ -394,10 +473,15 @@ contract TokenPaymasterTest is Test { ); bytes32 hash = keccak256((abi.encodePacked("some message"))); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(keyVerifyingSigner, hash.toEthSignedMessageHash()); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + keyVerifyingSigner, + hash.toEthSignedMessageHash() + ); bytes memory pmSig = abi.encodePacked(r, s, v); - BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster.ExchangeRateSource.ORACLE_BASED; + BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster + .ExchangeRateSource + .ORACLE_BASED; uint48 validUntil = 3735928559; uint48 validAfter = 4660; uint256 exchangeRate = 977100; @@ -406,7 +490,14 @@ contract TokenPaymasterTest is Test { op.paymasterAndData = abi.encodePacked( address(_btpm), priceSource, - abi.encode(validUntil, validAfter, address(usdc), address(_oa1), exchangeRate, priceMarkup), + abi.encode( + validUntil, + validAfter, + address(usdc), + address(_oa1), + exchangeRate, + priceMarkup + ), pmSig ); op.signature = signUserOp(op, keyUser); @@ -422,31 +513,53 @@ contract TokenPaymasterTest is Test { vm.deal(address(smartAccount), 1e18); usdc.mint(address(smartAccount), 100e6); // 100 usdc; usdc.mint(address(_btpm), 100e6); // 100 usdc; - console2.log("paymaster balance before ", usdc.balanceOf(address(_btpm))); + console2.log( + "paymaster balance before ", + usdc.balanceOf(address(_btpm)) + ); (UserOperation memory op, uint256 prefund) = fillUserOp( - smartAccount, + address(smartAccount), keyUser, address(usdc), 0, abi.encodeWithSelector(ERC20.approve.selector, address(_btpm), 10e6) ); - BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster.ExchangeRateSource.ORACLE_BASED; + BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster + .ExchangeRateSource + .ORACLE_BASED; uint48 validUntil = 3735928559; uint48 validAfter = 4660; uint256 exchangeRate = 977100; uint32 priceMarkup = 2200000; bytes32 hash = _btpm.getHash( - op, priceSource, validUntil, validAfter, address(usdc), address(_oa1), exchangeRate, priceMarkup + op, + priceSource, + validUntil, + validAfter, + address(usdc), + address(_oa1), + exchangeRate, + priceMarkup + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + keyVerifyingSigner, + hash.toEthSignedMessageHash() ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(keyVerifyingSigner, hash.toEthSignedMessageHash()); bytes memory pmSig = abi.encodePacked(r, s, v); op.paymasterAndData = abi.encodePacked( address(_btpm), priceSource, - abi.encode(validUntil, validAfter, address(usdc), address(_oa1), exchangeRate, priceMarkup), + abi.encode( + validUntil, + validAfter, + address(usdc), + address(_oa1), + exchangeRate, + priceMarkup + ), pmSig ); op.signature = signUserOp(op, keyUser); @@ -454,7 +567,9 @@ contract TokenPaymasterTest is Test { ops[0] = op; vm.expectRevert( abi.encodeWithSelector( - IEntryPoint.FailedOp.selector, uint256(0), "AA33 reverted: BTPM: price markup percentage too high" + IEntryPoint.FailedOp.selector, + uint256(0), + "AA33 reverted: BTPM: price markup percentage too high" ) ); @@ -488,9 +603,15 @@ contract TokenPaymasterTest is Test { ); } - function signUserOp(UserOperation memory op, uint256 _key) public returns (bytes memory signature) { + function signUserOp( + UserOperation memory op, + uint256 _key + ) public returns (bytes memory signature) { bytes32 hash = _ep.getUserOpHash(op); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, hash.toEthSignedMessageHash()); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + _key, + hash.toEthSignedMessageHash() + ); signature = abi.encodePacked(r, s, v); signature = abi.encode( signature, @@ -498,54 +619,83 @@ contract TokenPaymasterTest is Test { ); } - function signPaymasterSignature(UserOperation memory op, uint256 _key) public returns (bytes memory signature) { - BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster.ExchangeRateSource.ORACLE_BASED; + function signPaymasterSignature( + UserOperation memory op, + uint256 _key + ) public returns (bytes memory signature) { + BiconomyTokenPaymaster.ExchangeRateSource priceSource = BiconomyTokenPaymaster + .ExchangeRateSource + .ORACLE_BASED; uint48 validUntil = 3735928559; uint48 validAfter = 4660; uint256 exchangeRate = 977100; uint32 priceMarkup = 1100000; bytes32 hash = _btpm.getHash( - op, priceSource, validUntil, validAfter, address(usdc), address(_oa1), exchangeRate, priceMarkup + op, + priceSource, + validUntil, + validAfter, + address(usdc), + address(_oa1), + exchangeRate, + priceMarkup + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + _key, + hash.toEthSignedMessageHash() ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(_key, hash.toEthSignedMessageHash()); signature = abi.encodePacked(r, s, v); } - function simulateVerificationGas(IEntryPoint _entrypoint, UserOperation memory op) - public - returns (UserOperation memory, uint256 preFund) - { - (bool success, bytes memory ret) = - address(_entrypoint).call(abi.encodeWithSelector(EntryPoint.simulateValidation.selector, op)); + function simulateVerificationGas( + IEntryPoint _entrypoint, + UserOperation memory op + ) public returns (UserOperation memory, uint256 preFund) { + (bool success, bytes memory ret) = address(_entrypoint).call( + abi.encodeWithSelector(EntryPoint.simulateValidation.selector, op) + ); require(!success); bytes memory data = BytesLib.slice(ret, 4, ret.length - 4); - (IEntryPoint.ReturnInfo memory retInfo,,,) = abi.decode( - data, (IEntryPoint.ReturnInfo, IStakeManager.StakeInfo, IStakeManager.StakeInfo, IStakeManager.StakeInfo) + (IEntryPoint.ReturnInfo memory retInfo, , , ) = abi.decode( + data, + ( + IEntryPoint.ReturnInfo, + IStakeManager.StakeInfo, + IStakeManager.StakeInfo, + IStakeManager.StakeInfo + ) ); op.preVerificationGas = retInfo.preOpGas; op.verificationGasLimit = retInfo.preOpGas; - op.maxFeePerGas = retInfo.prefund * 11 / (retInfo.preOpGas * 10); + op.maxFeePerGas = (retInfo.prefund * 11) / (retInfo.preOpGas * 10); op.maxPriorityFeePerGas = 1; return (op, retInfo.prefund); } - function simulateCallGas(IEntryPoint _entrypoint, UserOperation memory op) internal returns (uint256) { + function simulateCallGas( + IEntryPoint _entrypoint, + UserOperation memory op + ) internal returns (uint256) { try this.calcGas(_entrypoint, op.sender, op.callData) { revert("Should have failed"); } catch Error(string memory reason) { uint256 gas = abi.decode(bytes(reason), (uint256)); - return gas * 11 / 10; + return (gas * 11) / 10; } catch { revert("Should have failed"); } } // not used internally - function calcGas(IEntryPoint _entrypoint, address _to, bytes memory _data) external { + function calcGas( + IEntryPoint _entrypoint, + address _to, + bytes memory _data + ) external { vm.startPrank(address(_entrypoint)); uint256 g = gasleft(); - (bool success,) = _to.call(_data); + (bool success, ) = _to.call(_data); require(success); g = g - gasleft(); bytes memory r = abi.encode(g); @@ -553,9 +703,9 @@ contract TokenPaymasterTest is Test { require(false, string(r)); } - function testDecode() external view{ - bytes memory d = - hex"0000023d6c240ae3c9610d519510004d2616c9ec010000000000000000000000000000000000000000000000000000000065157c23000000000000000000000000000000000000000000000000000000006515751b0000000000000000000000008ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000000000000000000065b8abb967271817555f23945eedf08015c00000000000000000000000000000000000000000000000b88f7f3bb38595e8a000000000000000000000000000000000000000000000000000000000010c8e019b54af51b156531fb11b7aabf4dba0d0eae1e519e54d176633de65eac43d41c431ce06784f1bab9085771a86c1a006d944214c33272e022e7168e5062fd8fb01c"; + function testDecode() external view { + bytes + memory d = hex"0000023d6c240ae3c9610d519510004d2616c9ec010000000000000000000000000000000000000000000000000000000065157c23000000000000000000000000000000000000000000000000000000006515751b0000000000000000000000008ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000000000000000000065b8abb967271817555f23945eedf08015c00000000000000000000000000000000000000000000000b88f7f3bb38595e8a000000000000000000000000000000000000000000000000000000000010c8e019b54af51b156531fb11b7aabf4dba0d0eae1e519e54d176633de65eac43d41c431ce06784f1bab9085771a86c1a006d944214c33272e022e7168e5062fd8fb01c"; ( BiconomyTokenPaymaster.ExchangeRateSource priceSource, uint48 validUntil, diff --git a/test/bundler-integration/token-paymaster/biconomy-token-paymaster-specs.ts b/test/bundler-integration/token-paymaster/biconomy-token-paymaster-specs.ts index 17913c7..786f22e 100644 --- a/test/bundler-integration/token-paymaster/biconomy-token-paymaster-specs.ts +++ b/test/bundler-integration/token-paymaster/biconomy-token-paymaster-specs.ts @@ -206,13 +206,14 @@ describe("Biconomy Token Paymaster (with Bundler)", function () { after(async function () { const chainId = (await ethers.provider.getNetwork()).chainId; - if (chainId === BundlerTestEnvironment.BUNDLER_ENVIRONMENT_CHAIN_ID) { + + if (chainId === BundlerTestEnvironment.BUNDLER_ENVIRONMENT_CHAIN_ID) { await Promise.all([ environment.revert(environment.defaultSnapshot!), environment.resetBundler(), ]); } - }); + }); describe("Token Payamster functionality: positive test", () => { it("succeed with valid signature and valid erc20 pre approval for allowed ERC20 token: Deployed account", async () => { diff --git a/test/bundler-integration/token-paymaster/btpm-undeployed-wallet.ts b/test/bundler-integration/token-paymaster/btpm-undeployed-wallet.ts index c7da731..bd52a13 100644 --- a/test/bundler-integration/token-paymaster/btpm-undeployed-wallet.ts +++ b/test/bundler-integration/token-paymaster/btpm-undeployed-wallet.ts @@ -201,7 +201,9 @@ describe("Biconomy Token Paymaster (with Bundler)", function () { after(async function () { const chainId = (await ethers.provider.getNetwork()).chainId; + if (chainId === BundlerTestEnvironment.BUNDLER_ENVIRONMENT_CHAIN_ID) { + await Promise.all([ environment.revert(environment.defaultSnapshot!), environment.resetBundler(), diff --git a/test/bundler-integration/token-paymaster/oracle-aggregator-specs.ts b/test/bundler-integration/token-paymaster/oracle-aggregator-specs.ts index ad73e88..26503ef 100644 --- a/test/bundler-integration/token-paymaster/oracle-aggregator-specs.ts +++ b/test/bundler-integration/token-paymaster/oracle-aggregator-specs.ts @@ -229,6 +229,7 @@ describe("Biconomy Token Paymaster (With Bundler)", function () { after(async function () { const chainId = (await ethers.provider.getNetwork()).chainId; + if (chainId === BundlerTestEnvironment.BUNDLER_ENVIRONMENT_CHAIN_ID) { await Promise.all([ environment.revert(environment.defaultSnapshot!), diff --git a/test/sponsorship-paymaster/biconomy-verifying-paymaster-specs.ts b/test/sponsorship-paymaster/biconomy-verifying-paymaster-specs.ts index a1a78af..74cbcd3 100644 --- a/test/sponsorship-paymaster/biconomy-verifying-paymaster-specs.ts +++ b/test/sponsorship-paymaster/biconomy-verifying-paymaster-specs.ts @@ -110,9 +110,6 @@ describe("EntryPoint with VerifyingPaymaster Singleton", function () { console.log("paymaster staked"); await entryPoint.depositTo(paymasterAddress, { value: parseEther("1") }); - - // const resultSet = await entryPoint.getDepositInfo(paymasterAddress); - // console.log("deposited state ", resultSet); }); async function getUserOpWithPaymasterInfo(paymasterId: string) { @@ -223,6 +220,7 @@ describe("EntryPoint with VerifyingPaymaster Singleton", function () { userOp.signature = signatureWithModuleAddress await entryPoint.handleOps([userOp], await offchainSigner.getAddress()); + // gas used VPM V1 134369 await expect( entryPoint.handleOps([userOp], await offchainSigner.getAddress()) ).to.be.reverted; diff --git a/test/sponsorship-paymaster/biconomy-verifying-paymaster-v2-specs.ts b/test/sponsorship-paymaster/biconomy-verifying-paymaster-v2-specs.ts new file mode 100644 index 0000000..22728ed --- /dev/null +++ b/test/sponsorship-paymaster/biconomy-verifying-paymaster-v2-specs.ts @@ -0,0 +1,251 @@ +/* eslint-disable node/no-missing-import */ +/* eslint-disable camelcase */ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { + BiconomyAccountImplementation, + BiconomyAccountImplementation__factory, + BiconomyAccountFactory, + BiconomyAccountFactory__factory, + VerifyingSingletonPaymasterV2, + VerifyingSingletonPaymasterV2__factory, +} from "../../typechain-types"; +import { fillAndSign } from "../utils/userOp"; +import { UserOperation } from "../../lib/account-abstraction/test/UserOperation"; +import { createAccount, simulationResultCatch } from "../../lib/account-abstraction/test/testutils"; +import { EntryPoint, EntryPoint__factory, SimpleAccount, TestToken, TestToken__factory } from "../../lib/account-abstraction/typechain"; +import { EcdsaOwnershipRegistryModule, EcdsaOwnershipRegistryModule__factory } from "@biconomy-devx/account-contracts-v2/dist/types"; + +export const AddressZero = ethers.constants.AddressZero; +import { arrayify, hexConcat, parseEther } from "ethers/lib/utils"; +import { BigNumber, BigNumberish, Contract, Signer } from "ethers"; + +const MOCK_VALID_UNTIL = "0x00000000deadbeef"; +const MOCK_VALID_AFTER = "0x0000000000001234"; +const dynamicMarkup = 1200000; // or 0 or 1100000 + +export async function deployEntryPoint( + provider = ethers.provider +): Promise { + const epf = await (await ethers.getContractFactory("EntryPoint")).deploy(); + return EntryPoint__factory.connect(epf.address, provider.getSigner()); +} + +describe("EntryPoint with VerifyingPaymaster Singleton", function () { + let entryPoint: EntryPoint; + let entryPointStatic: EntryPoint; + let depositorSigner: Signer; + let walletOwner: Signer; + let walletAddress: string, paymasterAddress: string; + let ethersSigner; + + let offchainSigner: Signer, deployer: Signer, feeCollector: Signer; + + let verifyingSingletonPaymaster: VerifyingSingletonPaymasterV2; + let smartWalletImp: BiconomyAccountImplementation; + let ecdsaModule: EcdsaOwnershipRegistryModule; + let walletFactory: BiconomyAccountFactory; + const abi = ethers.utils.defaultAbiCoder; + + beforeEach(async function () { + ethersSigner = await ethers.getSigners(); + entryPoint = await deployEntryPoint(); + entryPointStatic = entryPoint.connect(AddressZero); + + deployer = ethersSigner[0]; + offchainSigner = ethersSigner[1]; + depositorSigner = ethersSigner[2]; + feeCollector = ethersSigner[3]; + walletOwner = deployer; // ethersSigner[3]; + + const offchainSignerAddress = await offchainSigner.getAddress(); + const walletOwnerAddress = await walletOwner.getAddress(); + const feeCollectorAddress = await feeCollector.getAddress(); + + ecdsaModule = await new EcdsaOwnershipRegistryModule__factory(deployer).deploy(); + + verifyingSingletonPaymaster = + await new VerifyingSingletonPaymasterV2__factory(deployer).deploy( + await deployer.getAddress(), + entryPoint.address, + offchainSignerAddress, + feeCollectorAddress + ); + + smartWalletImp = await new BiconomyAccountImplementation__factory(deployer).deploy( + entryPoint.address + ); + + walletFactory = await new BiconomyAccountFactory__factory(deployer).deploy( + smartWalletImp.address, + walletOwnerAddress + ); + + await walletFactory.connect(deployer).addStake(entryPoint.address, 86400, { value: parseEther("2") }) + + const ecdsaOwnershipSetupData = + ecdsaModule.interface.encodeFunctionData( + "initForSmartAccount", + [walletOwnerAddress] + ); + + const smartAccountDeploymentIndex = 0; + + + await walletFactory.deployCounterFactualAccount(ecdsaModule.address, ecdsaOwnershipSetupData, smartAccountDeploymentIndex + ); + const expected = await walletFactory.getAddressForCounterFactualAccount( + ecdsaModule.address, ecdsaOwnershipSetupData, smartAccountDeploymentIndex + ); + + walletAddress = expected; + console.log(" wallet address ", walletAddress); + + paymasterAddress = verifyingSingletonPaymaster.address; + console.log("Paymaster address is ", paymasterAddress); + + await entryPoint.depositTo(paymasterAddress, { value: parseEther("1") }); + }); + + async function getUserOpWithPaymasterInfo(paymasterId: string) { + const userOp1 = await fillAndSign( + { + sender: walletAddress, + }, + walletOwner, + entryPoint, + "nonce" + ); + + const hash = await verifyingSingletonPaymaster.getHash( + userOp1, + paymasterId, + MOCK_VALID_UNTIL, + MOCK_VALID_AFTER, + dynamicMarkup + ); + const sig = await offchainSigner.signMessage(arrayify(hash)); + const paymasterData = abi.encode( + ["address", "uint48", "uint48", "uint32"], + [paymasterId, MOCK_VALID_UNTIL, MOCK_VALID_AFTER, dynamicMarkup] + ); + const paymasterAndData = hexConcat([paymasterAddress, paymasterData, sig]); + return await fillAndSign( + { + ...userOp1, + paymasterAndData, + }, + walletOwner, + entryPoint, + "nonce" + ); + } + + describe("#validatePaymasterUserOp", () => { + it("Should parse data properly", async () => { + const paymasterAndData = hexConcat([ + paymasterAddress, + ethers.utils.defaultAbiCoder.encode( + ["address", "uint48", "uint48", "uint32"], + [ + await offchainSigner.getAddress(), + MOCK_VALID_UNTIL, + MOCK_VALID_AFTER, + dynamicMarkup, + ] + ), + "0x" + "00".repeat(65) + ]) + + const res = await verifyingSingletonPaymaster.parsePaymasterAndData( + paymasterAndData + ); + + expect(res.paymasterId).to.equal(await offchainSigner.getAddress()); + expect(res.validUntil).to.equal(ethers.BigNumber.from(MOCK_VALID_UNTIL)); + expect(res.validAfter).to.equal(ethers.BigNumber.from(MOCK_VALID_AFTER)); + expect(res.priceMarkup).to.equal(dynamicMarkup); + expect(res.signature).to.equal("0x" + "00".repeat(65)); + }); + + it("Should Fail when there is no deposit for paymaster id", async () => { + const paymasterId = await depositorSigner.getAddress(); + console.log("paymaster Id ", paymasterId); + const userOp = await getUserOpWithPaymasterInfo(paymasterId); + console.log("entrypoint ", entryPoint.address); + await expect( + entryPoint.callStatic.simulateValidation(userOp) + // ).to.be.revertedWith("FailedOp"); + ).to.be.reverted; + }); + + it("succeed with valid signature", async () => { + const feeCollectorBalanceBefore = await verifyingSingletonPaymaster.getBalance(await feeCollector.getAddress()); + expect(feeCollectorBalanceBefore).to.be.equal(BigNumber.from(0)); + const signer = await verifyingSingletonPaymaster.verifyingSigner(); + const offchainSignerAddress = await offchainSigner.getAddress(); + expect(signer).to.be.equal(offchainSignerAddress); + + await verifyingSingletonPaymaster.depositFor( + await offchainSigner.getAddress(), + { value: ethers.utils.parseEther("1") } + ); + const userOp1 = await fillAndSign( + { + sender: walletAddress, + verificationGasLimit: 200000, + }, + walletOwner, + entryPoint, + "nonce" + ); + + const hash = await verifyingSingletonPaymaster.getHash( + userOp1, + await offchainSigner.getAddress(), + MOCK_VALID_UNTIL, + MOCK_VALID_AFTER, + dynamicMarkup + ); + const sig = await offchainSigner.signMessage(arrayify(hash)); + const userOp = await fillAndSign( + { + ...userOp1, + paymasterAndData: hexConcat([ + paymasterAddress, + ethers.utils.defaultAbiCoder.encode( + ["address", "uint48", "uint48", "uint32"], + [ + await offchainSigner.getAddress(), + MOCK_VALID_UNTIL, + MOCK_VALID_AFTER, + dynamicMarkup, + ] + ), + sig + ]), + }, + walletOwner, + entryPoint, + "nonce" + ); + + const signatureWithModuleAddress = ethers.utils.defaultAbiCoder.encode( + ["bytes", "address"], + [userOp.signature, ecdsaModule.address] + ); + userOp.signature = signatureWithModuleAddress + + + await entryPoint.handleOps([userOp], await offchainSigner.getAddress()); + // gas used VPM V2 162081 + await expect( + entryPoint.handleOps([userOp], await offchainSigner.getAddress()) + ).to.be.reverted; + + const feeCollectorBalanceAfter = await verifyingSingletonPaymaster.getBalance(await feeCollector.getAddress()) + expect(feeCollectorBalanceAfter).to.be.greaterThan(BigNumber.from(0)); + }); + }); +}); diff --git a/test/token-paymaster/biconomy-token-paymaster-specs.ts b/test/token-paymaster/biconomy-token-paymaster-specs.ts index f612eeb..1b66a3b 100644 --- a/test/token-paymaster/biconomy-token-paymaster-specs.ts +++ b/test/token-paymaster/biconomy-token-paymaster-specs.ts @@ -200,9 +200,6 @@ describe("Biconomy Token Paymaster", function () { console.log("paymaster staked"); await entryPoint.depositTo(paymasterAddress, { value: parseEther("2") }); - - // const resultSet = await entryPoint.getDepositInfo(paymasterAddress); - // console.log("deposited state ", resultSet); }); describe("Token Payamster read methods and state checks", () => { diff --git a/test/token-paymaster/btpm-undeployed-wallet.ts b/test/token-paymaster/btpm-undeployed-wallet.ts index 231c78f..8aba9eb 100644 --- a/test/token-paymaster/btpm-undeployed-wallet.ts +++ b/test/token-paymaster/btpm-undeployed-wallet.ts @@ -196,10 +196,7 @@ describe("Biconomy Token Paymaster", function () { .addStake(86400, { value: parseEther("2") }); console.log("paymaster staked"); */ - await entryPoint.depositTo(paymasterAddress, { value: parseEther("2") }); - - // const resultSet = await entryPoint.getDepositInfo(paymasterAddress); - // console.log("deposited state ", resultSet); + await entryPoint.depositTo(paymasterAddress, { value: parseEther("2") }); }); describe("Token Payamster read methods and state checks", () => { diff --git a/test/token-paymaster/oracle-aggregator-specs.ts b/test/token-paymaster/oracle-aggregator-specs.ts index 8696fcc..2cf2451 100644 --- a/test/token-paymaster/oracle-aggregator-specs.ts +++ b/test/token-paymaster/oracle-aggregator-specs.ts @@ -225,9 +225,6 @@ describe("Biconomy Token Paymaster", function () { console.log("paymaster staked"); await entryPoint.depositTo(paymasterAddress, { value: parseEther("2") }); - - // const resultSet = await entryPoint.getDepositInfo(paymasterAddress); - // console.log("deposited state ", resultSet); }); describe("Oracle Aggregator returning unexpected values / using stale feed", () => {