-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Token Paymaster - Add Swapping Feature #11
Changes from all commits
4e2a0e5
27f9f61
7eb9e97
85313e5
9018dbc
d90b257
3c7f0ec
385823a
02537c2
f20dba4
0c00bd6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,41 +15,45 @@ 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 "./swaps/Uniswapper.sol"; | ||
|
||
/** | ||
* @title BiconomyTokenPaymaster | ||
* @author ShivaanshK<[email protected]> | ||
* @author livingrockrises<[email protected]> | ||
* @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 | ||
* 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; | ||
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) 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 | ||
|
@@ -61,13 +65,18 @@ contract BiconomyTokenPaymaster is | |
address _verifyingSigner, | ||
IEntryPoint _entryPoint, | ||
uint256 _unaccountedGas, | ||
uint256 _priceMarkup, | ||
IOracle _nativeAssetToUsdOracle, | ||
uint256 _independentPriceMarkup, // price markup used for independent mode | ||
uint256 _priceExpiryDuration, | ||
address[] memory _tokens, // Array of token addresses | ||
IOracle[] memory _oracles // Array of corresponding oracle addresses | ||
IOracle _nativeAssetToUsdOracle, | ||
ISwapRouter _uniswapRouter, | ||
address _wrappedNative, | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is required by the swapper? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes. need to specify which fee tier pool to swap through. Some tokens have multiple supported tiers causing a difference in liquidity depth, and consequently, potential for price impact. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. got it |
||
) | ||
BasePaymaster(_owner, _entryPoint) | ||
Uniswapper(_uniswapRouter, _wrappedNative, _swappableTokens, _swappableTokenPoolFeeTiers) | ||
{ | ||
if (_isContract(_verifyingSigner)) { | ||
revert VerifyingSignerCanNotBeContract(); | ||
|
@@ -78,10 +87,11 @@ 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) { | ||
// Not between 0% and 100% markup | ||
revert InvalidPriceMarkup(); | ||
} | ||
if (_tokens.length != _oracles.length) { | ||
if (_independentTokens.length != _oracles.length) { | ||
revert TokensAndInfoLengthMismatch(); | ||
} | ||
if (_nativeAssetToUsdOracle.decimals() != 8) { | ||
|
@@ -93,18 +103,19 @@ 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) | ||
} | ||
|
||
// 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()); | ||
} | ||
} | ||
|
||
|
@@ -234,18 +245,19 @@ 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 (_newIndependentPriceMarkup > MAX_PRICE_MARKUP || _newIndependentPriceMarkup < PRICE_DENOMINATOR) { | ||
// Not between 0% and 100% markup | ||
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); | ||
} | ||
|
||
/** | ||
|
@@ -266,22 +278,22 @@ 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); | ||
} | ||
|
||
/** | ||
* @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. | ||
|
@@ -293,11 +305,58 @@ 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); | ||
} | ||
|
||
/** | ||
* @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. | ||
*/ | ||
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 | ||
* @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, | ||
uint256 _minEthAmountRecevied | ||
) | ||
external | ||
payable | ||
onlyOwner | ||
{ | ||
// Swap tokens for WETH | ||
uint256 amountOut = _swapTokenToWeth(_tokenAddress, _tokenAmount, _minEthAmountRecevied); | ||
// Unwrap WETH to ETH | ||
_unwrapWeth(amountOut); | ||
// Deposit ETH into EP | ||
entryPoint.depositTo{ value: amountOut }(address(this)); | ||
} | ||
|
||
/** | ||
* 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 +464,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 | ||
|
@@ -422,14 +480,15 @@ 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 | ||
} | ||
} | ||
|
@@ -487,7 +546,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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
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"; | ||
|
||
/** | ||
* @title Uniswapper | ||
* @author ShivaanshK<[email protected]> | ||
* @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; | ||
|
||
/// @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; | ||
|
||
// Errors | ||
error UniswapReverted(address tokenIn, address tokenOut, uint256 amountIn); | ||
error TokensAndPoolsLengthMismatch(); | ||
|
||
constructor( | ||
ISwapRouter _uniswapRouter, | ||
address _wrappedNative, | ||
address[] memory _tokens, | ||
uint24[] memory _tokenPoolFeeTiers | ||
) { | ||
if (_tokens.length != _tokenPoolFeeTiers.length) { | ||
revert TokensAndPoolsLengthMismatch(); | ||
} | ||
|
||
// 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]] = _tokenPoolFeeTiers[i]; // set mapping of token to uniswap pool to use for swap | ||
} | ||
} | ||
|
||
function _setTokenPool(address _token, uint24 _poolFeeTier) internal { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. two methods to add and remove from swappable tokens. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is internal |
||
IERC20(_token).approve(address(uniswapRouter), type(uint256).max); // one time max approval | ||
tokenToPools[_token] = _poolFeeTier; // set mapping of token to uniswap pool to use for swap | ||
} | ||
|
||
function _swapTokenToWeth(address _tokenIn, uint256 _amountIn, uint256 _minAmountOut) internal returns (uint256) { | ||
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ | ||
tokenIn: _tokenIn, | ||
tokenOut: wrappedNative, | ||
fee: tokenToPools[_tokenIn], | ||
recipient: address(this), | ||
deadline: block.timestamp, | ||
amountIn: _amountIn, | ||
amountOutMinimum: _minAmountOut, | ||
sqrtPriceLimitX96: 0 | ||
}); | ||
return uniswapRouter.exactInputSingle(params); | ||
} | ||
|
||
function _unwrapWeth(uint256 _amount) internal { | ||
IPeripheryPayments(address(uniswapRouter)).unwrapWETH9(_amount, address(this)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. which chain addresses are these? does this run on a fork? |
||
|
||
Vm.Wallet internal PAYMASTER_OWNER; | ||
Vm.Wallet internal PAYMASTER_SIGNER; | ||
Vm.Wallet internal PAYMASTER_FEE_COLLECTOR; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we really need to pass another array of addresses which is subset of above token addresses?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm to think of it, some tokens from another bucket (External mode) could also be swappable by uniswap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also maybe we should create some read method to retrieve token bucket supported by this paymaster for independent mode (and external mode? tricky bc signing service decides that and keeps mapping)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressing the last comment. For independent mode, it would use the tokens that are added by the owner through the public function. For external, the owner could add tokens just like for independent or it would auto add those tokens through the signed data on validation. The latter is impractical imo, since adding a token and corresponding pool is a one time job while the latter would require a change to the data that is signed by the verifyingSigner on every userop plus an additional check in the contract.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, I think only retrieving independent mode tokens makes sense through the contract. for other we can keep public gated repo
cc Nishant