Skip to content

Commit

Permalink
Independent paymaster mode validation
Browse files Browse the repository at this point in the history
  • Loading branch information
ShivaanshK committed Sep 9, 2024
1 parent e747ec7 commit a4d56bb
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 29 deletions.
15 changes: 15 additions & 0 deletions contracts/common/BiconomyTokenPaymasterErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ contract BiconomyTokenPaymasterErrors {
*/
error TokensAndInfoLengthMismatch();

/**
* @notice Throws when invalid PaymasterMode specified in paymasterAndData
*/
error InvalidPaymasterMode();

/**
* @notice Throws when oracle returns invalid price
*/
Expand All @@ -46,4 +51,14 @@ contract BiconomyTokenPaymasterErrors {
* @notice Throws when oracle price hasn't been updated for a duration of time the owner is comfortable with
*/
error OraclePriceExpired();

/**
* @notice Throws when token address to pay with is invalid
*/
error InvalidTokenAddress();

/**
* @notice Throws when user tries to pay with an unsupported token
*/
error TokenNotSupported();
}
7 changes: 6 additions & 1 deletion contracts/interfaces/IBiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ pragma solidity ^0.8.26;
import { IOracle } from "./oracles/IOracle.sol";

interface IBiconomyTokenPaymaster {
enum PaymasterMode {
EXTERNAL,
INDEPENDENT
}

// Struct for storing information about the token
struct TokenInfo {
IOracle oracle;
Expand All @@ -30,5 +35,5 @@ interface IBiconomyTokenPaymaster {

function setPriceExpiryDuration(uint256 _newPriceExpiryDuration) external payable;

function setTokenInfo(address _tokenAddress, IOracle _oracle, uint8 _decimals) external payable;
function setTokenInfo(address _tokenAddress, IOracle _oracle) external payable;
}
34 changes: 34 additions & 0 deletions contracts/libraries/PaymasterParser.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.26;

import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol";
import "@account-abstraction/contracts/core/UserOperationLib.sol";

// A helper library to parse paymaster and data
library PaymasterParser {
uint256 private constant PAYMASTER_MODE_OFFSET = UserOperationLib.PAYMASTER_DATA_OFFSET; // Start offset of mode in
// PND

function parsePaymasterAndData(bytes calldata paymasterAndData)
external
pure
returns (IBiconomyTokenPaymaster.PaymasterMode mode, bytes memory modeSpecificData)
{
unchecked {
mode = IBiconomyTokenPaymaster.PaymasterMode(
uint8(bytes1(paymasterAndData[PAYMASTER_MODE_OFFSET:PAYMASTER_MODE_OFFSET + 8]))
);
modeSpecificData = paymasterAndData[PAYMASTER_MODE_OFFSET + 8:];
}
}

function parseExternalModeSpecificData(bytes calldata modeSpecificData) external pure { }

function parseIndependentModeSpecificData(bytes calldata modeSpecificData)
external
pure
returns (address tokenAddress)
{
tokenAddress = address(bytes20(modeSpecificData[:20]));
}
}
88 changes: 61 additions & 27 deletions contracts/token/BiconomyTokenPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ pragma solidity ^0.8.26;
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import { PackedUserOperation, UserOperationLib } from "@account-abstraction/contracts/core/UserOperationLib.sol";
import { IERC20, ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol";
import { BasePaymaster } from "../base/BasePaymaster.sol";
import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol";
import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol";
import { IOracle } from "../interfaces/oracles/IOracle.sol";
import { PaymasterParser } from "../libraries/PaymasterParser.sol";
import "@account-abstraction/contracts/core/Helpers.sol";

/**
Expand All @@ -18,8 +20,9 @@ import "@account-abstraction/contracts/core/Helpers.sol";
* @author livingrockrises<[email protected]>
* @notice 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
* to handle gas remittances. For fair and "always available" operation, it relies on price oracles which
* implement the IOracle interface to calculate the gas cost of the transaction in a supported token. The owner has full
* to handle gas remittances. For fair and "always available" operation, it supports a mode that relies purely on price
* oracles (Offchain and TWAP) which
* implement the IOracle interface to calculate the token cost of a transaction. The owner has full
* discretion over the supported tokens, premium and discounts applied (if any), and how to manage the assets
* received by the paymaster. The properties described above, make the paymaster self-relaint: independent of any
* offchain service for use.
Expand All @@ -31,6 +34,7 @@ contract BiconomyTokenPaymaster is
IBiconomyTokenPaymaster
{
using UserOperationLib for PackedUserOperation;
using PaymasterParser for bytes;

// State variables
address public feeCollector;
Expand All @@ -40,9 +44,9 @@ contract BiconomyTokenPaymaster is
IOracle public nativeOracle; // ETH -> USD price
mapping(address => TokenInfo) tokenDirectory;

// Limit for unaccounted gas cost
uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000;
uint256 private constant PRICE_DENOMINATOR = 1e6;
// PAYMASTER_ID_OFFSET
uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; // Limit for unaccounted gas cost
uint256 private constant PRICE_DENOMINATOR = 1e6; // Denominator used when calculating cost with dynamic adjustment
uint256 private constant MAX_DYNAMIC_ADJUSTMENT = 2e6; // 100% premium on price (2e6/PRICE_DENOMINATOR)

constructor(
Expand All @@ -53,7 +57,6 @@ contract BiconomyTokenPaymaster is
IOracle _nativeOracle,
uint256 _priceExpiryDuration,
address[] memory _tokens, // Array of token addresses
uint8[] memory _decimals, // Array of corresponding token decimals
IOracle[] memory _oracles // Array of corresponding oracle addresses
)
BasePaymaster(_owner, _entryPoint)
Expand All @@ -62,19 +65,20 @@ contract BiconomyTokenPaymaster is
revert UnaccountedGasTooHigh();
} else if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment == 0) {
revert InvalidDynamicAdjustment();
} else if (_tokens.length != _oracles.length || _tokens.length != _decimals.length) {
} else if (_tokens.length != _oracles.length) {
revert TokensAndInfoLengthMismatch();
}
assembly ("memory-safe") {
sstore(feeCollector.slot, address()) // initialize fee collector to this contract
sstore(unaccountedGas.slot, _unaccountedGas)
sstore(dynamicAdjustment.slot, _dynamicAdjustment)
sstore(priceExpiryDuration.slot, _priceExpiryDuration)
sstore(nativeOracle.slot, _nativeOracle)
}

// Populate the tokenToOracle mapping
for (uint256 i = 0; i < _tokens.length; i++) {
tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], _decimals[i]);
tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], IERC20Metadata(_tokens[i]).decimals());
}
}

Expand Down Expand Up @@ -232,21 +236,12 @@ contract BiconomyTokenPaymaster is
* @dev Set or update a TokenInfo entry in the tokenDirectory mapping.
* @param _tokenAddress The new value to be set as the unaccounted gas value
* @param _oracle The new value to be set as the unaccounted gas value
* @param _decimals The new value to be set as the unaccounted gas value
* @notice only to be called by the owner of the contract.
*/
function setTokenInfo(
address _tokenAddress,
IOracle _oracle,
uint8 _decimals
)
external
payable
override
onlyOwner
{
tokenDirectory[_tokenAddress] = TokenInfo(_oracle, _decimals);
emit UpdatedTokenDirectory(_tokenAddress, _oracle, _decimals);
function setTokenInfo(address _tokenAddress, IOracle _oracle) external payable override onlyOwner {
uint8 decimals = IERC20Metadata(_tokenAddress).decimals();
tokenDirectory[_tokenAddress] = TokenInfo(_oracle, decimals);
emit UpdatedTokenDirectory(_tokenAddress, _oracle, decimals);
}

/**
Expand All @@ -264,7 +259,41 @@ contract BiconomyTokenPaymaster is
internal
override
returns (bytes memory context, uint256 validationData)
{ }
{
(PaymasterMode mode, bytes memory modeSpecificData) = userOp.paymasterAndData.parsePaymasterAndData();

if (uint8(mode) > 1) {
revert InvalidPaymasterMode();
}

if (mode == PaymasterMode.EXTERNAL) {
// Use the price and other params specified in modeSpecificData by the verifyingSigner
// Useful for supporting tokens which don't have oracle support
} else if (mode == PaymasterMode.INDEPENDENT) {
// Use only oracles for the token specified in modeSpecificData
if (modeSpecificData.length != 20) {
revert InvalidTokenAddress();
}

// Get address for token used to pay
address tokenAddress = modeSpecificData.parseIndependentModeSpecificData();
uint192 tokenPrice = getPrice(tokenAddress);
uint256 tokenAmount;

{
// Calculate token amount to precharge
uint256 maxFeePerGas = UserOperationLib.unpackMaxFeePerGas(userOp);
tokenAmount = (maxCost + (unaccountedGas) * maxFeePerGas) * dynamicAdjustment * 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.encodePacked(tokenAmount, tokenPrice, userOp.sender, userOpHash);
validationData = 0;
}
}

/**
* @dev Post-operation handler.
Expand Down Expand Up @@ -292,14 +321,19 @@ contract BiconomyTokenPaymaster is
}

/// @notice Fetches the latest token price.

/// @return price The latest token price fetched from the oracles.
function getPrice(address tokenAddress) internal view returns (uint192) {
function getPrice(address tokenAddress) internal view returns (uint192 price) {
TokenInfo memory tokenInfo = tokenDirectory[tokenAddress];

if (address(tokenInfo.oracle) == address(0)) {
// If oracle not set, token isn't supported
revert TokenNotSupported();
}

uint192 tokenPrice = _fetchPrice(tokenInfo.oracle);
uint192 nativeAssetPrice = _fetchPrice(nativeOracle);
uint192 price = nativeAssetPrice * uint192(tokenInfo.decimals) / tokenPrice;
return price;

price = nativeAssetPrice * uint192(tokenInfo.decimals) / tokenPrice;
}

/// @notice Fetches the latest price from the given oracle.
Expand Down
2 changes: 1 addition & 1 deletion contracts/token/oracles/TwapOracle.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
pragma solidity ^0.8.26;

import {IOracle} from "../../interfaces/oracles/IOracle.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
Expand Down

0 comments on commit a4d56bb

Please sign in to comment.