Skip to content

Commit 31681cc

Browse files
committed
added initial oracle storage and price calc
1 parent 49a48b0 commit 31681cc

10 files changed

+192
-19
lines changed

contracts/common/BiconomyTokenPaymasterErrors.sol

+10
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,14 @@ contract BiconomyTokenPaymasterErrors {
3131
* @notice Throws when invalid signature length in paymasterAndData
3232
*/
3333
error InvalidDynamicAdjustment();
34+
35+
/**
36+
* @notice Throws when each token doesnt have a corresponding oracle
37+
*/
38+
error TokensAndOraclesLengthMismatch();
39+
40+
/**
41+
* @notice Throws when oracle returns invalid price
42+
*/
43+
error OraclePriceNotPositive();
3444
}

contracts/interfaces/IBiconomySponsorshipPaymaster.sol

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { PackedUserOperation } from "@account-abstraction/contracts/core/UserOpe
66

77
interface IBiconomySponsorshipPaymaster{
88
event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue);
9-
event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue);
9+
event FixedDynamicAdjustmentChanged(uint256 indexed oldValue, uint256 indexed newValue);
1010
event VerifyingSignerChanged(address indexed oldSigner, address indexed newSigner, address indexed actor);
1111
event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor);
1212
event GasDeposited(address indexed paymasterId, uint256 indexed value);
@@ -22,7 +22,7 @@ interface IBiconomySponsorshipPaymaster{
2222

2323
function setFeeCollector(address _newFeeCollector) external payable;
2424

25-
function setUnaccountedGas(uint16 value) external payable;
25+
function setUnaccountedGas(uint256 value) external payable;
2626

2727
function withdrawERC20(IERC20 token, address target, uint256 amount) external;
2828

contracts/interfaces/IBiconomyTokenPaymaster.sol

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.26;
33

44
interface IBiconomyTokenPaymaster {
55
event UnaccountedGasChanged(uint256 indexed oldValue, uint256 indexed newValue);
6-
event FixedDynamicAdjustmentChanged(uint32 indexed oldValue, uint32 indexed newValue);
6+
event FixedDynamicAdjustmentChanged(uint256 indexed oldValue, uint256 indexed newValue);
77
event FeeCollectorChanged(address indexed oldFeeCollector, address indexed newFeeCollector, address indexed actor);
88
event GasDeposited(address indexed paymasterId, uint256 indexed value);
99
event GasWithdrawn(address indexed paymasterId, address indexed to, uint256 indexed value);
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
interface IOracle {
5+
function decimals() external view returns (uint8);
6+
function latestRoundData()
7+
external
8+
view
9+
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
10+
}

contracts/sponsorship/BiconomySponsorshipPaymaster.sol

+8-7
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ contract BiconomySponsorshipPaymaster is
4040

4141
address public verifyingSigner;
4242
address public feeCollector;
43-
uint16 public unaccountedGas;
44-
uint32 private constant PRICE_DENOMINATOR = 1e6;
43+
uint256 public unaccountedGas;
4544

45+
// Denominator to prevent precision errors when applying dynamic adjustment
46+
uint256 private constant PRICE_DENOMINATOR = 1e6;
4647
// Offset in PaymasterAndData to get to PAYMASTER_ID_OFFSET
4748
uint256 private constant PAYMASTER_ID_OFFSET = PAYMASTER_DATA_OFFSET;
4849
// Limit for unaccounted gas cost
49-
uint16 private constant UNACCOUNTED_GAS_LIMIT = 50_000;
50+
uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000;
5051

5152
mapping(address => uint256) public paymasterIdBalances;
5253

@@ -55,7 +56,7 @@ contract BiconomySponsorshipPaymaster is
5556
IEntryPoint _entryPoint,
5657
address _verifyingSigner,
5758
address _feeCollector,
58-
uint16 _unaccountedGas
59+
uint256 _unaccountedGas
5960
)
6061
BasePaymaster(_owner, _entryPoint)
6162
{
@@ -123,11 +124,11 @@ contract BiconomySponsorshipPaymaster is
123124
* @param value The new value to be set as the unaccountedEPGasOverhead.
124125
* @notice only to be called by the owner of the contract.
125126
*/
126-
function setUnaccountedGas(uint16 value) external payable override onlyOwner {
127+
function setUnaccountedGas(uint256 value) external payable override onlyOwner {
127128
if (value > UNACCOUNTED_GAS_LIMIT) {
128129
revert UnaccountedGasTooHigh();
129130
}
130-
uint16 oldValue = unaccountedGas;
131+
uint256 oldValue = unaccountedGas;
131132
unaccountedGas = value;
132133
emit UnaccountedGasChanged(oldValue, value);
133134
}
@@ -346,7 +347,7 @@ contract BiconomySponsorshipPaymaster is
346347
function _checkConstructorArgs(
347348
address _verifyingSigner,
348349
address _feeCollector,
349-
uint16 _unaccountedGas
350+
uint256 _unaccountedGas
350351
)
351352
internal
352353
view

contracts/token/BiconomyTokenPaymaster.sol

+49-4
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ pragma solidity ^0.8.26;
44
import { ReentrancyGuardTransient } from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
55
import { IEntryPoint } from "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
66
import { PackedUserOperation, UserOperationLib } from "@account-abstraction/contracts/core/UserOperationLib.sol";
7-
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
import { IERC20, ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
88
import { SafeTransferLib } from "@solady/src/utils/SafeTransferLib.sol";
99
import { BasePaymaster } from "../base/BasePaymaster.sol";
1010
import { BiconomyTokenPaymasterErrors } from "../common/BiconomyTokenPaymasterErrors.sol";
1111
import { IBiconomyTokenPaymaster } from "../interfaces/IBiconomyTokenPaymaster.sol";
12+
import { IOracle } from "../interfaces/oracles/IOracle.sol";
1213
import "@account-abstraction/contracts/core/Helpers.sol";
1314

1415
/**
@@ -29,9 +30,17 @@ contract BiconomyTokenPaymaster is
2930
{
3031
using UserOperationLib for PackedUserOperation;
3132

33+
struct TokenInfo {
34+
IOracle oracle;
35+
uint8 decimals;
36+
}
37+
38+
// State variables
3239
address public feeCollector;
3340
uint256 public unaccountedGas;
3441
uint256 public dynamicAdjustment;
42+
IOracle public nativeOracle; // ETH -> USD price
43+
mapping(address => TokenInfo) tokenDirectory;
3544

3645
// Limit for unaccounted gas cost
3746
uint256 private constant UNACCOUNTED_GAS_LIMIT = 50_000;
@@ -42,19 +51,30 @@ contract BiconomyTokenPaymaster is
4251
address _owner,
4352
IEntryPoint _entryPoint,
4453
uint256 _unaccountedGas,
45-
uint256 _dynamicAdjustment
54+
uint256 _dynamicAdjustment,
55+
IOracle _nativeOracle,
56+
address[] memory _tokens, // Array of token addresses
57+
IOracle[] memory _oracles // Array of corresponding oracle addresses
4658
)
4759
BasePaymaster(_owner, _entryPoint)
4860
{
4961
if (_unaccountedGas > UNACCOUNTED_GAS_LIMIT) {
5062
revert UnaccountedGasTooHigh();
5163
} else if (_dynamicAdjustment > MAX_DYNAMIC_ADJUSTMENT || _dynamicAdjustment == 0) {
5264
revert InvalidDynamicAdjustment();
65+
} else if (_tokens.length != _oracles.length) {
66+
revert TokensAndOraclesLengthMismatch();
5367
}
5468
assembly ("memory-safe") {
5569
sstore(feeCollector.slot, address()) // initialize fee collector to this contract
5670
sstore(unaccountedGas.slot, _unaccountedGas)
5771
sstore(dynamicAdjustment.slot, _dynamicAdjustment)
72+
sstore(nativeOracle.slot, _nativeOracle)
73+
}
74+
75+
// Populate the tokenToOracle mapping
76+
for (uint256 i = 0; i < _tokens.length; i++) {
77+
tokenDirectory[_tokens[i]] = TokenInfo(_oracles[i], ERC20(_tokens[i]).decimals());
5878
}
5979
}
6080

@@ -211,8 +231,7 @@ contract BiconomyTokenPaymaster is
211231
override
212232
returns (bytes memory context, uint256 validationData)
213233
{
214-
(maxCost);
215-
// Implementation of post-operation logic
234+
216235
}
217236

218237
/**
@@ -239,4 +258,30 @@ contract BiconomyTokenPaymaster is
239258
if (target == address(0)) revert CanNotWithdrawToZeroAddress();
240259
SafeTransferLib.safeTransfer(address(token), target, amount);
241260
}
261+
262+
/// @notice Fetches the latest token price.
263+
264+
/// @return price The latest token price fetched from the oracles.
265+
function getPrice(address tokenAddress) internal view returns (uint192) {
266+
TokenInfo memory tokenInfo = tokenDirectory[tokenAddress];
267+
uint192 tokenPrice = _fetchPrice(tokenInfo.oracle);
268+
uint192 nativeAssetPrice = _fetchPrice(nativeOracle);
269+
uint192 price = nativeAssetPrice * uint192(tokenInfo.decimals) / tokenPrice;
270+
return price;
271+
}
272+
273+
/// @notice Fetches the latest price from the given oracle.
274+
/// @dev This function is used to get the latest price from the tokenOracle or nativeAssetOracle.
275+
/// @param _oracle The oracle contract to fetch the price from.
276+
/// @return price The latest price fetched from the oracle.
277+
function _fetchPrice(IOracle _oracle) internal view returns (uint192 price) {
278+
(, int256 answer,, uint256 updatedAt,) = _oracle.latestRoundData();
279+
if (answer <= 0) {
280+
revert OraclePriceNotPositive();
281+
}
282+
// if (updatedAt < block.timestamp - stalenessThreshold) {
283+
// revert OraclePriceStale();
284+
// }
285+
price = uint192(int192(answer));
286+
}
242287
}
+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
import {IOracle} from "../../interfaces/oracles/IOracle.sol";
5+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6+
import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol";
7+
import {IUniswapV3PoolImmutables} from "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolImmutables.sol";
8+
9+
10+
contract TwapOracle is IOracle {
11+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
12+
/* CUSTOM ERRORS */
13+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
14+
/// @dev Invalid TWAP age, either too low or too high
15+
error InvalidTwapAge();
16+
17+
/// @dev Pool doesn't contain the base token
18+
error InvalidTokenOrPool();
19+
20+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
21+
/* CONSTANTS AND IMMUTABLES */
22+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
23+
/// @dev The Uniswap V3 pool address
24+
address public immutable pool;
25+
26+
/// @dev The base token address (the one which price is being fetched)
27+
address public immutable baseToken;
28+
29+
/// @dev The base token decimals
30+
uint256 public immutable baseTokenDecimals;
31+
32+
/// @dev The quote token address (WETH or USD stable coin)
33+
address public immutable quoteToken;
34+
35+
/// @dev The quote token decimals
36+
uint256 public immutable quoteTokenDecimals;
37+
38+
/// @dev Default TWAP age, used to fetch the price
39+
uint32 public immutable twapAge;
40+
41+
uint32 public constant MINIMUM_TWAP_AGE = 1 minutes;
42+
uint32 public constant MAXIMUM_TWAP_AGE = 7 days;
43+
44+
uint256 public constant ORACLE_DECIMALS = 1e8;
45+
46+
constructor(
47+
address _pool,
48+
uint32 _twapAge,
49+
address _baseToken
50+
) {
51+
pool = _pool;
52+
53+
if (_twapAge < MINIMUM_TWAP_AGE || _twapAge > MAXIMUM_TWAP_AGE) revert InvalidTwapAge();
54+
twapAge = _twapAge;
55+
56+
address token0 = IUniswapV3PoolImmutables(_pool).token0();
57+
address token1 = IUniswapV3PoolImmutables(_pool).token1();
58+
59+
if (_baseToken != token0 && _baseToken != token1) revert InvalidTokenOrPool();
60+
61+
baseToken = _baseToken;
62+
baseTokenDecimals = 10 ** IERC20Metadata(baseToken).decimals();
63+
64+
quoteToken = token0 == baseToken ? token1 : token0;
65+
quoteTokenDecimals = 10 ** IERC20Metadata(quoteToken).decimals();
66+
}
67+
68+
function decimals() external override pure returns (uint8) {
69+
return 8;
70+
}
71+
72+
function latestRoundData() external override view returns (
73+
uint80 roundId,
74+
int256 answer,
75+
uint256 startedAt,
76+
uint256 updatedAt,
77+
uint80 answeredInRound
78+
) {
79+
uint256 _price = _fetchTwap();
80+
81+
// Normalize the price to the oracle decimals
82+
uint256 price = _price * ORACLE_DECIMALS / quoteTokenDecimals;
83+
84+
return _buildLatestRoundData(price);
85+
}
86+
87+
function _buildLatestRoundData(uint256 price) internal view returns (
88+
uint80 roundId,
89+
int256 answer,
90+
uint256 startedAt,
91+
uint256 updatedAt,
92+
uint80 answeredInRound
93+
) {
94+
return (0, int256(price), 0, block.timestamp, 0);
95+
}
96+
97+
function _fetchTwap() internal view returns (uint256) {
98+
(int24 arithmeticMeanTick,) = OracleLibrary.consult(pool, twapAge);
99+
100+
return OracleLibrary.getQuoteAtTick(
101+
arithmeticMeanTick,
102+
uint128(baseTokenDecimals), // Base token amount is equal to 1 token
103+
baseToken,
104+
quoteToken
105+
);
106+
}
107+
}

lib/v3-core

Submodule v3-core updated 93 files

lib/v3-periphery

Submodule v3-periphery updated 90 files

test/unit/concrete/TestSponsorshipPaymaster.t.sol

+3-3
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,14 @@ contract TestSponsorshipPaymasterWithDynamicAdjustment is TestBase {
129129
}
130130

131131
function test_SetUnaccountedGas() external prankModifier(PAYMASTER_OWNER.addr) {
132-
uint16 initialUnaccountedGas = bicoPaymaster.unaccountedGas();
133-
uint16 newUnaccountedGas = 5000;
132+
uint256 initialUnaccountedGas = bicoPaymaster.unaccountedGas();
133+
uint256 newUnaccountedGas = 5000;
134134

135135
vm.expectEmit(true, true, false, true, address(bicoPaymaster));
136136
emit IBiconomySponsorshipPaymaster.UnaccountedGasChanged(initialUnaccountedGas, newUnaccountedGas);
137137
bicoPaymaster.setUnaccountedGas(newUnaccountedGas);
138138

139-
uint48 resultingUnaccountedGas = bicoPaymaster.unaccountedGas();
139+
uint256 resultingUnaccountedGas = bicoPaymaster.unaccountedGas();
140140
assertEq(resultingUnaccountedGas, newUnaccountedGas);
141141
}
142142

0 commit comments

Comments
 (0)