diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 6a3235a..2ad54c3 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -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 */ @@ -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(); } diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 50189f0..97116cc 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -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; @@ -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; } diff --git a/contracts/libraries/PaymasterParser.sol b/contracts/libraries/PaymasterParser.sol new file mode 100644 index 0000000..d60ab1e --- /dev/null +++ b/contracts/libraries/PaymasterParser.sol @@ -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])); + } +} diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index af3dc4c..fc6aba4 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -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"; /** @@ -18,8 +20,9 @@ import "@account-abstraction/contracts/core/Helpers.sol"; * @author livingrockrises * @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. @@ -31,6 +34,7 @@ contract BiconomyTokenPaymaster is IBiconomyTokenPaymaster { using UserOperationLib for PackedUserOperation; + using PaymasterParser for bytes; // State variables address public feeCollector; @@ -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( @@ -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) @@ -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()); } } @@ -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); } /** @@ -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. @@ -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. diff --git a/contracts/token/oracles/TwapOracle.sol b/contracts/token/oracles/TwapOracle.sol index 93e64d7..a34b8d9 100644 --- a/contracts/token/oracles/TwapOracle.sol +++ b/contracts/token/oracles/TwapOracle.sol @@ -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";