From 31681cc1d14af326e362b66772f439c73099181e Mon Sep 17 00:00:00 2001 From: Shivaansh Kapoor Date: Thu, 5 Sep 2024 19:47:59 +0400 Subject: [PATCH] added initial oracle storage and price calc --- .../common/BiconomyTokenPaymasterErrors.sol | 10 ++ .../IBiconomySponsorshipPaymaster.sol | 4 +- .../interfaces/IBiconomyTokenPaymaster.sol | 2 +- contracts/interfaces/oracles/IOracle.sol | 10 ++ .../BiconomySponsorshipPaymaster.sol | 15 +-- contracts/token/BiconomyTokenPaymaster.sol | 53 ++++++++- contracts/token/oracles/TwapOracle.sol | 107 ++++++++++++++++++ lib/v3-core | 2 +- lib/v3-periphery | 2 +- .../concrete/TestSponsorshipPaymaster.t.sol | 6 +- 10 files changed, 192 insertions(+), 19 deletions(-) create mode 100644 contracts/interfaces/oracles/IOracle.sol create mode 100644 contracts/token/oracles/TwapOracle.sol diff --git a/contracts/common/BiconomyTokenPaymasterErrors.sol b/contracts/common/BiconomyTokenPaymasterErrors.sol index 882e638..9b0c365 100644 --- a/contracts/common/BiconomyTokenPaymasterErrors.sol +++ b/contracts/common/BiconomyTokenPaymasterErrors.sol @@ -31,4 +31,14 @@ contract BiconomyTokenPaymasterErrors { * @notice Throws when invalid signature length in paymasterAndData */ error InvalidDynamicAdjustment(); + + /** + * @notice Throws when each token doesnt have a corresponding oracle + */ + error TokensAndOraclesLengthMismatch(); + + /** + * @notice Throws when oracle returns invalid price + */ + error OraclePriceNotPositive(); } diff --git a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol index 7ad2651..2948273 100644 --- a/contracts/interfaces/IBiconomySponsorshipPaymaster.sol +++ b/contracts/interfaces/IBiconomySponsorshipPaymaster.sol @@ -6,7 +6,7 @@ import { PackedUserOperation } from "@account-abstraction/contracts/core/UserOpe interface IBiconomySponsorshipPaymaster{ event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); - event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); + event FixedDynamicAdjustmentChanged(uint256 indexed oldValue, uint256 indexed newValue); event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor); event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event GasDeposited(address indexed paymasterId, uint256 indexed value); @@ -22,7 +22,7 @@ interface IBiconomySponsorshipPaymaster{ function setFeeCollector(address _newFeeCollector) external payable; - function setUnaccountedGas(uint16 value) external payable; + function setUnaccountedGas(uint256 value) external payable; function withdrawERC20(IERC20 token, address target, uint256 amount) external; diff --git a/contracts/interfaces/IBiconomyTokenPaymaster.sol b/contracts/interfaces/IBiconomyTokenPaymaster.sol index 7ac2633..58157f9 100644 --- a/contracts/interfaces/IBiconomyTokenPaymaster.sol +++ b/contracts/interfaces/IBiconomyTokenPaymaster.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.26; interface IBiconomyTokenPaymaster { event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue); - event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue); + event FixedDynamicAdjustmentChanged(uint256 indexed oldValue, uint256 indexed newValue); event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor); event GasDeposited(address indexed paymasterId, uint256 indexed value); event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value); diff --git a/contracts/interfaces/oracles/IOracle.sol b/contracts/interfaces/oracles/IOracle.sol new file mode 100644 index 0000000..5a15d66 --- /dev/null +++ b/contracts/interfaces/oracles/IOracle.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IOracle { + function decimals() external view returns (uint8); + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} \ No newline at end of file diff --git a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol index 9db7657..706b38e 100644 --- a/contracts/sponsorship/BiconomySponsorshipPaymaster.sol +++ b/contracts/sponsorship/BiconomySponsorshipPaymaster.sol @@ -40,13 +40,14 @@ contract BiconomySponsorshipPaymaster is address public verifyingSigner; address public feeCollector; - uint16 public unaccountedGas; - uint32 private constant PRICE_DENOMINATOR = 1e6; + uint256 public unaccountedGas; + // Denominator to prevent precision errors when applying dynamic adjustment + uint256 private constant PRICE_DENOMINATOR = 1e6; // Offset in PaymasterAndData to get to PAYMASTER_ID_OFFSET uint256 private constant PAYMASTER_ID_OFFSET = PAYMASTER_DATA_OFFSET; // Limit for unaccounted gas cost - uint16 private constant UNACCOUNTED_GAS_LIMIT = 50_000; + uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; mapping(address => uint256) public paymasterIdBalances; @@ -55,7 +56,7 @@ contract BiconomySponsorshipPaymaster is IEntryPoint _entryPoint, address _verifyingSigner, address _feeCollector, - uint16 _unaccountedGas + uint256 _unaccountedGas ) BasePaymaster(_owner, _entryPoint) { @@ -123,11 +124,11 @@ contract BiconomySponsorshipPaymaster is * @param value The new value to be set as the unaccountedEPGasOverhead. * @notice only to be called by the owner of the contract. */ - function setUnaccountedGas(uint16 value) external payable override onlyOwner { + function setUnaccountedGas(uint256 value) external payable override onlyOwner { if (value > UNACCOUNTED_GAS_LIMIT) { revert UnaccountedGasTooHigh(); } - uint16 oldValue = unaccountedGas; + uint256 oldValue = unaccountedGas; unaccountedGas = value; emit UnaccountedGasChanged(oldValue, value); } @@ -346,7 +347,7 @@ contract BiconomySponsorshipPaymaster is function _checkConstructorArgs( address _verifyingSigner, address _feeCollector, - uint16 _unaccountedGas + uint256 _unaccountedGas ) internal view diff --git a/contracts/token/BiconomyTokenPaymaster.sol b/contracts/token/BiconomyTokenPaymaster.sol index 0810fc0..df36782 100644 --- a/contracts/token/BiconomyTokenPaymaster.sol +++ b/contracts/token/BiconomyTokenPaymaster.sol @@ -4,11 +4,12 @@ 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 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20, ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.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 "@account-abstraction/contracts/core/Helpers.sol"; /** @@ -29,9 +30,17 @@ contract BiconomyTokenPaymaster is { using UserOperationLib for PackedUserOperation; + struct TokenInfo { + IOracle oracle; + uint8 decimals; + } + + // State variables address public feeCollector; uint256 public unaccountedGas; uint256 public dynamicAdjustment; + IOracle public nativeOracle; // ETH -> USD price + mapping(address => TokenInfo) tokenDirectory; // Limit for unaccounted gas cost uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000; @@ -42,7 +51,10 @@ contract BiconomyTokenPaymaster is address _owner, IEntryPoint _entryPoint, uint256 _unaccountedGas, - uint256 _dynamicAdjustment + uint256 _dynamicAdjustment, + IOracle _nativeOracle, + address[] memory _tokens, // Array of token addresses + IOracle[] memory _oracles // Array of corresponding oracle addresses ) BasePaymaster(_owner, _entryPoint) { @@ -50,11 +62,19 @@ contract BiconomyTokenPaymaster is revert UnaccountedGasTooHigh(); } else if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment == 0) { revert InvalidDynamicAdjustment(); + } else if (_tokens.length != _oracles.length) { + revert TokensAndOraclesLengthMismatch(); } assembly ("memory-safe") { sstore(feeCollector.slot, address()) // initialize fee collector to this contract sstore(unaccountedGas.slot, _unaccountedGas) sstore(dynamicAdjustment.slot, _dynamicAdjustment) + sstore(nativeOracle.slot, _nativeOracle) + } + + // Populate the tokenToOracle mapping + for (uint256 i = 0; i < _tokens.length; i++) { + tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], ERC20(_tokens[i]).decimals()); } } @@ -211,8 +231,7 @@ contract BiconomyTokenPaymaster is override returns (bytes memory context, uint256 validationData) { - (maxCost); - // Implementation of post-operation logic + } /** @@ -239,4 +258,30 @@ contract BiconomyTokenPaymaster is if (target == address(0)) revert CanNotWithdrawToZeroAddress(); SafeTransferLib.safeTransfer(address(token), target, amount); } + + /// @notice Fetches the latest token price. + + /// @return price The latest token price fetched from the oracles. + function getPrice(address tokenAddress) internal view returns (uint192) { + TokenInfo memory tokenInfo = tokenDirectory[tokenAddress]; + uint192 tokenPrice = _fetchPrice(tokenInfo.oracle); + uint192 nativeAssetPrice = _fetchPrice(nativeOracle); + uint192 price = nativeAssetPrice * uint192(tokenInfo.decimals) / tokenPrice; + return price; + } + + /// @notice Fetches the latest price from the given oracle. + /// @dev This function is used to get the latest price from the tokenOracle or nativeAssetOracle. + /// @param _oracle The oracle contract to fetch the price from. + /// @return price The latest price fetched from the oracle. + function _fetchPrice(IOracle _oracle) internal view returns (uint192 price) { + (, int256 answer,, uint256 updatedAt,) = _oracle.latestRoundData(); + if (answer <= 0) { + revert OraclePriceNotPositive(); + } + // if (updatedAt < block.timestamp - stalenessThreshold) { + // revert OraclePriceStale(); + // } + price = uint192(int192(answer)); + } } diff --git a/contracts/token/oracles/TwapOracle.sol b/contracts/token/oracles/TwapOracle.sol new file mode 100644 index 0000000..93e64d7 --- /dev/null +++ b/contracts/token/oracles/TwapOracle.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {IOracle} from "../../interfaces/oracles/IOracle.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol"; +import {IUniswapV3PoolImmutables} from "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolImmutables.sol"; + + +contract TwapOracle is IOracle { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @dev Invalid TWAP age, either too low or too high + error InvalidTwapAge(); + + /// @dev Pool doesn't contain the base token + error InvalidTokenOrPool(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CONSTANTS AND IMMUTABLES */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + /// @dev The Uniswap V3 pool address + address public immutable pool; + + /// @dev The base token address (the one which price is being fetched) + address public immutable baseToken; + + /// @dev The base token decimals + uint256 public immutable baseTokenDecimals; + + /// @dev The quote token address (WETH or USD stable coin) + address public immutable quoteToken; + + /// @dev The quote token decimals + uint256 public immutable quoteTokenDecimals; + + /// @dev Default TWAP age, used to fetch the price + uint32 public immutable twapAge; + + uint32 public constant MINIMUM_TWAP_AGE = 1 minutes; + uint32 public constant MAXIMUM_TWAP_AGE = 7 days; + + uint256 public constant ORACLE_DECIMALS = 1e8; + + constructor( + address _pool, + uint32 _twapAge, + address _baseToken + ) { + pool = _pool; + + if (_twapAge < MINIMUM_TWAP_AGE || _twapAge > MAXIMUM_TWAP_AGE) revert InvalidTwapAge(); + twapAge = _twapAge; + + address token0 = IUniswapV3PoolImmutables(_pool).token0(); + address token1 = IUniswapV3PoolImmutables(_pool).token1(); + + if (_baseToken != token0 && _baseToken != token1) revert InvalidTokenOrPool(); + + baseToken = _baseToken; + baseTokenDecimals = 10 ** IERC20Metadata(baseToken).decimals(); + + quoteToken = token0 == baseToken ? token1 : token0; + quoteTokenDecimals = 10 ** IERC20Metadata(quoteToken).decimals(); + } + + function decimals() external override pure returns (uint8) { + return 8; + } + + function latestRoundData() external override view returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + uint256 _price = _fetchTwap(); + + // Normalize the price to the oracle decimals + uint256 price = _price * ORACLE_DECIMALS / quoteTokenDecimals; + + return _buildLatestRoundData(price); + } + + function _buildLatestRoundData(uint256 price) internal view returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) { + return (0, int256(price), 0, block.timestamp, 0); + } + + function _fetchTwap() internal view returns (uint256) { + (int24 arithmeticMeanTick,) = OracleLibrary.consult(pool, twapAge); + + return OracleLibrary.getQuoteAtTick( + arithmeticMeanTick, + uint128(baseTokenDecimals), // Base token amount is equal to 1 token + baseToken, + quoteToken + ); + } +} \ No newline at end of file diff --git a/lib/v3-core b/lib/v3-core index e3589b1..6562c52 160000 --- a/lib/v3-core +++ b/lib/v3-core @@ -1 +1 @@ -Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 +Subproject commit 6562c52e8f75f0c10f9deaf44861847585fc8129 diff --git a/lib/v3-periphery b/lib/v3-periphery index 80f26c8..b325bb0 160000 --- a/lib/v3-periphery +++ b/lib/v3-periphery @@ -1 +1 @@ -Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 +Subproject commit b325bb0905d922ae61fcc7df85ee802e8df5e96c diff --git a/test/unit/concrete/TestSponsorshipPaymaster.t.sol b/test/unit/concrete/TestSponsorshipPaymaster.t.sol index 4800594..cc8bc8c 100644 --- a/test/unit/concrete/TestSponsorshipPaymaster.t.sol +++ b/test/unit/concrete/TestSponsorshipPaymaster.t.sol @@ -129,14 +129,14 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase { } function test_SetUnaccountedGas() external prankModifier(PAYMASTER_OWNER.addr) { - uint16 initialUnaccountedGas = bicoPaymaster.unaccountedGas(); - uint16 newUnaccountedGas = 5000; + uint256 initialUnaccountedGas = bicoPaymaster.unaccountedGas(); + uint256 newUnaccountedGas = 5000; vm.expectEmit(true, true, false, true, address(bicoPaymaster)); emit IBiconomySponsorshipPaymaster.UnaccountedGasChanged(initialUnaccountedGas, newUnaccountedGas); bicoPaymaster.setUnaccountedGas(newUnaccountedGas); - uint48 resultingUnaccountedGas = bicoPaymaster.unaccountedGas(); + uint256 resultingUnaccountedGas = bicoPaymaster.unaccountedGas(); assertEq(resultingUnaccountedGas, newUnaccountedGas); }