Skip to content

Commit

Permalink
Merge pull request #11 from bcnmy/feat/token-paymaster
Browse files Browse the repository at this point in the history
Token Paymaster - Add Swapping Feature
  • Loading branch information
livingrockrises authored Sep 25, 2024
2 parents 063442c + 0c00bd6 commit acc0dca
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 69 deletions.
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
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();
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 {
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);

Vm.Wallet internal PAYMASTER_OWNER;
Vm.Wallet internal PAYMASTER_SIGNER;
Vm.Wallet internal PAYMASTER_FEE_COLLECTOR;
Expand Down
Loading

0 comments on commit acc0dca

Please sign in to comment.