Skip to content
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

Merged
merged 11 commits into from
Sep 25, 2024
2 changes: 1 addition & 1 deletion contracts/interfaces/IBiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
125 changes: 92 additions & 33 deletions contracts/token/BiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

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)

Copy link
Contributor Author

@ShivaanshK ShivaanshK Sep 12, 2024

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.

Copy link
Contributor

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

uint24[] memory _swappableTokenPoolFeeTiers // Array of uniswap pool fee tiers for each swappable token
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is required by the swapper?

Copy link
Contributor Author

@ShivaanshK ShivaanshK Sep 12, 2024

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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();
Expand All @@ -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) {
Expand All @@ -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());
}
}

Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions contracts/token/swaps/Uniswapper.sol
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two methods to add and remove from swappable tokens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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));
}
}
4 changes: 4 additions & 0 deletions test/base/TestBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
Expand Down
Loading
Loading