From 4e2a0e54d4a8b83c9ecda7896fc4b2cd26f882fa Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 11 Sep 2024 18:17:18 +0400 Subject: [PATCH 01/10] uniswapper to perform token to weth swaps through uniswaps --- .../interfaces/IBiconomyTokenPaymaster.sol | 2 +- contracts/token/BiconomyTokenPaymaster.sol | 24 +++-- contracts/token/swaps/Uniswapper.sol | 87 +++++++++++++++++++ test/unit/concrete/TestTokenPaymaster.t.sol | 4 +- 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 contracts/token/swaps/Uniswapper.sol diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index b5e1f2f..0a3a2a5 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -46,7 +46,7 @@ interface IBiconomyTokenPaymaster { function setPriceExpiryDuration(uint256 _newPriceExpiryDuration) external payable; - function setNativeOracle(IOracle _oracle) external payable; + function setNativeAssetToUsdOracle(IOracle _oracle) external payable; function updateTokenDirectory(address _tokenAddress, IOracle _oracle) external payable; } diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index e49743f..02b836b 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -15,6 +15,8 @@ import { TokenPaymasterParserLib } from "../libraries/TokenPaymasterParserLib.so import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; /** * @title BiconomyTokenPaymaster @@ -62,8 +64,8 @@ contract BiconomyTokenPaymaster is IEntryPoint _entryPoint, uint256 _unaccountedGas, uint256 _priceMarkup, - IOracle _nativeAssetToUsdOracle, uint256 _priceExpiryDuration, + IOracle _nativeAssetToUsdOracle, address[] memory _tokens, // Array of token addresses IOracle[] memory _oracles // Array of corresponding oracle addresses ) @@ -266,18 +268,18 @@ contract BiconomyTokenPaymaster is * @param _oracle The new native asset oracle * @notice only to be called by the owner of the contract. */ - function setNativeOracle(IOracle _oracle) external payable override onlyOwner { + function setNativeAssetToUsdOracle(IOracle _oracle) external payable override onlyOwner { if (_oracle.decimals() != 8) { // Native -> USD will always have 8 decimals revert InvalidOracleDecimals(); } - IOracle oldNativeOracle = nativeAssetToUsdOracle; + IOracle oldNativeAssetToUsdOracle = nativeAssetToUsdOracle; assembly ("memory-safe") { sstore(nativeAssetToUsdOracle.slot, _oracle) } - emit UpdatedNativeAssetOracle(oldNativeOracle, _oracle); + emit UpdatedNativeAssetOracle(oldNativeAssetToUsdOracle, _oracle); } /** @@ -298,6 +300,17 @@ contract BiconomyTokenPaymaster is emit UpdatedTokenDirectory(_tokenAddress, _oracle, decimals); } + /** + * @dev Swap a token in the paymaster for ETH to increase its entry point deposit + * @param _swapRouter The address of the swap router to use to facilitate the swap + * @param _tokenAddress The token address of the token to swap for ETH + * @param _tokenAmount The amount of the token to swap + * @notice only to be called by the owner of the contract. + */ + function swapTokenAndDeposit(ISwapRouter _swapRouter, address _tokenAddress, uint256 _tokenAmount) external payable onlyOwner { + + } + /** * return the hash we're going to sign off-chain (and validate on-chain) * this method is called by the off-chain service, to sign the request. @@ -405,8 +418,7 @@ contract BiconomyTokenPaymaster is // Transfer full amount to this address. Unused amount will be refunded in postOP SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); - context = - abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, externalPriceMarkup, userOpHash); + context = abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, externalPriceMarkup, userOpHash); validationData = _packValidationData(false, validUntil, validAfter); } else if (mode == PaymasterMode.INDEPENDENT) { // Use only oracles for the token specified in modeSpecificData diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol new file mode 100644 index 0000000..a4d88f9 --- /dev/null +++ b/contracts/token/swaps/Uniswapper.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.23; + +/* solhint-disable not-rely-on-time */ + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryPayments.sol"; + +abstract contract Uniswapper { + uint256 private constant SWAP_PRICE_DENOMINATOR = 1e26; + + /// @notice The Uniswap V3 SwapRouter contract + ISwapRouter public immutable uniswapRouter; + + /// @notice The ERC-20 token that wraps the native asset for current chain + address public immutable wrappedNative; + + // Token address -> Fee tier of the pool to swap through + mapping(address => uint24) public tokenToPools; + + event UniswapReverted(address indexed tokenIn, address indexed tokenOut, uint256 amountIn); + + error TokensAndAmountsLengthMismatch(); + + constructor( + ISwapRouter _uniswapRouter, + address _wrappedNative, + address[] memory _tokens, + uint24[] memory _tokenPools + ) { + if (_tokens.length != _tokenPools.length) { + revert TokensAndAmountsLengthMismatch(); + } + + // Set router and native wrapped asset addresses + uniswapRouter = _uniswapRouter; + wrappedNative = _wrappedNative; + + for (uint256 i = 0; i < _tokens.length; ++i) { + IERC20(_tokens[i]).approve(address(_uniswapRouter), type(uint256).max); // one time max approval + tokenToPools[_tokens[i]] = _tokenPools[i]; // set mapping of token to uniswap pool to use for swap + } + } + + function _setTokenPool(address _token, uint24 _feeTier) internal { + tokenToPools[_token] = _feeTier; // set mapping of token to uniswap pool to use for swap + } + + function _swapTokenToWeth(address _tokenIn, uint256 _amountIn) internal returns (uint256 amountOut) { + uint24 poolFee = tokenToPools[_tokenIn]; + + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: _tokenIn, + tokenOut: wrappedNative, + fee: poolFee, + recipient: address(this), + deadline: block.timestamp, + amountIn: _amountIn, + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + }); + try uniswapRouter.exactInputSingle(params) returns (uint256 _amountOut) { + amountOut = _amountOut; + } catch { + emit UniswapReverted(_tokenIn, wrappedNative, _amountIn); + amountOut = 0; + } + } + + function addSlippage(uint256 amount, uint8 slippage) private pure returns (uint256) { + return amount * (1000 - slippage) / 1000; + } + + function tokenToWei(uint256 amount, uint256 price) public pure returns (uint256) { + return amount * price / SWAP_PRICE_DENOMINATOR; + } + + function weiToToken(uint256 amount, uint256 price) public pure returns (uint256) { + return amount * SWAP_PRICE_DENOMINATOR / price; + } + + function unwrapWeth(uint256 _amount) internal { + IPeripheryPayments(address(uniswapRouter)).unwrapWETH9(_amount, address(this)); + } +} diff --git a/test/unit/concrete/TestTokenPaymaster.t.sol b/test/unit/concrete/TestTokenPaymaster.t.sol index a7de937..2c33a77 100644 --- a/test/unit/concrete/TestTokenPaymaster.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.t.sol @@ -194,12 +194,12 @@ contract TestTokenPaymaster is TestBase { ); } - function test_SetNativeOracle() external prankModifier(PAYMASTER_OWNER.addr) { + function test_SetNativeAssetToUsdOracle() external prankModifier(PAYMASTER_OWNER.addr) { MockOracle newOracle = new MockOracle(100_000_000, 8); vm.expectEmit(true, true, false, true, address(tokenPaymaster)); emit IBiconomyTokenPaymaster.UpdatedNativeAssetOracle(nativeAssetToUsdOracle, newOracle); - tokenPaymaster.setNativeOracle(newOracle); + tokenPaymaster.setNativeAssetToUsdOracle(newOracle); assertEq(address(tokenPaymaster.nativeAssetToUsdOracle()), address(newOracle)); } From 27f9f611a361b498cfa61b3f7ebc0c7cdf9bc7ed Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Wed, 11 Sep 2024 18:28:52 +0400 Subject: [PATCH 02/10] approval on setting token fee tier --- contracts/token/swaps/Uniswapper.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol index a4d88f9..93e3a00 100644 --- a/contracts/token/swaps/Uniswapper.sol +++ b/contracts/token/swaps/Uniswapper.sol @@ -45,6 +45,7 @@ abstract contract Uniswapper { } function _setTokenPool(address _token, uint24 _feeTier) internal { + IERC20(_token).approve(address(uniswapRouter), type(uint256).max); // one time max approval tokenToPools[_token] = _feeTier; // set mapping of token to uniswap pool to use for swap } From 7eb9e97a2766a5bbc142095eb661ad1f1f876b35 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 12 Sep 2024 16:59:10 +0400 Subject: [PATCH 03/10] integrate uniswapper into token paymaster --- contracts/token/BiconomyTokenPaymaster.sol | 52 +++++++++++++++++----- contracts/token/swaps/Uniswapper.sol | 21 +++++---- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 02b836b..a2ce442 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -15,8 +15,7 @@ import { TokenPaymasterParserLib } from "../libraries/TokenPaymasterParserLib.so import { SignatureCheckerLib } from "@solady/src/utils/SignatureCheckerLib.sol"; import { ECDSA as ECDSA_solady } from "@solady/src/utils/ECDSA.sol"; import "@account-abstraction/contracts/core/Helpers.sol"; -import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; -import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; +import "./swaps/Uniswapper.sol"; /** * @title BiconomyTokenPaymaster @@ -36,10 +35,11 @@ import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; * to manage the assets received by the paymaster. */ contract BiconomyTokenPaymaster is + IBiconomyTokenPaymaster, BasePaymaster, ReentrancyGuardTransient, BiconomyTokenPaymasterErrors, - IBiconomyTokenPaymaster + Uniswapper { using UserOperationLib for PackedUserOperation; using TokenPaymasterParserLib for bytes; @@ -66,10 +66,15 @@ contract BiconomyTokenPaymaster is uint256 _priceMarkup, uint256 _priceExpiryDuration, IOracle _nativeAssetToUsdOracle, - address[] memory _tokens, // Array of token addresses - IOracle[] memory _oracles // Array of corresponding oracle addresses + ISwapRouter _uniswapRouter, + address _wrappedNative, + address[] memory _tokens, // Array of token addresses supported by the paymaster + IOracle[] memory _oracles, // Array of corresponding oracle addresses + 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 ) BasePaymaster(_owner, _entryPoint) + Uniswapper(_uniswapRouter, _wrappedNative, _swappableTokens, _swappableTokenPoolFeeTiers) { if (_isContract(_verifyingSigner)) { revert VerifyingSignerCanNotBeContract(); @@ -301,14 +306,41 @@ contract BiconomyTokenPaymaster is } /** - * @dev Swap a token in the paymaster for ETH to increase its entry point deposit - * @param _swapRouter The address of the swap router to use to facilitate the swap - * @param _tokenAddress The token address of the token to swap for ETH - * @param _tokenAmount The amount of the token to swap + * @dev Swap a token in the paymaster for ETH and deposit the amount received into the entry point + * @param _tokenAddresses The token address to add/update to/for uniswapper + * @param _poolFeeTiers The pool fee tiers for the corresponding token address to use * @notice only to be called by the owner of the contract. */ - function swapTokenAndDeposit(ISwapRouter _swapRouter, address _tokenAddress, uint256 _tokenAmount) external payable onlyOwner { + function updateSwappableTokens( + address[] memory _tokenAddresses, + uint24[] memory _poolFeeTiers + ) + external + payable + onlyOwner + { + if (_tokenAddresses.length != _poolFeeTiers.length) { + revert TokensAndPoolsLengthMismatch(); + } + for (uint256 i = 0; i < _tokenAddresses.length; ++i) { + _setTokenPool(_tokenAddresses[i], _poolFeeTiers[i]); + } + } + + /** + * @dev Swap a token in the paymaster for ETH and deposit the amount received into the entry point + * @param _tokenAddress The token address of the token to swap + * @param _tokenAmount The amount of the token to swap + * @notice only to be called by the owner of the contract. + */ + function swapTokenAndDeposit(address _tokenAddress, uint256 _tokenAmount) external payable onlyOwner { + // Swap tokens + uint256 amountOut = _swapTokenToWeth(_tokenAddress, _tokenAmount); + // Unwrap WETH to ETH + unwrapWeth(amountOut); + // Deposit into EP + entryPoint.depositTo{ value: amountOut }(address(this)); } /** diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol index 93e3a00..e4d1fbd 100644 --- a/contracts/token/swaps/Uniswapper.sol +++ b/contracts/token/swaps/Uniswapper.sol @@ -20,18 +20,18 @@ abstract contract Uniswapper { // Token address -> Fee tier of the pool to swap through mapping(address => uint24) public tokenToPools; - event UniswapReverted(address indexed tokenIn, address indexed tokenOut, uint256 amountIn); - - error TokensAndAmountsLengthMismatch(); + // Errors + error UniswapReverted(address tokenIn, address tokenOut, uint256 amountIn); + error TokensAndPoolsLengthMismatch(); constructor( ISwapRouter _uniswapRouter, address _wrappedNative, address[] memory _tokens, - uint24[] memory _tokenPools + uint24[] memory _tokenPoolFeeTiers ) { - if (_tokens.length != _tokenPools.length) { - revert TokensAndAmountsLengthMismatch(); + if (_tokens.length != _tokenPoolFeeTiers.length) { + revert TokensAndPoolsLengthMismatch(); } // Set router and native wrapped asset addresses @@ -40,13 +40,13 @@ abstract contract Uniswapper { for (uint256 i = 0; i < _tokens.length; ++i) { IERC20(_tokens[i]).approve(address(_uniswapRouter), type(uint256).max); // one time max approval - tokenToPools[_tokens[i]] = _tokenPools[i]; // set mapping of token to uniswap pool to use for swap + tokenToPools[_tokens[i]] = _tokenPoolFeeTiers[i]; // set mapping of token to uniswap pool to use for swap } } - function _setTokenPool(address _token, uint24 _feeTier) internal { + function _setTokenPool(address _token, uint24 _poolFeeTier) internal { IERC20(_token).approve(address(uniswapRouter), type(uint256).max); // one time max approval - tokenToPools[_token] = _feeTier; // set mapping of token to uniswap pool to use for swap + tokenToPools[_token] = _poolFeeTier; // set mapping of token to uniswap pool to use for swap } function _swapTokenToWeth(address _tokenIn, uint256 _amountIn) internal returns (uint256 amountOut) { @@ -65,8 +65,7 @@ abstract contract Uniswapper { try uniswapRouter.exactInputSingle(params) returns (uint256 _amountOut) { amountOut = _amountOut; } catch { - emit UniswapReverted(_tokenIn, wrappedNative, _amountIn); - amountOut = 0; + revert UniswapReverted(_tokenIn, wrappedNative, _amountIn); } } From 85313e550f6ce59f97496b287be64c0d743470e8 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 12 Sep 2024 17:00:38 +0400 Subject: [PATCH 04/10] update sol version of uniswapper --- contracts/token/swaps/Uniswapper.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol index e4d1fbd..1da7c17 100644 --- a/contracts/token/swaps/Uniswapper.sol +++ b/contracts/token/swaps/Uniswapper.sol @@ -1,10 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 -pragma solidity ^0.8.23; - -/* solhint-disable not-rely-on-time */ +pragma solidity ^0.8.26; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryPayments.sol"; From 9018dbc317a18625b583e0a73810244b5076efab Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 12 Sep 2024 17:55:46 +0400 Subject: [PATCH 05/10] update comment --- contracts/token/BiconomyTokenPaymaster.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index a2ce442..22c41a0 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -306,7 +306,7 @@ contract BiconomyTokenPaymaster is } /** - * @dev Swap a token in the paymaster for ETH and deposit the amount received into the entry point + * @dev Update or add a swappable token to the Uniswapper * @param _tokenAddresses The token address to add/update to/for uniswapper * @param _poolFeeTiers The pool fee tiers for the corresponding token address to use * @notice only to be called by the owner of the contract. From d90b25772911d59cbe82c10e8c7f4c974e2a9685 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Sat, 14 Sep 2024 09:05:05 +0200 Subject: [PATCH 06/10] ommit try/catch to save gas and let uniwap revert internally --- contracts/token/swaps/Uniswapper.sol | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol index 1da7c17..de5fd47 100644 --- a/contracts/token/swaps/Uniswapper.sol +++ b/contracts/token/swaps/Uniswapper.sol @@ -46,24 +46,18 @@ abstract contract Uniswapper { tokenToPools[_token] = _poolFeeTier; // set mapping of token to uniswap pool to use for swap } - function _swapTokenToWeth(address _tokenIn, uint256 _amountIn) internal returns (uint256 amountOut) { - uint24 poolFee = tokenToPools[_tokenIn]; - + function _swapTokenToWeth(address _tokenIn, uint256 _amountIn) internal returns (uint256) { ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ tokenIn: _tokenIn, tokenOut: wrappedNative, - fee: poolFee, + fee: tokenToPools[_tokenIn], recipient: address(this), deadline: block.timestamp, amountIn: _amountIn, amountOutMinimum: 0, sqrtPriceLimitX96: 0 }); - try uniswapRouter.exactInputSingle(params) returns (uint256 _amountOut) { - amountOut = _amountOut; - } catch { - revert UniswapReverted(_tokenIn, wrappedNative, _amountIn); - } + return uniswapRouter.exactInputSingle(params); } function addSlippage(uint256 amount, uint8 slippage) private pure returns (uint256) { From 3c7f0ecb7dc9b1a159d0a7ce2e415c09af80b6b7 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Sat, 14 Sep 2024 09:08:58 +0200 Subject: [PATCH 07/10] rename --- contracts/token/BiconomyTokenPaymaster.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 22c41a0..1cc2909 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -51,7 +51,7 @@ contract BiconomyTokenPaymaster is uint256 public priceMarkup; uint256 public priceExpiryDuration; IOracle public nativeAssetToUsdOracle; // ETH -> USD price oracle - mapping(address => TokenInfo) tokenDirectory; + mapping(address => TokenInfo) independentTokenDirectory; // mapping of token address => info for tokens supported in independent mode // PAYMASTER_ID_OFFSET uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; // Limit for unaccounted gas cost @@ -68,8 +68,8 @@ contract BiconomyTokenPaymaster is IOracle _nativeAssetToUsdOracle, ISwapRouter _uniswapRouter, address _wrappedNative, - address[] memory _tokens, // Array of token addresses supported by the paymaster - IOracle[] memory _oracles, // Array of corresponding oracle addresses + address[] memory _independentTokens, // Array of token addresses supported by the paymaster in independent mode + IOracle[] memory _oracles, // Array of corresponding oracle addresses for independently supported tokens 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 ) @@ -88,7 +88,7 @@ contract BiconomyTokenPaymaster is if (_priceMarkup > MAX_PRICE_MARKUP || _priceMarkup < PRICE_DENOMINATOR) { revert InvalidPriceMarkup(); } - if (_tokens.length != _oracles.length) { + if (_independentTokens.length != _oracles.length) { revert TokensAndInfoLengthMismatch(); } if (_nativeAssetToUsdOracle.decimals() != 8) { @@ -106,12 +106,12 @@ contract BiconomyTokenPaymaster is } // Populate the tokenToOracle mapping - for (uint256 i = 0; i < _tokens.length; i++) { + for (uint256 i = 0; i < _independentTokens.length; i++) { if (_oracles[i].decimals() != 8) { // Token -> USD will always have 8 decimals revert InvalidOracleDecimals(); } - tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], 10 ** IERC20Metadata(_tokens[i]).decimals()); + independentTokenDirectory[_independentTokens[i]] = TokenInfo(_oracles[i], 10 ** IERC20Metadata(_independentTokens[i]).decimals()); } } @@ -288,7 +288,7 @@ contract BiconomyTokenPaymaster is } /** - * @dev Set or update a TokenInfo entry in the tokenDirectory mapping. + * @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 * @notice only to be called by the owner of the contract. @@ -300,7 +300,7 @@ contract BiconomyTokenPaymaster is } uint8 decimals = IERC20Metadata(_tokenAddress).decimals(); - tokenDirectory[_tokenAddress] = TokenInfo(_oracle, 10 ** decimals); + independentTokenDirectory[_tokenAddress] = TokenInfo(_oracle, 10 ** decimals); emit UpdatedTokenDirectory(_tokenAddress, _oracle, decimals); } @@ -531,7 +531,7 @@ contract BiconomyTokenPaymaster is /// @return price The latest token price fetched from the oracles. function getPrice(address tokenAddress) internal view returns (uint192 price) { // Fetch token information from directory - TokenInfo memory tokenInfo = tokenDirectory[tokenAddress]; + TokenInfo memory tokenInfo = independentTokenDirectory[tokenAddress]; if (address(tokenInfo.oracle) == address(0)) { // If oracle not set, token isn't supported From 385823a4726d7a51098daabd23e71238654fc13a Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Sun, 15 Sep 2024 13:37:25 -0700 Subject: [PATCH 08/10] min eth received param for uniswapper --- contracts/token/BiconomyTokenPaymaster.sol | 32 +++++++++++++++------- contracts/token/swaps/Uniswapper.sol | 24 ++++++---------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 1cc2909..9b8a438 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -22,13 +22,14 @@ import "./swaps/Uniswapper.sol"; * @author ShivaanshK * @author livingrockrises * @notice Biconomy's Token Paymaster for Entry Point v0.7 - * @dev A paymaster that allows user to pay gas fees in ERC20 tokens. The paymaster uses the precharge and refund model + * @dev A paymaster that allows users to pay gas fees in ERC20 tokens. The paymaster uses the precharge and refund + * model * to handle gas remittances. * * Currently, the paymaster supports two modes: * 1. EXTERNAL - Relies on a quoted token price from a trusted entity (verifyingSigner). - * 2. INDEPENDENT - Relies purely on price oracles (Offchain and TWAP) which implement the IOracle interface. This mode - * doesn't require a signature and is always "available" to use. + * 2. INDEPENDENT - Relies purely on price oracles (Chainlink and TWAP) which implement the IOracle interface. This mode + * doesn't require a signature and is "always available" to use. * * The paymaster's owner has full discretion over the supported tokens (for independent mode), price adjustments * applied, and how @@ -51,7 +52,8 @@ contract BiconomyTokenPaymaster is uint256 public priceMarkup; uint256 public priceExpiryDuration; IOracle public nativeAssetToUsdOracle; // ETH -> USD price oracle - mapping(address => TokenInfo) independentTokenDirectory; // mapping of token address => info for tokens supported in independent mode + mapping(address => TokenInfo) independentTokenDirectory; // mapping of token address => info for tokens supported in + // independent mode // PAYMASTER_ID_OFFSET uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; // Limit for unaccounted gas cost @@ -111,7 +113,8 @@ contract BiconomyTokenPaymaster is // Token -> USD will always have 8 decimals revert InvalidOracleDecimals(); } - independentTokenDirectory[_independentTokens[i]] = TokenInfo(_oracles[i], 10 ** IERC20Metadata(_independentTokens[i]).decimals()); + independentTokenDirectory[_independentTokens[i]] = + TokenInfo(_oracles[i], 10 ** IERC20Metadata(_independentTokens[i]).decimals()); } } @@ -332,14 +335,23 @@ contract BiconomyTokenPaymaster is * @dev Swap a token in the paymaster for ETH and deposit the amount received into the entry point * @param _tokenAddress The token address of the token to swap * @param _tokenAmount The amount of the token to swap + * @param _minEthAmountRecevied The minimum amount of ETH amount recevied post-swap * @notice only to be called by the owner of the contract. */ - function swapTokenAndDeposit(address _tokenAddress, uint256 _tokenAmount) external payable onlyOwner { - // Swap tokens - uint256 amountOut = _swapTokenToWeth(_tokenAddress, _tokenAmount); + function swapTokenAndDeposit( + address _tokenAddress, + uint256 _tokenAmount, + uint256 _minEthAmountRecevied + ) + external + payable + onlyOwner + { + // Swap tokens for WETH + uint256 amountOut = _swapTokenToWeth(_tokenAddress, _tokenAmount, _minEthAmountRecevied); // Unwrap WETH to ETH - unwrapWeth(amountOut); - // Deposit into EP + _unwrapWeth(amountOut); + // Deposit ETH into EP entryPoint.depositTo{ value: amountOut }(address(this)); } diff --git a/contracts/token/swaps/Uniswapper.sol b/contracts/token/swaps/Uniswapper.sol index de5fd47..6ba45b2 100644 --- a/contracts/token/swaps/Uniswapper.sol +++ b/contracts/token/swaps/Uniswapper.sol @@ -5,6 +5,12 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; import "@uniswap/v3-periphery/contracts/interfaces/IPeripheryPayments.sol"; +/** + * @title Uniswapper + * @author ShivaanshK + * @notice An abstract contract to assist the paymaster in swapping tokens to WETH and unwrapping WETH + * @notice Based on Infinitism's Uniswap Helper contract + */ abstract contract Uniswapper { uint256 private constant SWAP_PRICE_DENOMINATOR = 1e26; @@ -46,7 +52,7 @@ abstract contract Uniswapper { tokenToPools[_token] = _poolFeeTier; // set mapping of token to uniswap pool to use for swap } - function _swapTokenToWeth(address _tokenIn, uint256 _amountIn) internal returns (uint256) { + function _swapTokenToWeth(address _tokenIn, uint256 _amountIn, uint256 _minAmountOut) internal returns (uint256) { ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ tokenIn: _tokenIn, tokenOut: wrappedNative, @@ -54,25 +60,13 @@ abstract contract Uniswapper { recipient: address(this), deadline: block.timestamp, amountIn: _amountIn, - amountOutMinimum: 0, + amountOutMinimum: _minAmountOut, sqrtPriceLimitX96: 0 }); return uniswapRouter.exactInputSingle(params); } - function addSlippage(uint256 amount, uint8 slippage) private pure returns (uint256) { - return amount * (1000 - slippage) / 1000; - } - - function tokenToWei(uint256 amount, uint256 price) public pure returns (uint256) { - return amount * price / SWAP_PRICE_DENOMINATOR; - } - - function weiToToken(uint256 amount, uint256 price) public pure returns (uint256) { - return amount * SWAP_PRICE_DENOMINATOR / price; - } - - function unwrapWeth(uint256 _amount) internal { + function _unwrapWeth(uint256 _amount) internal { IPeripheryPayments(address(uniswapRouter)).unwrapWETH9(_amount, address(this)); } } From 02537c25bb1cdddb0f980c127496c1707ba68cd1 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Sun, 15 Sep 2024 14:38:36 -0700 Subject: [PATCH 09/10] rename and comment priceMarkup --- contracts/token/BiconomyTokenPaymaster.sol | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 9b8a438..7bc6b89 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -47,10 +47,10 @@ contract BiconomyTokenPaymaster is using SignatureCheckerLib for address; // State variables - address public verifyingSigner; + address public verifyingSigner; // entity used to provide external token price and markup uint256 public unaccountedGas; - uint256 public priceMarkup; - uint256 public priceExpiryDuration; + uint256 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) independentTokenDirectory; // mapping of token address => info for tokens supported in // independent mode @@ -65,7 +65,7 @@ contract BiconomyTokenPaymaster is address _verifyingSigner, IEntryPoint _entryPoint, uint256 _unaccountedGas, - uint256 _priceMarkup, + uint256 _independentPriceMarkup, // price markup used for independent mode uint256 _priceExpiryDuration, IOracle _nativeAssetToUsdOracle, ISwapRouter _uniswapRouter, @@ -87,7 +87,7 @@ contract BiconomyTokenPaymaster is if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } - if (_priceMarkup > MAX_PRICE_MARKUP || _priceMarkup < PRICE_DENOMINATOR) { + if (_independentPriceMarkup > MAX_PRICE_MARKUP || _independentPriceMarkup < PRICE_DENOMINATOR) { revert InvalidPriceMarkup(); } if (_independentTokens.length != _oracles.length) { @@ -102,7 +102,7 @@ contract BiconomyTokenPaymaster is assembly ("memory-safe") { sstore(verifyingSigner.slot, _verifyingSigner) sstore(unaccountedGas.slot, _unaccountedGas) - sstore(priceMarkup.slot, _priceMarkup) + sstore(independentPriceMarkup.slot, _independentPriceMarkup) sstore(priceExpiryDuration.slot, _priceExpiryDuration) sstore(nativeAssetToUsdOracle.slot, _nativeAssetToUsdOracle) } @@ -244,18 +244,18 @@ contract BiconomyTokenPaymaster is /** * @dev Set a new priceMarkup value. - * @param _newPriceMarkup The new value to be set as the price markup + * @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(uint256 _newPriceMarkup) external payable override onlyOwner { - if (_newPriceMarkup > MAX_PRICE_MARKUP || _newPriceMarkup < PRICE_DENOMINATOR) { + function setPriceMarkup(uint256 _newIndependentPriceMarkup) external payable override onlyOwner { + if (_newPriceMarkup > MAX_PRICE_MARKUP || _newIndependentPriceMarkup < PRICE_DENOMINATOR) { revert InvalidPriceMarkup(); } - uint256 oldPriceMarkup = priceMarkup; + uint256 oldIndependentPriceMarkup = independentPriceMarkup; assembly ("memory-safe") { - sstore(priceMarkup.slot, _newPriceMarkup) + sstore(independentPriceMarkup.slot, _newIndependentPriceMarkup) } - emit UpdatedFixedPriceMarkup(oldPriceMarkup, _newPriceMarkup); + emit UpdatedFixedPriceMarkup(oldIndependentPriceMarkup, _newIndependentPriceMarkup); } /** @@ -478,14 +478,14 @@ contract BiconomyTokenPaymaster is { // Calculate token amount to precharge uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp); - tokenAmount = ((maxCost + (unaccountedGas) * maxFeePerGas) * priceMarkup * tokenPrice) + tokenAmount = ((maxCost + (unaccountedGas) * maxFeePerGas) * independentPriceMarkup * tokenPrice) / (1e18 * PRICE_DENOMINATOR); } // Transfer full amount to this address. Unused amount will be refunded in postOP SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); - context = abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, priceMarkup, userOpHash); + context = abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, independentPriceMarkup, userOpHash); validationData = 0; // Validation success and price is valid indefinetly } } From f20dba4f8ca01fc18013b1d86dfd3e6d3d154839 Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Sun, 15 Sep 2024 15:01:23 -0700 Subject: [PATCH 10/10] tests compiling --- contracts/token/BiconomyTokenPaymaster.sol | 7 +- test/base/TestBase.sol | 4 + test/unit/concrete/TestTokenPaymaster.t.sol | 117 ++++++++++++++------ 3 files changed, 93 insertions(+), 35 deletions(-) diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 7bc6b89..8f7023a 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -88,6 +88,7 @@ contract BiconomyTokenPaymaster is revert UnaccountedGasTooHigh(); } if (_independentPriceMarkup > MAX_PRICE_MARKUP || _independentPriceMarkup < PRICE_DENOMINATOR) { + // Not between 0% and 100% markup revert InvalidPriceMarkup(); } if (_independentTokens.length != _oracles.length) { @@ -248,7 +249,8 @@ contract BiconomyTokenPaymaster is * @notice only to be called by the owner of the contract. */ function setPriceMarkup(uint256 _newIndependentPriceMarkup) external payable override onlyOwner { - if (_newPriceMarkup > MAX_PRICE_MARKUP || _newIndependentPriceMarkup < PRICE_DENOMINATOR) { + if (_newIndependentPriceMarkup > MAX_PRICE_MARKUP || _newIndependentPriceMarkup < PRICE_DENOMINATOR) { + // Not between 0% and 100% markup revert InvalidPriceMarkup(); } uint256 oldIndependentPriceMarkup = independentPriceMarkup; @@ -485,7 +487,8 @@ contract BiconomyTokenPaymaster is // Transfer full amount to this address. Unused amount will be refunded in postOP SafeTransferLib.safeTransferFrom(tokenAddress, userOp.sender, address(this), tokenAmount); - context = abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, independentPriceMarkup, userOpHash); + context = + abi.encode(userOp.sender, tokenAddress, tokenAmount, tokenPrice, independentPriceMarkup, userOpHash); validationData = 0; // Validation success and price is valid indefinetly } } diff --git a/test/base/TestBase.sol b/test/base/TestBase.sol index 3df22c2..ffcd9a1 100644 --- a/test/base/TestBase.sol +++ b/test/base/TestBase.sol @@ -27,6 +27,10 @@ import { abstract contract TestBase is CheatCodes, TestHelper, BaseEventsAndErrors { address constant ENTRYPOINT_ADDRESS = address(0x0000000071727De22E5E9d8BAf0edAc6f37da032); + address constant WRAPPED_NATIVE_ADDRESS = address(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + address constant SWAP_ROUTER_ADDRESS = address(0xE592427A0AEce92De3Edee1F18E0157C05861564); + Vm.Wallet internal PAYMASTER_OWNER; Vm.Wallet internal PAYMASTER_SIGNER; Vm.Wallet internal PAYMASTER_FEE_COLLECTOR; diff --git a/test/unit/concrete/TestTokenPaymaster.t.sol b/test/unit/concrete/TestTokenPaymaster.t.sol index 2c33a77..3766279 100644 --- a/test/unit/concrete/TestTokenPaymaster.t.sol +++ b/test/unit/concrete/TestTokenPaymaster.t.sol @@ -11,10 +11,11 @@ import { import { MockOracle } from "../../mocks/MockOracle.sol"; import { MockToken } from "@nexus/contracts/mocks/MockToken.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - +import "../../../contracts/token/swaps/Uniswapper.sol"; contract TestTokenPaymaster is TestBase { BiconomyTokenPaymaster public tokenPaymaster; + ISwapRouter swapRouter; MockOracle public nativeAssetToUsdOracle; MockToken public testToken; MockToken public testToken2; @@ -24,12 +25,12 @@ contract TestTokenPaymaster is TestBase { setupPaymasterTestEnvironment(); // Deploy mock oracles and tokens + swapRouter = ISwapRouter(address(SWAP_ROUTER_ADDRESS)); nativeAssetToUsdOracle = new MockOracle(100_000_000, 8); // Oracle with 8 decimals for ETH tokenOracle = new MockOracle(100_000_000, 8); // Oracle with 8 decimals for ERC20 token testToken = new MockToken("Test Token", "TKN"); testToken2 = new MockToken("Test Token 2", "TKN2"); - // Deploy the token paymaster tokenPaymaster = new BiconomyTokenPaymaster( PAYMASTER_OWNER.addr, @@ -37,24 +38,33 @@ contract TestTokenPaymaster is TestBase { ENTRYPOINT, 5000, // unaccounted gas 1e6, // price markup - nativeAssetToUsdOracle, 1 days, // price expiry duration + nativeAssetToUsdOracle, + swapRouter, + WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))) + _toSingletonArray(IOracle(address(tokenOracle))), + new address[](0), + new uint24[](0) ); } function test_Deploy() external { + // Deploy the token paymaster BiconomyTokenPaymaster testArtifact = new BiconomyTokenPaymaster( PAYMASTER_OWNER.addr, PAYMASTER_SIGNER.addr, ENTRYPOINT, - 5000, - 1e6, + 5000, // unaccounted gas + 1e6, // price markup + 1 days, // price expiry duration nativeAssetToUsdOracle, - 1 days, + swapRouter, + WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))) + _toSingletonArray(IOracle(address(tokenOracle))), + new address[](0), + new uint24[](0) ); assertEq(testArtifact.owner(), PAYMASTER_OWNER.addr); @@ -62,36 +72,46 @@ contract TestTokenPaymaster is TestBase { assertEq(testArtifact.verifyingSigner(), PAYMASTER_SIGNER.addr); assertEq(address(testArtifact.nativeAssetToUsdOracle()), address(nativeAssetToUsdOracle)); assertEq(testArtifact.unaccountedGas(), 5000); - assertEq(testArtifact.priceMarkup(), 1e6); + assertEq(testArtifact.independentPriceMarkup(), 1e6); } function test_RevertIf_DeployWithSignerSetToZero() external { vm.expectRevert(BiconomyTokenPaymasterErrors.VerifyingSignerCanNotBeZero.selector); + // Deploy the token paymaster new BiconomyTokenPaymaster( PAYMASTER_OWNER.addr, address(0), ENTRYPOINT, - 5000, - 1e6, + 5000, // unaccounted gas + 1e6, // price markup + 1 days, // price expiry duration nativeAssetToUsdOracle, - 1 days, + swapRouter, + WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))) + _toSingletonArray(IOracle(address(tokenOracle))), + new address[](0), + new uint24[](0) ); } function test_RevertIf_DeployWithSignerAsContract() external { vm.expectRevert(BiconomyTokenPaymasterErrors.VerifyingSignerCanNotBeContract.selector); + // Deploy the token paymaster new BiconomyTokenPaymaster( PAYMASTER_OWNER.addr, - address(ENTRYPOINT), + ENTRYPOINT_ADDRESS, ENTRYPOINT, - 5000, - 1e6, + 5000, // unaccounted gas + 1e6, // price markup + 1 days, // price expiry duration nativeAssetToUsdOracle, - 1 days, + swapRouter, + WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))) + _toSingletonArray(IOracle(address(tokenOracle))), + new address[](0), + new uint24[](0) ); } @@ -101,12 +121,16 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_OWNER.addr, PAYMASTER_SIGNER.addr, ENTRYPOINT, - 50_001, // too high unaccounted gas - 1e6, + 500_001, // unaccounted gas + 1e6, // price markup + 1 days, // price expiry duration nativeAssetToUsdOracle, - 1 days, + swapRouter, + WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))) + _toSingletonArray(IOracle(address(tokenOracle))), + new address[](0), + new uint24[](0) ); } @@ -116,12 +140,16 @@ contract TestTokenPaymaster is TestBase { PAYMASTER_OWNER.addr, PAYMASTER_SIGNER.addr, ENTRYPOINT, - 5000, - 2e6 + 1, // too high price markup + 5000, // unaccounted gas + 2e6 + 1, // price markup + 1 days, // price expiry duration nativeAssetToUsdOracle, - 1 days, + swapRouter, + WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))) + _toSingletonArray(IOracle(address(tokenOracle))), + new address[](0), + new uint24[](0) ); } @@ -178,19 +206,43 @@ contract TestTokenPaymaster is TestBase { assertEq(testToken.balanceOf(ALICE_ADDRESS), mintAmount); } - function test_RevertIf_InvalidOracleDecimals() external { + function test_RevertIf_InvalidNativeOracleDecimals() external { + MockOracle invalidOracle = new MockOracle(100_000_000, 18); // invalid oracle with 18 decimals + vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidOracleDecimals.selector); + new BiconomyTokenPaymaster( + PAYMASTER_OWNER.addr, + PAYMASTER_SIGNER.addr, + ENTRYPOINT, + 5000, // unaccounted gas + 1e6, // price markup + 1 days, // price expiry duration + invalidOracle, + swapRouter, + WRAPPED_NATIVE_ADDRESS, + _toSingletonArray(address(testToken)), + _toSingletonArray(IOracle(address(tokenOracle))), + new address[](0), + new uint24[](0) + ); + } + + function test_RevertIf_InvalidTokenOracleDecimals() external { MockOracle invalidOracle = new MockOracle(100_000_000, 18); // invalid oracle with 18 decimals vm.expectRevert(BiconomyTokenPaymasterErrors.InvalidOracleDecimals.selector); new BiconomyTokenPaymaster( PAYMASTER_OWNER.addr, PAYMASTER_SIGNER.addr, ENTRYPOINT, - 5000, - 1e6, - invalidOracle, // incorrect oracle decimals - 1 days, + 50000, // unaccounted gas + 1e6, // price markup + 1 days, // price expiry duration + nativeAssetToUsdOracle, + swapRouter, + WRAPPED_NATIVE_ADDRESS, _toSingletonArray(address(testToken)), - _toSingletonArray(IOracle(address(tokenOracle))) + _toSingletonArray(IOracle(address(invalidOracle))), + new address[](0), + new uint24[](0) ); } @@ -395,5 +447,4 @@ contract TestTokenPaymaster is TestBase { vm.expectRevert(); ENTRYPOINT.handleOps(ops, payable(BUNDLER.addr)); } - }