From 85b53fec2d40e9897dd5d0f269e13fff7266c1ae Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Thu, 28 Nov 2024 17:24:49 +0400 Subject: [PATCH 1/5] init --- .../interfaces/IBiconomyTokenPaymaster.sol | 3 +- contracts/token/BiconomyTokenPaymaster.sol | 48 +++++++++++-------- .../concrete/TestTokenPaymaster.Base.t.sol | 7 ++- test/unit/concrete/TestTokenPaymaster.t.sol | 10 ++-- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 365411e..fdf82fa 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -14,7 +14,8 @@ interface IBiconomyTokenPaymaster { // Struct for storing information about the token struct TokenInfo { IOracle oracle; - uint256 decimals; + uint32 priceMarkup; + uint256 priceExpiryDuration; } event UpdatedUnaccountedGas(uint256 indexed oldValue, uint256 indexed newValue); diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index ee0f498..83ddf0d 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -50,8 +50,8 @@ contract BiconomyTokenPaymaster is // State variables address public verifyingSigner; // entity used to provide external token price and markup uint256 public unaccountedGas; - uint32 public independentPriceMarkup; // price markup used for independent mode - uint256 public priceExpiryDuration; // oracle price expiry duration + //uint32 public independentPriceMarkup; // price markup used for independent mode + //uint256 public priceExpiryDuration; // oracle price expiry duration IOracle public nativeAssetToUsdOracle; // ETH -> USD price oracle mapping(address => TokenInfo) public independentTokenDirectory; // mapping of token address => info for tokens // supported in // independent mode @@ -59,22 +59,23 @@ contract BiconomyTokenPaymaster is uint256 private constant _UNACCOUNTED_GAS_LIMIT = 200_000; // Limit for unaccounted gas cost uint32 private constant _PRICE_DENOMINATOR = 1e6; // Denominator used when calculating cost with price markup uint32 private constant _MAX_PRICE_MARKUP = 2e6; // 100% premium on price (2e6/PRICE_DENOMINATOR) - uint256 private immutable _NATIVE_TOKEN_DECIMALS; + uint256 private immutable _NATIVE_TOKEN_DECIMALS; // gas savings + uint256 private immutable _NATIVE_ASSET_PRICE_EXPIRY_DURATION; // gas savings constructor( address owner, address verifyingSignerArg, IEntryPoint entryPoint, uint256 unaccountedGasArg, - uint32 independentPriceMarkupArg, // price markup used for independent mode - uint256 priceExpiryDurationArg, + //uint32 independentPriceMarkupArg, // price markup used for independent mode + //uint256 priceExpiryDurationArg, uint256 nativeAssetDecimalsArg, IOracle nativeAssetToUsdOracleArg, + uint256 nativeAssetPriceExpiryDurationArg, IV3SwapRouter uniswapRouterArg, address wrappedNativeArg, - address[] memory independentTokensArg, // Array of token addresses supported by the paymaster in independent - // mode - IOracle[] memory oraclesArg, // Array of corresponding oracle addresses for independently supported tokens + address[] memory independentTokensArg, // Array of tokens supported in independent mode + TokenInfo[] memory tokenInfosArg, // Array of corresponding tokenInfo objects address[] memory swappableTokens, // Array of tokens that you want swappable by the uniswapper uint24[] memory swappableTokenPoolFeeTiers // Array of uniswap pool fee tiers for each swappable token ) @@ -82,6 +83,8 @@ contract BiconomyTokenPaymaster is Uniswapper(uniswapRouterArg, wrappedNativeArg, swappableTokens, swappableTokenPoolFeeTiers) { _NATIVE_TOKEN_DECIMALS = nativeAssetDecimalsArg; + _NATIVE_ASSET_PRICE_EXPIRY_DURATION = nativeAssetPriceExpiryDurationArg; + if (_isContract(verifyingSignerArg)) { revert VerifyingSignerCanNotBeContract(); } @@ -122,7 +125,12 @@ contract BiconomyTokenPaymaster is revert InvalidOracleDecimals(); } independentTokenDirectory[independentTokensArg[i]] = - TokenInfo(oraclesArg[i], 10 ** IERC20Metadata(independentTokensArg[i]).decimals()); + TokenInfo( + tokenInfosArg[i].oracle, + tokenInfosArg[i].priceMarkup, + tokenInfosArg[i].priceExpiryDuration + ); + //10 ** IERC20Metadata(independentTokensArg[i]).decimals()); } } @@ -304,16 +312,15 @@ contract BiconomyTokenPaymaster is * @param oracle The oracle to use for the specified token * @notice only to be called by the owner of the contract. */ - function addToTokenDirectory(address tokenAddress, IOracle oracle) external payable onlyOwner { - if (oracle.decimals() != 8) { + function addToTokenDirectory(address tokenAddress, TokenInfo tokenInfo) external payable onlyOwner { + if (tokenInfo.oracle.decimals() != 8) { // Token -> USD will always have 8 decimals revert InvalidOracleDecimals(); } - uint8 decimals = IERC20Metadata(tokenAddress).decimals(); - independentTokenDirectory[tokenAddress] = TokenInfo(oracle, 10 ** decimals); + independentTokenDirectory[tokenAddress] = tokenInfo; - emit AddedToTokenDirectory(tokenAddress, oracle, decimals); + emit AddedToTokenDirectory(tokenAddress, tokenInfo.oracle, tokenInfo.oracle.decimals()); } /** @@ -555,11 +562,12 @@ contract BiconomyTokenPaymaster is revert TokenNotSupported(); } uint256 tokenAmount; + uint32 priceMarkup = independentTokenDirectory[tokenAddress].priceMarkup; { // Calculate token amount to precharge uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp); - tokenAmount = ((maxCost + maxPenalty + (unaccountedGas * maxFeePerGas)) * independentPriceMarkup * tokenPrice) + tokenAmount = ((maxCost + maxPenalty + (unaccountedGas * maxFeePerGas)) * priceMarkup * tokenPrice) / (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR); } @@ -570,9 +578,9 @@ contract BiconomyTokenPaymaster is abi.encode( userOp.sender, tokenAddress, - tokenAmount-((maxPenalty*tokenPrice*independentPriceMarkup)/(_NATIVE_TOKEN_DECIMALS*_PRICE_DENOMINATOR)), + tokenAmount-((maxPenalty*tokenPrice*priceMarkup)/(_NATIVE_TOKEN_DECIMALS*_PRICE_DENOMINATOR)), tokenPrice, - independentPriceMarkup, + priceMarkup, userOpHash ); validationData = 0; // Validation success and price is valid indefinetly @@ -633,8 +641,8 @@ contract BiconomyTokenPaymaster is } // Calculate price by using token and native oracle - uint256 tokenPrice = _fetchPrice(tokenInfo.oracle); - uint256 nativeAssetPrice = _fetchPrice(nativeAssetToUsdOracle); + uint256 tokenPrice = _fetchPrice(tokenInfo.oracle, tokenInfo.priceExpiryDuration); + uint256 nativeAssetPrice = _fetchPrice(nativeAssetToUsdOracle, _NATIVE_ASSET_PRICE_EXPIRY_DURATION); // Adjust to token decimals price = (nativeAssetPrice * tokenInfo.decimals) / tokenPrice; @@ -645,7 +653,7 @@ contract BiconomyTokenPaymaster is /// @param oracle The oracle contract to fetch the price from. /// @return price The latest price fetched from the oracle. /// Note: We could do this using oracle aggregator, so we can also use Pyth. or Twap based oracle and just not chainlink. - function _fetchPrice(IOracle oracle) internal view returns (uint256 price) { + function _fetchPrice(IOracle oracle, uint256 priceExpiryDuration) internal view returns (uint256 price) { (, int256 answer,, uint256 updatedAt,) = oracle.latestRoundData(); if (answer <= 0) { revert OraclePriceNotPositive(); diff --git a/test/unit/concrete/TestTokenPaymaster.Base.t.sol b/test/unit/concrete/TestTokenPaymaster.Base.t.sol index 54a8c4f..ca7b678 100644 --- a/test/unit/concrete/TestTokenPaymaster.Base.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.Base.t.sol @@ -38,14 +38,13 @@ contract TestTokenPaymasterBase is TestBase { PAYMASTER_SIGNER.addr, ENTRYPOINT, 50000, // unaccounted gas - 1e6, // price markup (for independent mode) - 1 days, // price expiry duration - 1e18, // native token decimals + 1e18, // native asset decimals nativeOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(usdc)), - _toSingletonArray(IOracle(address(tokenOracle))), + _toSingletonArray(TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), _toSingletonArray(address(usdc)), _toSingletonArray(uint24(500)) // from here: https://basescan.org/address/0xd0b53D9277642d899DF5C87A3966A349A798F224#readContract ); diff --git a/test/unit/concrete/TestTokenPaymaster.t.sol b/test/unit/concrete/TestTokenPaymaster.t.sol index c787c03..f4828a6 100644 --- a/test/unit/concrete/TestTokenPaymaster.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.t.sol @@ -6,7 +6,8 @@ import { BiconomyTokenPaymaster, IBiconomyTokenPaymaster, BiconomyTokenPaymasterErrors, - IOracle + IOracle, + TokenInfo } from "../../../contracts/token/BiconomyTokenPaymaster.sol"; import { MockOracle } from "../../mocks/MockOracle.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; @@ -41,14 +42,13 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_SIGNER.addr, ENTRYPOINT, 50000, // unaccounted gas - 1e6, // price markup - 1 days, // price expiry duration - 1e18, // native token decimals + 1e18, // native asset decimals nativeAssetToUsdOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))), + _toSingletonArray(TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), new address[](0), new uint24[](0) ); From f49376796c6d80c3b7a3bc8244da1795fe9445a9 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Fri, 29 Nov 2024 12:30:25 +0400 Subject: [PATCH 2/5] debug --- .../interfaces/IBiconomyTokenPaymaster.sol | 6 +- contracts/token/BiconomyTokenPaymaster.sol | 79 +++++++++++++------ test/base/TestBase.sol | 7 +- .../concrete/TestTokenPaymaster.Base.t.sol | 11 +-- test/unit/concrete/TestTokenPaymaster.t.sol | 53 ++++++------- 5 files changed, 90 insertions(+), 66 deletions(-) diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index fdf82fa..9fc43e8 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -47,11 +47,11 @@ interface IBiconomyTokenPaymaster { function setUnaccountedGas(uint256 value) external payable; - function setPriceMarkup(uint32 newPriceMarkup) external payable; + function setPriceMarkupForToken(address tokenAddress, uint32 newPriceMarkup) external payable; - function setPriceExpiryDuration(uint256 newPriceExpiryDuration) external payable; + function setPriceExpiryDurationForToken(address tokenAddress, uint256 newPriceExpiryDuration) external payable; function setNativeAssetToUsdOracle(IOracle oracle) external payable; - function addToTokenDirectory(address tokenAddress, IOracle oracle) external payable; + function addToTokenDirectory(address tokenAddress, TokenInfo memory tokenInfo) external payable; } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 83ddf0d..67b6355 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -18,6 +18,8 @@ import { ECDSA as ECDSA_solady } from "solady/utils/ECDSA.sol"; import "account-abstraction/core/Helpers.sol"; import { Uniswapper, IV3SwapRouter } from "./swaps/Uniswapper.sol"; +import "forge-std/console2.sol"; + /** * @title BiconomyTokenPaymaster * @author ShivaanshK @@ -94,36 +96,37 @@ contract BiconomyTokenPaymaster is if (unaccountedGasArg > _UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } - if (independentPriceMarkupArg > _MAX_PRICE_MARKUP || independentPriceMarkupArg < _PRICE_DENOMINATOR) { - // Not between 0% and 100% markup - revert InvalidPriceMarkup(); - } - if (independentTokensArg.length != oraclesArg.length) { + + if (independentTokensArg.length != tokenInfosArg.length) { revert TokensAndInfoLengthMismatch(); } if (nativeAssetToUsdOracleArg.decimals() != 8) { // ETH -> USD will always have 8 decimals for Chainlink and TWAP revert InvalidOracleDecimals(); } - if (block.timestamp < priceExpiryDurationArg) { - revert InvalidPriceExpiryDuration(); - } // Set state variables assembly ("memory-safe") { sstore(verifyingSigner.slot, verifyingSignerArg) sstore(unaccountedGas.slot, unaccountedGasArg) - sstore(independentPriceMarkup.slot, independentPriceMarkupArg) - sstore(priceExpiryDuration.slot, priceExpiryDurationArg) + //sstore(independentPriceMarkup.slot, independentPriceMarkupArg) + //sstore(priceExpiryDuration.slot, priceExpiryDurationArg) sstore(nativeAssetToUsdOracle.slot, nativeAssetToUsdOracleArg) } // Populate the tokenToOracle mapping for (uint256 i = 0; i < independentTokensArg.length; i++) { - if (oraclesArg[i].decimals() != 8) { + if (tokenInfosArg[i].oracle.decimals() != 8) { // Token -> USD will always have 8 decimals revert InvalidOracleDecimals(); } + if (tokenInfosArg[i].priceMarkup > _MAX_PRICE_MARKUP || tokenInfosArg[i].priceMarkup < _PRICE_DENOMINATOR) { + // Not between 0% and 100% markup + revert InvalidPriceMarkup(); + } + if (block.timestamp < tokenInfosArg[i].priceExpiryDuration) { + revert InvalidPriceExpiryDuration(); + } independentTokenDirectory[independentTokensArg[i]] = TokenInfo( tokenInfosArg[i].oracle, @@ -261,15 +264,13 @@ contract BiconomyTokenPaymaster is * @param newIndependentPriceMarkup The new value to be set as the price markup * @notice only to be called by the owner of the contract. */ - function setPriceMarkup(uint32 newIndependentPriceMarkup) external payable onlyOwner { + function setPriceMarkupForToken(address tokenAddress, uint32 newIndependentPriceMarkup) external payable onlyOwner { if (newIndependentPriceMarkup > _MAX_PRICE_MARKUP || newIndependentPriceMarkup < _PRICE_DENOMINATOR) { // Not between 0% and 100% markup revert InvalidPriceMarkup(); } - uint32 oldIndependentPriceMarkup = independentPriceMarkup; - assembly ("memory-safe") { - sstore(independentPriceMarkup.slot, newIndependentPriceMarkup) - } + uint32 oldIndependentPriceMarkup = independentTokenDirectory[tokenAddress].priceMarkup; + independentTokenDirectory[tokenAddress].priceMarkup = newIndependentPriceMarkup; emit UpdatedFixedPriceMarkup(oldIndependentPriceMarkup, newIndependentPriceMarkup); } @@ -278,12 +279,10 @@ contract BiconomyTokenPaymaster is * @param newPriceExpiryDuration The new value to be set as the price expiry duration * @notice only to be called by the owner of the contract. */ - function setPriceExpiryDuration(uint256 newPriceExpiryDuration) external payable onlyOwner { + function setPriceExpiryDurationForToken(address tokenAddress, uint256 newPriceExpiryDuration) external payable onlyOwner { if(block.timestamp < newPriceExpiryDuration) revert InvalidPriceExpiryDuration(); - uint256 oldPriceExpiryDuration = priceExpiryDuration; - assembly ("memory-safe") { - sstore(priceExpiryDuration.slot, newPriceExpiryDuration) - } + uint256 oldPriceExpiryDuration = independentTokenDirectory[tokenAddress].priceExpiryDuration; + independentTokenDirectory[tokenAddress].priceExpiryDuration = newPriceExpiryDuration; emit UpdatedPriceExpiryDuration(oldPriceExpiryDuration, newPriceExpiryDuration); } @@ -309,10 +308,10 @@ contract BiconomyTokenPaymaster is /** * @dev Set or update a TokenInfo entry in the independentTokenDirectory mapping. * @param tokenAddress The token address to add or update in directory - * @param oracle The oracle to use for the specified token + * @param tokenInfo The TokenInfo struct to add or update * @notice only to be called by the owner of the contract. */ - function addToTokenDirectory(address tokenAddress, TokenInfo tokenInfo) external payable onlyOwner { + function addToTokenDirectory(address tokenAddress, TokenInfo memory tokenInfo) external payable onlyOwner { if (tokenInfo.oracle.decimals() != 8) { // Token -> USD will always have 8 decimals revert InvalidOracleDecimals(); @@ -330,7 +329,7 @@ contract BiconomyTokenPaymaster is */ function removeFromTokenDirectory(address tokenAddress) external payable onlyOwner { delete independentTokenDirectory[tokenAddress]; - emit RemovedFromTokenDirectory(tokenAddress ); + emit RemovedFromTokenDirectory(tokenAddress); } /** @@ -463,6 +462,24 @@ contract BiconomyTokenPaymaster is return independentTokenDirectory[tokenAddress].oracle != IOracle(address(0)); } + /** + * @dev Get the price markup for a token + * @param tokenAddress The address of the token to get the price markup of + * @return priceMarkup The price markup for the token + */ + function independentPriceMarkup(address tokenAddress) public view returns (uint32) { + return independentTokenDirectory[tokenAddress].priceMarkup; + } + + /** + * @dev Get the price expiry duration for a token + * @param tokenAddress The address of the token to get the price expiry duration of + * @return priceExpiryDuration The price expiry duration for the token + */ + function independentPriceExpiryDuration(address tokenAddress) public view returns (uint256) { + return independentTokenDirectory[tokenAddress].priceExpiryDuration; + } + /** * @dev Validate a user operation. * This method is abstract in BasePaymaster and must be implemented in derived contracts. @@ -613,10 +630,18 @@ contract BiconomyTokenPaymaster is bytes32 userOpHash ) = abi.decode(context, (address, address, uint256, uint256, uint32, bytes32)); + + console2.log("actualGasCost", actualGasCost); + console2.log("unaccountedGas", unaccountedGas); + console2.log("actualUserOpFeePerGas", actualUserOpFeePerGas); + console2.log("appliedPriceMarkup", appliedPriceMarkup); + console2.log("tokenPrice", tokenPrice); // Calculate the actual cost in tokens based on the actual gas cost and the token price uint256 actualTokenAmount = ( (actualGasCost + (unaccountedGas * actualUserOpFeePerGas)) * appliedPriceMarkup * tokenPrice ) / (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR); + console2.log("actualTokenAmount", actualTokenAmount); + console2.log("prechargedAmount ", prechargedAmount); if (prechargedAmount > actualTokenAmount) { // If the user was overcharged, refund the excess tokens uint256 refundAmount = prechargedAmount - actualTokenAmount; @@ -644,8 +669,12 @@ contract BiconomyTokenPaymaster is uint256 tokenPrice = _fetchPrice(tokenInfo.oracle, tokenInfo.priceExpiryDuration); uint256 nativeAssetPrice = _fetchPrice(nativeAssetToUsdOracle, _NATIVE_ASSET_PRICE_EXPIRY_DURATION); + console2.log("tokenPrice", tokenPrice); + console2.log("nativeAssetPrice", nativeAssetPrice); + // Adjust to token decimals - price = (nativeAssetPrice * tokenInfo.decimals) / tokenPrice; + price = (nativeAssetPrice * tokenInfo.oracle.decimals()) / tokenPrice; + console2.log("price", price); } /// @notice Fetches the latest price from the given oracle. diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index 414be2d..efe7333 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -20,6 +20,7 @@ import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { BiconomySponsorshipPaymaster } from "../../contracts/sponsorship/BiconomySponsorshipPaymaster.sol"; +import { IBiconomyTokenPaymaster } from "../../contracts/interfaces/IBiconomyTokenPaymaster.sol"; import { BiconomyTokenPaymaster, @@ -499,9 +500,9 @@ abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { return array; } - function _toSingletonArray(IOracle oracle) internal pure returns (IOracle[] memory) { - IOracle[] memory array = new IOracle[](1); - array[0] = oracle; + function _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo memory tokenInfo) internal pure returns (IBiconomyTokenPaymaster.TokenInfo[] memory) { + IBiconomyTokenPaymaster.TokenInfo[] memory array = new IBiconomyTokenPaymaster.TokenInfo[](1); + array[0] = tokenInfo; return array; } } \ No newline at end of file diff --git a/test/unit/concrete/TestTokenPaymaster.Base.t.sol b/test/unit/concrete/TestTokenPaymaster.Base.t.sol index ca7b678..e5e6347 100644 --- a/test/unit/concrete/TestTokenPaymaster.Base.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.Base.t.sol @@ -8,6 +8,7 @@ import { BiconomyTokenPaymasterErrors, IOracle } from "../../../contracts/token/BiconomyTokenPaymaster.sol"; +import { IBiconomyTokenPaymaster } from "../../../contracts/interfaces/IBiconomyTokenPaymaster.sol"; import { MockOracle } from "../../mocks/MockOracle.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -44,7 +45,7 @@ contract TestTokenPaymasterBase is TestBase { swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(usdc)), - _toSingletonArray(TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), _toSingletonArray(address(usdc)), _toSingletonArray(uint24(500)) // from here: https://basescan.org/address/0xd0b53D9277642d899DF5C87A3966A349A798F224#readContract ); @@ -57,14 +58,13 @@ contract TestTokenPaymasterBase is TestBase { PAYMASTER_SIGNER.addr, ENTRYPOINT, 50000, // unaccounted gas - 1e6, // price markup - 1 days, // price expiry duration 1e18, // native token decimals nativeOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(usdc)), - _toSingletonArray(IOracle(address(tokenOracle))), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), _toSingletonArray(address(usdc)), _toSingletonArray(uint24(500)) // from here: https://basescan.org/address/0xd0b53D9277642d899DF5C87A3966A349A798F224#readContract ); @@ -74,7 +74,8 @@ contract TestTokenPaymasterBase is TestBase { assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(address(testArtifact.nativeAssetToUsdOracle()), address(nativeOracle)); assertEq(testArtifact.unaccountedGas(), 50000); - assertEq(testArtifact.independentPriceMarkup(), 1e6); + assertEq(testArtifact.independentPriceMarkup(address(usdc)), 1e6); + assertEq(testArtifact.independentPriceExpiryDuration(address(usdc)), 1 days); } function test_BaseFork_Success_TokenPaymaster_IndependentMode_WithoutPremium() external { diff --git a/test/unit/concrete/TestTokenPaymaster.t.sol b/test/unit/concrete/TestTokenPaymaster.t.sol index f4828a6..44a8140 100644 --- a/test/unit/concrete/TestTokenPaymaster.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.t.sol @@ -6,9 +6,9 @@ import { BiconomyTokenPaymaster, IBiconomyTokenPaymaster, BiconomyTokenPaymasterErrors, - IOracle, - TokenInfo + IOracle } from "../../../contracts/token/BiconomyTokenPaymaster.sol"; +import { IBiconomyTokenPaymaster } from "../../../contracts/interfaces/IBiconomyTokenPaymaster.sol"; import { MockOracle } from "../../mocks/MockOracle.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -48,7 +48,7 @@ contract TestTokenPaymaster is TestBase { swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), new address[](0), new uint24[](0) ); @@ -61,14 +61,13 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_SIGNER.addr, ENTRYPOINT, 5000, // unaccounted gas - 1e6, // price markup - 1 days, // price expiry duration 1e18, // native token decimals nativeAssetToUsdOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), new address[](0), new uint24[](0) ); @@ -78,7 +77,8 @@ contract TestTokenPaymaster is TestBase { assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(address(testArtifact.nativeAssetToUsdOracle()), address(nativeAssetToUsdOracle)); assertEq(testArtifact.unaccountedGas(), 5000); - assertEq(testArtifact.independentPriceMarkup(), 1e6); + assertEq(testArtifact.independentPriceMarkup(address(testToken)), 1e6); + assertEq(testArtifact.independentPriceExpiryDuration(address(testToken)), 1 days); } function test_RevertIf_DeployWithSignerSetToZero() external { @@ -88,15 +88,14 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_OWNER.addr, address(0), ENTRYPOINT, - 5000, // unaccounted gas - 1e6, // price markup - 1 days, // price expiry duration + 5000, // unaccounted gas 1e18, // native token decimals nativeAssetToUsdOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), new address[](0), new uint24[](0) ); @@ -110,14 +109,13 @@ contract TestTokenPaymaster is TestBase { ENTRYPOINT_ADDRESS, ENTRYPOINT, 5000, // unaccounted gas - 1e6, // price markup - 1 days, // price expiry duration 1e18, // native token decimals nativeAssetToUsdOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), new address[](0), new uint24[](0) ); @@ -130,14 +128,13 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_SIGNER.addr, ENTRYPOINT, 500_001, // unaccounted gas - 1e6, // price markup - 1 days, // price expiry duration 1e18, // native token decimals nativeAssetToUsdOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), new address[](0), new uint24[](0) ); @@ -150,14 +147,13 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_SIGNER.addr, ENTRYPOINT, 5000, // unaccounted gas - 2e6 + 1, // price markup - 1 days, // price expiry duration 1e18, // native token decimals nativeAssetToUsdOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(tokenOracle)), 2e6 + 1, 1 days)), new address[](0), new uint24[](0) ); @@ -224,14 +220,15 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_SIGNER.addr, ENTRYPOINT, 5000, // unaccounted gas - 1e6, // price markup - 1 days, // price expiry duration + //1e6, // price markup + //1 days, // price expiry duration 1e18, // native token decimals invalidOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(tokenOracle)), 1e6, 1 days)), new address[](0), new uint24[](0) ); @@ -245,14 +242,13 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_SIGNER.addr, ENTRYPOINT, 50_000, // unaccounted gas - 1e6, // price markup - 1 days, // price expiry duration 1e18, // native token decimals nativeAssetToUsdOracle, + 1 days, // native asset price expiry duration swapRouter, WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(invalidOracle))), + _toSingletonArray(IBiconomyTokenPaymaster.TokenInfo(IOracle(address(invalidOracle)), 1e6, 1 days)), new address[](0), new uint24[](0) ); @@ -346,7 +342,7 @@ contract TestTokenPaymaster is TestBase { // Test setting a high price markup function test_SetPriceMarkupTooHigh() external prankModifier(PAYMASTER_OWNER.addr) { vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidPriceMarkup.selector); - tokenPaymaster.setPriceMarkup(2e6 + 1); // Setting too high + tokenPaymaster.setPriceMarkupForToken(address(testToken), 2e6 + 1); // Setting too high } // Test invalid signature in external mode @@ -504,13 +500,10 @@ contract TestTokenPaymaster is TestBase { PackedUserOperation[] memory ops = new PackedUserOperation[](1); ops[0] = userOp; - vm.expectEmit(true, true, false, false, address(tokenPaymaster)); emit IBiconomyTokenPaymaster.TokensRefunded(address(ALICE_ACCOUNT), address(testToken), 0, bytes32(0)); - vm.expectEmit(true, true, false, false, address(tokenPaymaster)); emit IBiconomyTokenPaymaster.PaidGasInTokens(address(ALICE_ACCOUNT), address(testToken), 0, 0, 1e6, 0, bytes32(0)); - startPrank(BUNDLER.addr); uint256 gasValue = gasleft(); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); From 56d0aed1c846fbb7a079e12487071e2ce639ac60 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Fri, 29 Nov 2024 13:09:09 +0400 Subject: [PATCH 3/5] per token --- contracts/token/BiconomyTokenPaymaster.sol | 27 ++-------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 67b6355..3e9e50c 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -18,8 +18,6 @@ import { ECDSA as ECDSA_solady } from "solady/utils/ECDSA.sol"; import "account-abstraction/core/Helpers.sol"; import { Uniswapper, IV3SwapRouter } from "./swaps/Uniswapper.sol"; -import "forge-std/console2.sol"; - /** * @title BiconomyTokenPaymaster * @author ShivaanshK @@ -52,12 +50,8 @@ contract BiconomyTokenPaymaster is // State variables address public verifyingSigner; // entity used to provide external token price and markup uint256 public unaccountedGas; - //uint32 public independentPriceMarkup; // price markup used for independent mode - //uint256 public priceExpiryDuration; // oracle price expiry duration IOracle public nativeAssetToUsdOracle; // ETH -> USD price oracle mapping(address => TokenInfo) public independentTokenDirectory; // mapping of token address => info for tokens - // supported in // independent mode - uint256 private constant _UNACCOUNTED_GAS_LIMIT = 200_000; // Limit for unaccounted gas cost uint32 private constant _PRICE_DENOMINATOR = 1e6; // Denominator used when calculating cost with price markup uint32 private constant _MAX_PRICE_MARKUP = 2e6; // 100% premium on price (2e6/PRICE_DENOMINATOR) @@ -69,8 +63,6 @@ contract BiconomyTokenPaymaster is address verifyingSignerArg, IEntryPoint entryPoint, uint256 unaccountedGasArg, - //uint32 independentPriceMarkupArg, // price markup used for independent mode - //uint256 priceExpiryDurationArg, uint256 nativeAssetDecimalsArg, IOracle nativeAssetToUsdOracleArg, uint256 nativeAssetPriceExpiryDurationArg, @@ -109,8 +101,6 @@ contract BiconomyTokenPaymaster is assembly ("memory-safe") { sstore(verifyingSigner.slot, verifyingSignerArg) sstore(unaccountedGas.slot, unaccountedGasArg) - //sstore(independentPriceMarkup.slot, independentPriceMarkupArg) - //sstore(priceExpiryDuration.slot, priceExpiryDurationArg) sstore(nativeAssetToUsdOracle.slot, nativeAssetToUsdOracleArg) } @@ -133,7 +123,6 @@ contract BiconomyTokenPaymaster is tokenInfosArg[i].priceMarkup, tokenInfosArg[i].priceExpiryDuration ); - //10 ** IERC20Metadata(independentTokensArg[i]).decimals()); } } @@ -319,7 +308,7 @@ contract BiconomyTokenPaymaster is independentTokenDirectory[tokenAddress] = tokenInfo; - emit AddedToTokenDirectory(tokenAddress, tokenInfo.oracle, tokenInfo.oracle.decimals()); + emit AddedToTokenDirectory(tokenAddress, tokenInfo.oracle, IERC20Metadata(tokenAddress).decimals()); } /** @@ -630,18 +619,10 @@ contract BiconomyTokenPaymaster is bytes32 userOpHash ) = abi.decode(context, (address, address, uint256, uint256, uint32, bytes32)); - - console2.log("actualGasCost", actualGasCost); - console2.log("unaccountedGas", unaccountedGas); - console2.log("actualUserOpFeePerGas", actualUserOpFeePerGas); - console2.log("appliedPriceMarkup", appliedPriceMarkup); - console2.log("tokenPrice", tokenPrice); // Calculate the actual cost in tokens based on the actual gas cost and the token price uint256 actualTokenAmount = ( (actualGasCost + (unaccountedGas * actualUserOpFeePerGas)) * appliedPriceMarkup * tokenPrice ) / (_NATIVE_TOKEN_DECIMALS * _PRICE_DENOMINATOR); - console2.log("actualTokenAmount", actualTokenAmount); - console2.log("prechargedAmount ", prechargedAmount); if (prechargedAmount > actualTokenAmount) { // If the user was overcharged, refund the excess tokens uint256 refundAmount = prechargedAmount - actualTokenAmount; @@ -669,12 +650,8 @@ contract BiconomyTokenPaymaster is uint256 tokenPrice = _fetchPrice(tokenInfo.oracle, tokenInfo.priceExpiryDuration); uint256 nativeAssetPrice = _fetchPrice(nativeAssetToUsdOracle, _NATIVE_ASSET_PRICE_EXPIRY_DURATION); - console2.log("tokenPrice", tokenPrice); - console2.log("nativeAssetPrice", nativeAssetPrice); - // Adjust to token decimals - price = (nativeAssetPrice * tokenInfo.oracle.decimals()) / tokenPrice; - console2.log("price", price); + price = (nativeAssetPrice * 10**IERC20Metadata(tokenAddress).decimals()) / tokenPrice; } /// @notice Fetches the latest price from the given oracle. From 4725ef351aed8ee4169038a0e8ee3c98f9a19df2 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Fri, 29 Nov 2024 13:18:08 +0400 Subject: [PATCH 4/5] inline docs --- contracts/token/BiconomyTokenPaymaster.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 3e9e50c..39d283a 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -58,6 +58,13 @@ contract BiconomyTokenPaymaster is uint256 private immutable _NATIVE_TOKEN_DECIMALS; // gas savings uint256 private immutable _NATIVE_ASSET_PRICE_EXPIRY_DURATION; // gas savings + /** + * @dev markup and expiry duration are provided for each token. + * Price expiry duration should be set to the heartbeat value of the token. + * Additionally, priceMarkup must be higher than Chainlink’s deviation threshold value. + * More: https://docs.chain.link/architecture-overview/architecture-decentralized-model + */ + constructor( address owner, address verifyingSignerArg, From b834dca92cea05847d152acca2cff858e32d1736 Mon Sep 17 00:00:00 2001 From: Filipp Makarov Date: Mon, 2 Dec 2024 10:58:44 +0300 Subject: [PATCH 5/5] checks + refactor --- contracts/token/BiconomyTokenPaymaster.sol | 37 +++++++++------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 39d283a..81c0820 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -111,25 +111,9 @@ contract BiconomyTokenPaymaster is sstore(nativeAssetToUsdOracle.slot, nativeAssetToUsdOracleArg) } - // Populate the tokenToOracle mapping for (uint256 i = 0; i < independentTokensArg.length; i++) { - if (tokenInfosArg[i].oracle.decimals() != 8) { - // Token -> USD will always have 8 decimals - revert InvalidOracleDecimals(); - } - if (tokenInfosArg[i].priceMarkup > _MAX_PRICE_MARKUP || tokenInfosArg[i].priceMarkup < _PRICE_DENOMINATOR) { - // Not between 0% and 100% markup - revert InvalidPriceMarkup(); - } - if (block.timestamp < tokenInfosArg[i].priceExpiryDuration) { - revert InvalidPriceExpiryDuration(); - } - independentTokenDirectory[independentTokensArg[i]] = - TokenInfo( - tokenInfosArg[i].oracle, - tokenInfosArg[i].priceMarkup, - tokenInfosArg[i].priceExpiryDuration - ); + _validateTokenInfo(tokenInfosArg[i]); + independentTokenDirectory[independentTokensArg[i]] = tokenInfosArg[i]; } } @@ -308,10 +292,7 @@ contract BiconomyTokenPaymaster is * @notice only to be called by the owner of the contract. */ function addToTokenDirectory(address tokenAddress, TokenInfo memory tokenInfo) external payable onlyOwner { - if (tokenInfo.oracle.decimals() != 8) { - // Token -> USD will always have 8 decimals - revert InvalidOracleDecimals(); - } + _validateTokenInfo(tokenInfo); independentTokenDirectory[tokenAddress] = tokenInfo; @@ -642,6 +623,18 @@ contract BiconomyTokenPaymaster is ); } + function _validateTokenInfo(TokenInfo memory tokenInfo) internal view { + if (tokenInfo.oracle.decimals() != 8) { + revert InvalidOracleDecimals(); + } + if (tokenInfo.priceMarkup > _MAX_PRICE_MARKUP || tokenInfo.priceMarkup < _PRICE_DENOMINATOR) { + revert InvalidPriceMarkup(); + } + if (block.timestamp < tokenInfo.priceExpiryDuration) { + revert InvalidPriceExpiryDuration(); + } + } + /// @notice Fetches the latest token price. /// @return price The latest token price fetched from the oracles. function _getPrice(address tokenAddress) internal view returns (uint256 price) {