diff --git a/foundry.toml b/foundry.toml index 45b3c4e9..d08e5ee3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,8 @@ src = 'src' out = 'out' libs = ['node_modules', 'lib'] -cache_path = 'forge-cache/sol' +test = 'test/foundry' +cache_path = 'forge-cache/sol' optimizer = true optimizer_runs = 2000 via_ir = false @@ -11,14 +12,16 @@ remappings = ['ds-test/=lib/forge-std/lib/ds-test/src/', 'forge-std/=lib/forge-std/src/', '@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/', '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/', - '@offchainlabs/upgrade-executor/=node_modules/@offchainlabs/upgrade-executor/'] + '@offchainlabs/upgrade-executor/=node_modules/@offchainlabs/upgrade-executor/', + '@uniswap/v2-core/=node_modules/@uniswap/v2-core/contracts', + '@uniswap/lib/=node_modules/@uniswap/lib/contracts'] fs_permissions = [{ access = "read", path = "./"}] [profile.yul] src = 'yul' out = 'out/yul' libs = ['node_modules', 'lib'] -cache_path = 'forge-cache/yul' +cache_path = 'forge-cache/yul' remappings = [] auto_detect_remappings = false diff --git a/package.json b/package.json index 3a3460ba..0b0451dc 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,8 @@ "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/eslint-plugin-tslint": "^5.27.1", "@typescript-eslint/parser": "^5.14.0", + "@uniswap/lib": "^4.0.1-alpha", + "@uniswap/v2-core": "^1.0.1", "audit-ci": "^6.6.1", "chai": "^4.3.4", "dotenv": "^16.3.1", diff --git a/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol b/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol new file mode 100644 index 00000000..e2ebee41 --- /dev/null +++ b/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IFeeTokenPricer} from "../../../src/bridge/ISequencerInbox.sol"; + +// NOTICE: This contract has not been audited or properly tested. It is for example purposes only + +/// @title A constant price fee token pricer +/// @notice The most simple kind of fee token pricer, does not account for any change in exchange rate +contract ConstantExchangeRatePricer is IFeeTokenPricer { + uint256 immutable exchangeRate; + + constructor( + uint256 _exchangeRate + ) { + exchangeRate = _exchangeRate; + } + + // @inheritdoc IFeeTokenPricer + function getExchangeRate() external view returns (uint256) { + return exchangeRate; + } +} diff --git a/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol b/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol new file mode 100644 index 00000000..c8713b5d --- /dev/null +++ b/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IFeeTokenPricer} from "../../../src/bridge/ISequencerInbox.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title A uniswap twap pricer +/// @notice An example of a type 1 fee token pricer. The owner can adjust the exchange rate at any time +/// to ensure the batch poster is reimbursed an appropriate amount on the child chain +contract OwnerAdjustableExchangeRatePricer is IFeeTokenPricer, Ownable { + uint256 public exchangeRate; + + event ExchangeRateSet(uint256 newExchangeRate); + + constructor( + uint256 initialExchangeRate + ) Ownable() { + exchangeRate = initialExchangeRate; + emit ExchangeRateSet(initialExchangeRate); + } + + function setExchangeRate( + uint256 _exchangeRate + ) external onlyOwner { + exchangeRate = _exchangeRate; + emit ExchangeRateSet(_exchangeRate); + } + + // @inheritdoc IFeeTokenPricer + function getExchangeRate() external view returns (uint256) { + return exchangeRate; + } +} diff --git a/test/foundry/fee-token-pricers/README.md b/test/foundry/fee-token-pricers/README.md new file mode 100644 index 00000000..7f5c1177 --- /dev/null +++ b/test/foundry/fee-token-pricers/README.md @@ -0,0 +1,41 @@ +# Fee token pricer + +When a chain is in AnyTrust mode transaction data is posted to an alternative data availability provider. However when in a chain is in rollup mode, transaction data is posted to the parent chain which the batch poster must pay for. When not using a custom fee token, the cost of posting this batch is relayed to the child chain where the batch poster will be reimbursed from user fees. However when the child chain is using a different fee token to the parent chain the data cost will paid in units of the parent chain fee token, and refunded in units of the child chain fee token. Therefore in order to refund the correct amount an exchange rate between the tokens must be used. This is what the fee token pricer provides. + +## Implementation approach + +When the batch poster posts data to the parent chain a batch spending report is produced. This batch spending report contains the gas price paid by the poster, and the amount of data in the batch. In order to reimburse the batch poster the correct amount in child chain fee tokens, the gas price in the batch spending report is scaled by the child to parent chain token price. In order to get this price the SequencerInbox calls `getExchangeRate` function on the fee token pricer at the time of creating a report. The chain owner can update the fee token pricer to a different implementation at any time. + +Although the batch poster is receiving reimbursement in units of the child chain fee token rather than the parent chain units which they used to pay for gas, the value that they are reimbursed should be equal to the value that they paid. + +## Fee token pricer types + +A chain can choose different fee token pricer implementations to retrieve the exchange rate. Since the fees are reimbursed in child chain tokens but paid for in the parent chain tokens, there is an exchange rate risk. If the price deviates a lot before the batch poster converts the child chain currency back to parent chain currency, they may end up receiving less or more tokens than they originally paid for in gas. Below are some implementation types for the fee token pricer that have different tradeoffs for the batch poster and chain owner. Since the chain owner can change the fee token pricer at any time, the batch poster must always trust the chain owner not to do this for malicious purpose. + +**Note.** There are some examples of these pricers in this repo, however none of these examples have been audited or properly tested, and are not ready for production use. These are example implementations to give an idea of the different options. Chain owners are expected to implement their own fee token pricer. + +### Type 1 - Chain owner defined oracle + +In this option the chain owner simply updated the exchange rate manually. This is the simplest option as it requires no external oracle or complicated implementation. However, unless the chain owner updates the price regularly it may diverge from the real price, causing under or over reimbursement. Additionally, unless a further safe guards are added, the batch poster must completely trust the chain owner to reimburse the correct amount. This option makes the most sense for a new chain, and where the batch poster and chain owner are the same entity or have a trusted relationship. The batch poster must also have an appetite for exchange risk, however this can be mitigated by artificially inflating the price to reduce the chance the batch poster is under reimbursed. + +### Type 2 - External oracle + +In this option an external oracle is used to fetch the exchange rate. Here the fee token pricer is responsible for ensuring the price is in the correct format and applying any safe guards that might be relevant. This option is easier to maintain that option 1. since an external party is reponsible for keep an up to date price on chain. However this places trust in the external party to keep the price up to date and to provide the correct price. To that end the pricer may apply some safe guards to avoid the price going too high or too low. This option also carries the same exchange risk as option 1, so a similar mitigation of marking up the price by a small amount might help to avoid under reimbursement + +An example of this approach can be seen in [UniswapV2TwapPricer.sol](./uniswap-v2-twap/UniswapV2TwapPricer.sol). + +### Type 3 - Exchange rate tracking + +In this option it is assumed the batch poster has units of the child chain token and needs to trade them for units of the parent chain token to pay for the gas. They can record the exchange rate they used for this original trade in the fee token pricer, which will return that price when the batch poster requests an exchange rate to use. This removes the exchange risk problem, at the expense of a more complex accounting system in the fee token pricer. In this option the batch poster is implicitly a holder of the same number of child chain tokens at all times, they are not guaranteed any number of parent chain tokens. + +The trust model in this approach is not that the batch poster is not forced to honestly report the correct price, but instead that the batch poster can be sure that they'll be refunded the correct amount. + +An example of this approach can be seen in [TradeTracker.sol](./trade-tracker/TradeTracker.sol). + +## Fee token pricer implementation considerations + +When implementing a fee token pricer the trust assumptions of each of the involved parties must be considered. + +- **Chain owner** - the chain owner is always trusted as they can change the fee token pricer at any time +- **Batch poster** - the batch poster is already trusted to provide valid batches that don't inflate data costs for users. In a type 3 fee token pricer they are additionally trusted to report the correct trade price +- **External parties** - in a type 2 fee token pricer an external party is trusted to provide up to date price information. If the price provided is too low the batch poster will be under-refunded, if the price provided is too high the batch poster will be over-refunded. To that end implementers should consider including price guards in their pricer to ensure the external can't provide values too far from the correct price. As an example, if the external party chose to set the price to max(uint) it would drain the child chain's gas pool, and dramatically raise the price for users. The chain owner would need to call admin functions to reset the sytem. This could be avoided by putting logic in the pricer to prevent extreme values. diff --git a/test/foundry/fee-token-pricers/trade-tracker/TradeTracker.sol b/test/foundry/fee-token-pricers/trade-tracker/TradeTracker.sol new file mode 100644 index 00000000..691ca9f4 --- /dev/null +++ b/test/foundry/fee-token-pricers/trade-tracker/TradeTracker.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IFeeTokenPricer} from "../../../../src/bridge/ISequencerInbox.sol"; +import {IGasRefunder} from "../../../../src/libraries/IGasRefunder.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +abstract contract TradeTracker is IFeeTokenPricer, IGasRefunder { + using SafeERC20 for IERC20; + + uint8 public immutable childTokenDecimals; + uint256 public immutable calldataCost; + address public immutable sequencerInbox; + + uint256 public thisChainTokenReserve; + uint256 public childChainTokenReserve; + + error NotSequencerInbox(address caller); + error InsufficientThisChainTokenReserve(address batchPoster); + error InsufficientChildChainTokenReserve(address batchPoster); + + constructor(uint8 _childTokenDecimals, uint256 _calldataCost, address _sequencerInbox) { + childTokenDecimals = _childTokenDecimals; + calldataCost = _calldataCost; + sequencerInbox = _sequencerInbox; + } + + // @inheritdoc IFeeTokenPricer + function getExchangeRate() public view returns (uint256) { + uint256 thisChainTokens = thisChainTokenReserve; + uint256 childChainTokens = childChainTokenReserve; + // if either of the reserves is empty the spender will receive no reimbursement + if (thisChainTokens == 0 || childChainTokens == 0) { + return 0; + } + + // gas tokens on this chain always have 18 decimals + return (childChainTokens * 1e18) / thisChainTokens; + } + + /// @notice Record that a trade occurred. The sub contract can choose how and when trades can be recorded + /// but it is likely that the batchposter will be trusted to report the correct trade price. + /// @param thisChainTokensPurchased The number of this chain tokens purchased + /// @param childChainTokensPaid The number of child chain tokens purchased + function recordTrade(uint256 thisChainTokensPurchased, uint256 childChainTokensPaid) internal { + thisChainTokenReserve += thisChainTokensPurchased; + childChainTokenReserve += scaleTo18Decimals(childChainTokensPaid); + } + + /// @notice A hook to record when gas is spent by the batch poster + /// Matches the interface used in GasRefundEnable so can be used by the caller as a gas refunder + /// @param batchPoster The address spending the gas + /// @param gasUsed The amount of gas used + /// @param calldataSize The calldata size - will be added to the gas used at some predetermined rate + function onGasSpent( + address payable batchPoster, + uint256 gasUsed, + uint256 calldataSize + ) external returns (bool) { + if (msg.sender != sequencerInbox) revert NotSequencerInbox(msg.sender); + + // each time gas is spent we reduce the reserves + // to represent what will have been refunded on the child chain + + gasUsed += calldataSize * calldataCost; + uint256 thisTokenSpent = gasUsed * block.basefee; + uint256 exchangeRateUsed = getExchangeRate(); + uint256 childTokenReceived = exchangeRateUsed * thisTokenSpent / 1e18; + + if (thisTokenSpent > thisChainTokenReserve) { + revert InsufficientThisChainTokenReserve(batchPoster); + } + thisChainTokenReserve -= thisTokenSpent; + + if (childTokenReceived > childChainTokenReserve) { + // it shouldn't be possible to hit this revert if the maths of calculating an exchange rate are correct + revert InsufficientChildChainTokenReserve(batchPoster); + } + childChainTokenReserve -= childTokenReceived; + + return true; + } + + function scaleTo18Decimals( + uint256 amount + ) internal view returns (uint256) { + if (childTokenDecimals == 18) { + return amount; + } else if (childTokenDecimals < 18) { + return amount * 10 ** (18 - childTokenDecimals); + } else { + return amount / 10 ** (childTokenDecimals - 18); + } + } +} diff --git a/test/foundry/fee-token-pricers/trade-tracker/TradeTracker.t.sol b/test/foundry/fee-token-pricers/trade-tracker/TradeTracker.t.sol new file mode 100644 index 00000000..46e90e47 --- /dev/null +++ b/test/foundry/fee-token-pricers/trade-tracker/TradeTracker.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import {TradeTracker, IERC20} from "./TradeTracker.sol"; +import "../../../../src/bridge/SequencerInbox.sol"; +import {ERC20Bridge} from "../../../../src/bridge/ERC20Bridge.sol"; +import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; + +import "../../util/TestUtil.sol"; + +contract SimpleTradeTracker is TradeTracker { + constructor( + address _sequencerInbox + ) TradeTracker(6, 16, _sequencerInbox) {} + + function trade(uint256 thisChainTokens, uint256 childChainTokens) public { + recordTrade(thisChainTokens, childChainTokens); + } +} + +contract TrackerTest is Test { + address public batchPoster = makeAddr("batchPoster"); + address public seqInbox = makeAddr("seqInbox"); + + function testExchangeRate() public { + SimpleTradeTracker tradeTracker = new SimpleTradeTracker(seqInbox); + + uint256 thisChainReserve = 10e18; + uint256 childChainReserve = 100e6; + + vm.startPrank(address(this), batchPoster); + assertEq(tradeTracker.getExchangeRate(), 0); + + // do a trade and set the exchange rate + uint256 exRate1 = (childChainReserve * 1e18 / thisChainReserve) * 1e12; + tradeTracker.trade(thisChainReserve, childChainReserve); + assertEq(tradeTracker.getExchangeRate(), exRate1); + + // trade again at the same rate + tradeTracker.trade(thisChainReserve, childChainReserve); + assertEq(tradeTracker.getExchangeRate(), exRate1); + + // trade again at different rate + tradeTracker.trade(thisChainReserve / 2, childChainReserve); + uint256 exRate2 = (childChainReserve * 3 * 1e18 / (thisChainReserve * 5 / 2)) * 1e12; + assertEq(tradeTracker.getExchangeRate(), exRate2); + + vm.stopPrank(); + } + + function testOnGasSpent() public { + vm.fee(1 gwei); + + SimpleTradeTracker tradeTracker = new SimpleTradeTracker(seqInbox); + + uint256 gasUsed = 300_000; + uint256 calldataSize = 10_000; + + vm.startPrank(address(seqInbox), batchPoster); + vm.expectRevert( + abi.encodeWithSelector( + TradeTracker.InsufficientThisChainTokenReserve.selector, batchPoster + ) + ); + tradeTracker.onGasSpent(payable(batchPoster), gasUsed, calldataSize); + + // trade some, but not enough + tradeTracker.trade( + (gasUsed - 1000 + calldataSize * tradeTracker.calldataCost()) * block.basefee, + 10 * 10 ** tradeTracker.childTokenDecimals() + ); + vm.expectRevert( + abi.encodeWithSelector( + TradeTracker.InsufficientThisChainTokenReserve.selector, batchPoster + ) + ); + tradeTracker.onGasSpent(payable(batchPoster), gasUsed, calldataSize); + + // trade some more + tradeTracker.trade(10000 * block.basefee, 10 * 10 ** tradeTracker.childTokenDecimals()); + uint256 exchangeRateBefore = tradeTracker.getExchangeRate(); + tradeTracker.onGasSpent(payable(batchPoster), gasUsed, calldataSize); + + uint256 thisChainTokensUsed = + (gasUsed + calldataSize * tradeTracker.calldataCost()) * block.basefee; + uint256 childChainTokensUsed = thisChainTokensUsed * exchangeRateBefore / 1e18; + uint256 thisChainReserveAfter = ( + (10000 + gasUsed - 1000 + calldataSize * tradeTracker.calldataCost()) * block.basefee + - thisChainTokensUsed + ); + uint256 childChainReserveAfter = + (20 * 10 ** tradeTracker.childTokenDecimals() * 1e12) - childChainTokensUsed; + uint256 exchangeRateAfter = childChainReserveAfter * 1e18 / thisChainReserveAfter; + assertEq(tradeTracker.getExchangeRate(), exchangeRateAfter); + } +} diff --git a/test/foundry/fee-token-pricers/uniswap-v2-twap/FixedPoint.sol b/test/foundry/fee-token-pricers/uniswap-v2-twap/FixedPoint.sol new file mode 100644 index 00000000..a8754869 --- /dev/null +++ b/test/foundry/fee-token-pricers/uniswap-v2-twap/FixedPoint.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./FullMath.sol"; + +// Modified from https://github.com/Uniswap/solidity-lib +// DO NOT USE IN PRODUCTION + +// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format)) +library FixedPoint { + // range: [0, 2**112 - 1] + // resolution: 1 / 2**112 + struct uq112x112 { + uint224 _x; + } + + // range: [0, 2**144 - 1] + // resolution: 1 / 2**112 + struct uq144x112 { + uint256 _x; + } + + uint8 public constant RESOLUTION = 112; + uint256 public constant Q112 = 0x10000000000000000000000000000; // 2**112 + uint256 private constant Q224 = 0x100000000000000000000000000000000000000000000000000000000; // 2**224 + uint256 private constant LOWER_MASK = 0xffffffffffffffffffffffffffff; // decimal of UQ*x112 (lower 112 bits) + + // decode a UQ144x112 into a uint144 by truncating after the radix point + function decode144( + uq144x112 memory self + ) internal pure returns (uint144) { + return uint144(self._x >> RESOLUTION); + } + + // multiply a UQ112x112 by a uint, returning a UQ144x112 + // reverts on overflow + function mul(uq112x112 memory self, uint256 y) internal pure returns (uq144x112 memory) { + uint256 z = 0; + require(y == 0 || (z = self._x * y) / y == self._x, "FixedPoint::mul: overflow"); + return uq144x112(z); + } + + // returns a UQ112x112 which represents the ratio of the numerator to the denominator + // can be lossy + function fraction( + uint256 numerator, + uint256 denominator + ) internal pure returns (uq112x112 memory) { + require(denominator > 0, "FixedPoint::fraction: division by zero"); + if (numerator == 0) return FixedPoint.uq112x112(0); + + if (numerator <= type(uint144).max) { + uint256 result = (numerator << RESOLUTION) / denominator; + require(result <= type(uint224).max, "FixedPoint::fraction: overflow"); + return uq112x112(uint224(result)); + } else { + uint256 result = FullMath.mulDiv(numerator, Q112, denominator); + require(result <= type(uint224).max, "FixedPoint::fraction: overflow"); + return uq112x112(uint224(result)); + } + } +} diff --git a/test/foundry/fee-token-pricers/uniswap-v2-twap/FullMath.sol b/test/foundry/fee-token-pricers/uniswap-v2-twap/FullMath.sol new file mode 100644 index 00000000..fe60ab4a --- /dev/null +++ b/test/foundry/fee-token-pricers/uniswap-v2-twap/FullMath.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Modified from https://github.com/Uniswap/solidity-lib +// DO NOT USE IN PRODUCTION + +library FullMath { + function fullMul(uint256 x, uint256 y) internal pure returns (uint256 l, uint256 h) { + unchecked { + uint256 mm = mulmod(x, y, type(uint256).max); + l = x * y; + h = mm - l; + if (mm < l) h -= 1; + } + } + + function fullDiv(uint256 l, uint256 h, uint256 d) private pure returns (uint256) { + unchecked { + uint256 pow2 = d & (0 - d); + d /= pow2; + l /= pow2; + l += h * ((0 - pow2) / pow2 + 1); + uint256 r = 1; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + r *= 2 - d * r; + return l * r; + } + } + + function mulDiv(uint256 x, uint256 y, uint256 d) internal pure returns (uint256) { + unchecked { + (uint256 l, uint256 h) = fullMul(x, y); + + uint256 mm = mulmod(x, y, d); + if (mm > l) h -= 1; + l -= mm; + + if (h == 0) return l / d; + + require(h < d, "FullMath: FULLDIV_OVERFLOW"); + return fullDiv(l, h, d); + } + } +} diff --git a/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol new file mode 100644 index 00000000..e8a7acc9 --- /dev/null +++ b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IFeeTokenPricer} from "../../../../src/bridge/ISequencerInbox.sol"; +import {FixedPoint} from "./FixedPoint.sol"; +import {IUniswapV2Pair} from "@uniswap/v2-core/interfaces/IUniswapV2Pair.sol"; + +/// @title A uniswap twap pricer +/// @notice An example of a type 2 fee token pricer. It uses an oracle to get the fee token price at +/// at the time the batch is posted +contract UniswapV2TwapPricer is IFeeTokenPricer { + using FixedPoint for *; + + uint256 public constant TWAP_WINDOW = 1 hours; + + IUniswapV2Pair immutable pair; + address public immutable weth; + address public immutable token; + + uint256 public price0CumulativeLast; + uint256 public price1CumulativeLast; + uint32 public pricerUpdatedAt; + + FixedPoint.uq112x112 public price0Average; + FixedPoint.uq112x112 public price1Average; + + constructor(IUniswapV2Pair _pair, address _weth) { + pair = _pair; + address token0 = _pair.token0(); + address token1 = _pair.token1(); + + require(token0 == _weth || token1 == _weth, "WETH not in pair"); + + weth = _weth; + token = token0 == _weth ? token1 : token0; + + price0CumulativeLast = _pair.price0CumulativeLast(); + price1CumulativeLast = _pair.price1CumulativeLast(); + uint112 reserve0; + uint112 reserve1; + (reserve0, reserve1, pricerUpdatedAt) = _pair.getReserves(); + require(reserve0 != 0 && reserve1 != 0, "No reserves"); // ensure that there's liquidity in the pair + } + + // @inheritdoc IFeeTokenPricer + function getExchangeRate() external returns (uint256) { + uint32 currentBlockTimestamp = uint32(block.timestamp); + uint32 timeElapsed = currentBlockTimestamp - pricerUpdatedAt; + + if (timeElapsed >= TWAP_WINDOW) { + _update(timeElapsed); + } + + if (weth == pair.token0()) { + return FixedPoint.mul(price0Average, uint256(1)).decode144(); + } else { + return FixedPoint.mul(price1Average, uint256(1)).decode144(); + } + } + + function updatePrice() external { + uint32 currentBlockTimestamp = uint32(block.timestamp); + uint32 timeElapsed = currentBlockTimestamp - pricerUpdatedAt; + require(timeElapsed >= TWAP_WINDOW, "Minimum TWAP window not elapsed"); + + _update(timeElapsed); + } + + function _update( + uint256 timeElapsed + ) internal { + uint32 currentBlockTimestamp = uint32(block.timestamp); + + // fetch latest cumulative price accumulators + IUniswapV2Pair _pair = pair; + uint256 price0Cumulative = _pair.price0CumulativeLast(); + uint256 price1Cumulative = _pair.price1CumulativeLast(); + + // add the current price if prices haven't been updated in this block + (uint112 reserve0, uint112 reserve1, uint32 pairUpdatedAt) = + IUniswapV2Pair(pair).getReserves(); + if (pairUpdatedAt != currentBlockTimestamp) { + uint256 delta = currentBlockTimestamp - pairUpdatedAt; + unchecked { + price0Cumulative += uint256(FixedPoint.fraction(reserve1, reserve0)._x) * delta; + price1Cumulative += uint256(FixedPoint.fraction(reserve0, reserve1)._x) * delta; + } + } + + // overflow is desired, casting never truncates + // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed + unchecked { + price0Average = FixedPoint.uq112x112( + uint224((price0Cumulative - price0CumulativeLast) / timeElapsed) + ); + price1Average = FixedPoint.uq112x112( + uint224((price1Cumulative - price1CumulativeLast) / timeElapsed) + ); + } + + price0CumulativeLast = price0Cumulative; + price1CumulativeLast = price1Cumulative; + pricerUpdatedAt = currentBlockTimestamp; + } +} diff --git a/yarn.lock b/yarn.lock index e1c492b7..3c2c4294 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1417,6 +1417,16 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@uniswap/lib@^4.0.1-alpha": + version "4.0.1-alpha" + resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-4.0.1-alpha.tgz#2881008e55f075344675b3bca93f020b028fbd02" + integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA== + +"@uniswap/v2-core@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" + integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"