From 960d7da9958ef8e869f1544615fc184fef70f733 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Mon, 30 Sep 2024 09:29:52 +0200 Subject: [PATCH 01/36] Add ConstantExchangeRatePricer --- .../ConstantExchangeRatePricer.sol | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol diff --git a/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol b/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol new file mode 100644 index 000000000..2efe50abb --- /dev/null +++ b/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IFeeTokenPricer} from "../../../src/bridge/ISequencerInbox.sol"; + +/** + * @title Test implementation of a fee token pricer that returns a constant exchange rate + * @notice Exchange rate is set in constructor and cannot be changed + */ +contract ConstantExchangeRatePricer is IFeeTokenPricer { + uint256 immutable exchangeRate; + + constructor(uint256 _exchangeRate) { + exchangeRate = _exchangeRate; + } + + // @inheritdoc IFeeTokenPricer + function getExchangeRate() external view returns (uint256) { + return exchangeRate; + } +} From e3cf8eda7ce78ac4b587afd36024f70ebb8b6074 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Mon, 30 Sep 2024 09:32:54 +0200 Subject: [PATCH 02/36] Add OwnerAdjustableExchangeRatePricer --- .../OwnerAdjustableExchangeRatePricer.sol | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol diff --git a/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol b/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol new file mode 100644 index 000000000..8a39b6bb2 --- /dev/null +++ b/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol @@ -0,0 +1,30 @@ +// 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 Test implementation of a fee token pricer that returns an exchange rate set by the owner + * @notice Exchange rate can be changed by the owner at any time, without any restrictions + */ +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; + } +} From d0d31d3215087763d5c42f1b9d745b18a5d26da0 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Mon, 30 Sep 2024 10:12:26 +0200 Subject: [PATCH 03/36] Add uniswap v2 dependency --- package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 5b2c0a5d9..810908572 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@offchainlabs/upgrade-executor": "1.1.0-beta.0", "@openzeppelin/contracts": "4.5.0", "@openzeppelin/contracts-upgradeable": "4.5.2", + "@uniswap/v2-core": "^1.0.1", "patch-package": "^6.4.7", "solady": "0.0.182" }, diff --git a/yarn.lock b/yarn.lock index 06a9ee890..cee6991a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1420,6 +1420,11 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@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" From fcd2938ce7e8e0c55a77e1ad91f551f6a1cc7b01 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Mon, 30 Sep 2024 10:45:52 +0200 Subject: [PATCH 04/36] Add uniswap lib --- foundry.toml | 16 ++++++++++------ package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/foundry.toml b/foundry.toml index 8c30bcf4e..9dad34408 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,21 +3,25 @@ src = 'src/' out = 'out' libs = ['node_modules', 'lib'] test = 'test/foundry' -cache_path = 'forge-cache/sol' +cache_path = 'forge-cache/sol' optimizer = true optimizer_runs = 100 via_ir = false solc_version = '0.8.9' -remappings = ['ds-test/=lib/forge-std/lib/ds-test/src/', - 'forge-std/=lib/forge-std/src/', - '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/', - '@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/'] +remappings = [ + 'ds-test/=lib/forge-std/lib/ds-test/src/', + 'forge-std/=lib/forge-std/src/', + '@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/', + '@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/', + '@uniswap/v2-core/=node_modules/@uniswap/v2-core/contracts', + '@uniswap/lib/=node_modules/@uniswap/lib/contracts', +] [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 810908572..514ffd647 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@offchainlabs/upgrade-executor": "1.1.0-beta.0", "@openzeppelin/contracts": "4.5.0", "@openzeppelin/contracts-upgradeable": "4.5.2", + "@uniswap/lib": "^4.0.1-alpha", "@uniswap/v2-core": "^1.0.1", "patch-package": "^6.4.7", "solady": "0.0.182" diff --git a/yarn.lock b/yarn.lock index cee6991a4..cbfea5200 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1420,6 +1420,11 @@ 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" From 0513e38875cf912d84fdb663cf66bbc4236e38c9 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Tue, 1 Oct 2024 16:29:28 +0200 Subject: [PATCH 05/36] Uniswap v2 twap draft --- .../fee-token-pricers/UniswapV2TwapPricer.sol | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 test/foundry/fee-token-pricers/UniswapV2TwapPricer.sol diff --git a/test/foundry/fee-token-pricers/UniswapV2TwapPricer.sol b/test/foundry/fee-token-pricers/UniswapV2TwapPricer.sol new file mode 100644 index 000000000..69bd3c85d --- /dev/null +++ b/test/foundry/fee-token-pricers/UniswapV2TwapPricer.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IFeeTokenPricer} from "../../../src/bridge/ISequencerInbox.sol"; +import "@uniswap/v2-core/interfaces/IUniswapV2Factory.sol"; +import "@uniswap/v2-core/interfaces/IUniswapV2Pair.sol"; +import "@uniswap/lib/libraries/FixedPoint.sol"; + +/** + * @title Test implementation of a fee token pricer that returns a constant exchange rate + * @notice Exchange rate is set in constructor and cannot be changed + */ +contract UniswapV2TwapPricer is IFeeTokenPricer { + uint256 public constant TWAP_WINDOW = 1 hours; + + IUniswapV2Pair immutable pair; + address public immutable token0; + address public immutable token1; + + uint256 public price0CumulativeLast; + uint256 public price1CumulativeLast; + uint32 public pricerUpdatedAt; + + FixedPoint.uq112x112 public price0Average; + FixedPoint.uq112x112 public price1Average; + + constructor(IUniswapV2Pair _pair) { + pair = _pair; + token0 = _pair.token0(); + token1 = _pair.token1(); + 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 view returns (uint256) { + uint32 currentBlockTimestamp = uint32(block.timestamp); + uint32 timeElapsed = currentBlockTimestamp - pricerUpdatedAt; + + if (timeElapsed >= TWAP_WINDOW) { + _update(); + } + + // calculate exchange rate + + return 0; + } + + function update() external { + uint32 currentBlockTimestamp = uint32(block.timestamp); + uint32 timeElapsed = currentBlockTimestamp - pricerUpdatedAt; + require(timeElapsed >= TWAP_WINDOW, "Minimum TWAP window not elapsed"); + + _update(); + } + + function _update() internal { + uint32 currentBlockTimestamp = uint32(block.timestamp); + uint32 timeElapsed = currentBlockTimestamp - pricerUpdatedAt; + + // 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 + price0Average = + FixedPoint.uq112x112(uint224((price0Cumulative - price0CumulativeLast) / timeElapsed)); + price1Average = + FixedPoint.uq112x112(uint224((price1Cumulative - price1CumulativeLast) / timeElapsed)); + + price0CumulativeLast = price0Cumulative; + price1CumulativeLast = price1Cumulative; + pricerUpdatedAt = currentBlockTimestamp; + } +} From cf9a96204bc9907ceb7e2b61f2eed971cc77b611 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 2 Oct 2024 08:27:27 +0200 Subject: [PATCH 06/36] Use local math files --- package.json | 1 - .../uniswap-v2-twap/FixedPoint.sol | 61 +++++++++++++++++++ .../uniswap-v2-twap/FullMath.sol | 50 +++++++++++++++ .../UniswapV2TwapPricer.sol | 9 ++- yarn.lock | 5 -- 5 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 test/foundry/fee-token-pricers/uniswap-v2-twap/FixedPoint.sol create mode 100644 test/foundry/fee-token-pricers/uniswap-v2-twap/FullMath.sol rename test/foundry/fee-token-pricers/{ => uniswap-v2-twap}/UniswapV2TwapPricer.sol (90%) diff --git a/package.json b/package.json index 514ffd647..810908572 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "@offchainlabs/upgrade-executor": "1.1.0-beta.0", "@openzeppelin/contracts": "4.5.0", "@openzeppelin/contracts-upgradeable": "4.5.2", - "@uniswap/lib": "^4.0.1-alpha", "@uniswap/v2-core": "^1.0.1", "patch-package": "^6.4.7", "solady": "0.0.182" 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 000000000..807739e97 --- /dev/null +++ b/test/foundry/fee-token-pricers/uniswap-v2-twap/FixedPoint.sol @@ -0,0 +1,61 @@ +// 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 000000000..fe60ab4ae --- /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/UniswapV2TwapPricer.sol b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol similarity index 90% rename from test/foundry/fee-token-pricers/UniswapV2TwapPricer.sol rename to test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol index 69bd3c85d..9f7c94d1c 100644 --- a/test/foundry/fee-token-pricers/UniswapV2TwapPricer.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import {IFeeTokenPricer} from "../../../src/bridge/ISequencerInbox.sol"; +import {IFeeTokenPricer} from "../../../../src/bridge/ISequencerInbox.sol"; import "@uniswap/v2-core/interfaces/IUniswapV2Factory.sol"; import "@uniswap/v2-core/interfaces/IUniswapV2Pair.sol"; -import "@uniswap/lib/libraries/FixedPoint.sol"; +import {FixedPoint} from "./FixedPoint.sol"; /** - * @title Test implementation of a fee token pricer that returns a constant exchange rate - * @notice Exchange rate is set in constructor and cannot be changed + * @title Test implementation of a fee token pricer that uses Uniswap V2 TWAP */ contract UniswapV2TwapPricer is IFeeTokenPricer { uint256 public constant TWAP_WINDOW = 1 hours; @@ -37,7 +36,7 @@ contract UniswapV2TwapPricer is IFeeTokenPricer { } // @inheritdoc IFeeTokenPricer - function getExchangeRate() external view returns (uint256) { + function getExchangeRate() external returns (uint256) { uint32 currentBlockTimestamp = uint32(block.timestamp); uint32 timeElapsed = currentBlockTimestamp - pricerUpdatedAt; diff --git a/yarn.lock b/yarn.lock index cbfea5200..cee6991a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1420,11 +1420,6 @@ 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" From 0cb90fdfc66a6b8a740f7cc0ca110a6ddb4fef0e Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 2 Oct 2024 08:49:34 +0200 Subject: [PATCH 07/36] Return exchange rate --- .../uniswap-v2-twap/UniswapV2TwapPricer.sol | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol index 9f7c94d1c..0c1385604 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol @@ -2,19 +2,20 @@ pragma solidity ^0.8.0; import {IFeeTokenPricer} from "../../../../src/bridge/ISequencerInbox.sol"; -import "@uniswap/v2-core/interfaces/IUniswapV2Factory.sol"; -import "@uniswap/v2-core/interfaces/IUniswapV2Pair.sol"; import {FixedPoint} from "./FixedPoint.sol"; +import {IUniswapV2Pair} from "@uniswap/v2-core/interfaces/IUniswapV2Pair.sol"; /** * @title Test implementation of a fee token pricer that uses Uniswap V2 TWAP */ contract UniswapV2TwapPricer is IFeeTokenPricer { + using FixedPoint for *; + uint256 public constant TWAP_WINDOW = 1 hours; IUniswapV2Pair immutable pair; - address public immutable token0; - address public immutable token1; + address public immutable weth; + address public immutable token; uint256 public price0CumulativeLast; uint256 public price1CumulativeLast; @@ -23,10 +24,16 @@ contract UniswapV2TwapPricer is IFeeTokenPricer { FixedPoint.uq112x112 public price0Average; FixedPoint.uq112x112 public price1Average; - constructor(IUniswapV2Pair _pair) { + constructor(IUniswapV2Pair _pair, address _weth) { pair = _pair; - token0 = _pair.token0(); - token1 = _pair.token1(); + 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; @@ -41,25 +48,26 @@ contract UniswapV2TwapPricer is IFeeTokenPricer { uint32 timeElapsed = currentBlockTimestamp - pricerUpdatedAt; if (timeElapsed >= TWAP_WINDOW) { - _update(); + _update(timeElapsed); } - // calculate exchange rate - - return 0; + if (weth == pair.token0()) { + return FixedPoint.mul(price0Average, uint256(1)).decode144(); + } else { + return FixedPoint.mul(price1Average, uint256(1)).decode144(); + } } - function update() external { + function updatePrice() external { uint32 currentBlockTimestamp = uint32(block.timestamp); uint32 timeElapsed = currentBlockTimestamp - pricerUpdatedAt; require(timeElapsed >= TWAP_WINDOW, "Minimum TWAP window not elapsed"); - _update(); + _update(timeElapsed); } - function _update() internal { + function _update(uint256 timeElapsed) internal { uint32 currentBlockTimestamp = uint32(block.timestamp); - uint32 timeElapsed = currentBlockTimestamp - pricerUpdatedAt; // fetch latest cumulative price accumulators IUniswapV2Pair _pair = pair; @@ -79,10 +87,14 @@ contract UniswapV2TwapPricer is IFeeTokenPricer { // overflow is desired, casting never truncates // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed - price0Average = - FixedPoint.uq112x112(uint224((price0Cumulative - price0CumulativeLast) / timeElapsed)); - price1Average = - FixedPoint.uq112x112(uint224((price1Cumulative - price1CumulativeLast) / timeElapsed)); + unchecked { + price0Average = FixedPoint.uq112x112( + uint224((price0Cumulative - price0CumulativeLast) / timeElapsed) + ); + price1Average = FixedPoint.uq112x112( + uint224((price1Cumulative - price1CumulativeLast) / timeElapsed) + ); + } price0CumulativeLast = price0Cumulative; price1CumulativeLast = price1Cumulative; From 640a497ca6e8899d2f6293ac5baf01536bd5d382 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 2 Oct 2024 09:48:19 +0200 Subject: [PATCH 08/36] AMM trade tracker draft --- .../AmmTradeTracker.sol | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol new file mode 100644 index 000000000..80c550a61 --- /dev/null +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IFeeTokenPricer} from "../../../../src/bridge/ISequencerInbox.sol"; + +/** + * @title Test implementation of a fee token pricer that trades on AMM and keeps track of trades + */ +contract AmmTradeTracker is IFeeTokenPricer { + IUniswapV2Router01 public immutable router; + address public immutable token; + address public immutable weth; + + uint256 public totalEthReceived; + uint256 public totalTokenSpent; + + constructor(IUniswapV2Router01 _router, address _token) { + router = _router; + token = _token; + weth = _router.WETH(); + } + + // @inheritdoc IFeeTokenPricer + function getExchangeRate() external returns (uint256) {} + + function _swap(uint256 tokenAmount) internal { + IERC20(token).approve(address(router), tokenAmount); + + address[] memory path = new address[](2); + path[0] = token; + path[1] = weth; + + uint256[] memory amounts = router.swapExactTokensForETH({ + amountIn: tokenAmount, + amountOutMin: 1, + path: path, + to: msg.sender, + deadline: block.timestamp + }); + uint256 ethReceived = amounts[amounts.length - 1]; + + totalEthReceived += ethReceived; + totalTokenSpent += tokenAmount; + } +} + +interface IUniswapV2Router01 { + function WETH() external pure returns (address); + function swapExactTokensForETH( + uint256 amountIn, + uint256 amountOutMin, + address[] calldata path, + address to, + uint256 deadline + ) external returns (uint256[] memory amounts); +} + +interface IERC20 { + function approve(address spender, uint256 value) external returns (bool); +} From 93df12666e50199811f40f0673da9641c5b2e57f Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 2 Oct 2024 09:53:48 +0200 Subject: [PATCH 09/36] Return exchange rate --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 80c550a61..6ba050bf9 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -2,11 +2,12 @@ pragma solidity ^0.8.0; import {IFeeTokenPricer} from "../../../../src/bridge/ISequencerInbox.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /** * @title Test implementation of a fee token pricer that trades on AMM and keeps track of trades */ -contract AmmTradeTracker is IFeeTokenPricer { +contract AmmTradeTracker is IFeeTokenPricer, Ownable { IUniswapV2Router01 public immutable router; address public immutable token; address public immutable weth; @@ -14,16 +15,18 @@ contract AmmTradeTracker is IFeeTokenPricer { uint256 public totalEthReceived; uint256 public totalTokenSpent; - constructor(IUniswapV2Router01 _router, address _token) { + constructor(IUniswapV2Router01 _router, address _token) Ownable() { router = _router; token = _token; weth = _router.WETH(); } // @inheritdoc IFeeTokenPricer - function getExchangeRate() external returns (uint256) {} + function getExchangeRate() external view returns (uint256) { + return totalTokenSpent * 1e18 / totalEthReceived; + } - function _swap(uint256 tokenAmount) internal { + function swapTokenToEth(uint256 tokenAmount) external onlyOwner { IERC20(token).approve(address(router), tokenAmount); address[] memory path = new address[](2); From d29724e798222094bc8979439b9261ce0da4883e Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 2 Oct 2024 11:11:58 +0200 Subject: [PATCH 10/36] Add comments --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 6ba050bf9..7df0e6b7a 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -19,23 +19,26 @@ contract AmmTradeTracker is IFeeTokenPricer, Ownable { router = _router; token = _token; weth = _router.WETH(); + + IERC20(token).approve(address(router), type(uint256).max); } // @inheritdoc IFeeTokenPricer function getExchangeRate() external view returns (uint256) { + // todo - scale for decimals to get 1e18 denominator return totalTokenSpent * 1e18 / totalEthReceived; } function swapTokenToEth(uint256 tokenAmount) external onlyOwner { - IERC20(token).approve(address(router), tokenAmount); - address[] memory path = new address[](2); path[0] = token; path[1] = weth; + // todo - properly calculate slippage + uint256 amountOutMin = 1; uint256[] memory amounts = router.swapExactTokensForETH({ amountIn: tokenAmount, - amountOutMin: 1, + amountOutMin: amountOutMin, path: path, to: msg.sender, deadline: block.timestamp From 3ce93dd0bb0a22a49a4987aadb884d4d41d84973 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 2 Oct 2024 11:24:18 +0200 Subject: [PATCH 11/36] Fetch tokens --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 7df0e6b7a..79718fb27 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -30,6 +30,8 @@ contract AmmTradeTracker is IFeeTokenPricer, Ownable { } function swapTokenToEth(uint256 tokenAmount) external onlyOwner { + IERC20(token).transferFrom(msg.sender, address(this), tokenAmount); + address[] memory path = new address[](2); path[0] = token; path[1] = weth; @@ -63,4 +65,5 @@ interface IUniswapV2Router01 { interface IERC20 { function approve(address spender, uint256 value) external returns (bool); + function transferFrom(address from, address to, uint256 value) external returns (bool); } From e0c7b7e2985d07b23132b040d90934c28e43f564 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 2 Oct 2024 15:58:44 +0200 Subject: [PATCH 12/36] Add gas reporting hook --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 79718fb27..97499adf1 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -3,11 +3,12 @@ pragma solidity ^0.8.0; import {IFeeTokenPricer} from "../../../../src/bridge/ISequencerInbox.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {IGasRefunder} from "../../../../src/libraries/IGasRefunder.sol"; /** * @title Test implementation of a fee token pricer that trades on AMM and keeps track of trades */ -contract AmmTradeTracker is IFeeTokenPricer, Ownable { +contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { IUniswapV2Router01 public immutable router; address public immutable token; address public immutable weth; @@ -50,6 +51,13 @@ contract AmmTradeTracker is IFeeTokenPricer, Ownable { totalEthReceived += ethReceived; totalTokenSpent += tokenAmount; } + + function onGasSpent(address payable spender, uint256 gasUsed, uint256 calldataSize) + external + returns (bool success) + { + // update internal state + } } interface IUniswapV2Router01 { From 112c94782740315dd47656144b5a920cec97125d Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Thu, 3 Oct 2024 12:39:23 +0200 Subject: [PATCH 13/36] Update internal state --- .../AmmTradeTracker.sol | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 97499adf1..390ddc654 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -13,8 +13,8 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { address public immutable token; address public immutable weth; - uint256 public totalEthReceived; - uint256 public totalTokenSpent; + uint256 public ethAccumulator; + uint256 public tokenAccumulator; constructor(IUniswapV2Router01 _router, address _token) Ownable() { router = _router; @@ -27,7 +27,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { // @inheritdoc IFeeTokenPricer function getExchangeRate() external view returns (uint256) { // todo - scale for decimals to get 1e18 denominator - return totalTokenSpent * 1e18 / totalEthReceived; + return tokenAccumulator * 1e18 / ethAccumulator; } function swapTokenToEth(uint256 tokenAmount) external onlyOwner { @@ -48,8 +48,8 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { }); uint256 ethReceived = amounts[amounts.length - 1]; - totalEthReceived += ethReceived; - totalTokenSpent += tokenAmount; + ethAccumulator += ethReceived; + tokenAccumulator += tokenAmount; } function onGasSpent(address payable spender, uint256 gasUsed, uint256 calldataSize) @@ -57,6 +57,16 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { returns (bool success) { // update internal state + uint256 exchangeRateUsed = tokenAccumulator * 1e18 / ethAccumulator; + + gasUsed += calldataSize * 16; + uint256 ethDelta = gasUsed * block.basefee; + uint256 tokenDelta = ethDelta * exchangeRateUsed / 1e18; + + ethAccumulator -= ethDelta; + tokenAccumulator -= tokenDelta; + + success = true; } } From 35aa8acf963b6df705df52310ed0cab53d530007 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Fri, 4 Oct 2024 11:12:04 +0200 Subject: [PATCH 14/36] Track exchange rate per spender --- .../AmmTradeTracker.sol | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 390ddc654..24c641d6c 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -13,8 +13,8 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { address public immutable token; address public immutable weth; - uint256 public ethAccumulator; - uint256 public tokenAccumulator; + mapping(address => uint256) ethAccumulatorPerSpender; + mapping(address => uint256) tokenAccumulatorPerSpender; constructor(IUniswapV2Router01 _router, address _token) Ownable() { router = _router; @@ -26,8 +26,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { // @inheritdoc IFeeTokenPricer function getExchangeRate() external view returns (uint256) { - // todo - scale for decimals to get 1e18 denominator - return tokenAccumulator * 1e18 / ethAccumulator; + return _getExchangeRate(); } function swapTokenToEth(uint256 tokenAmount) external onlyOwner { @@ -48,8 +47,8 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { }); uint256 ethReceived = amounts[amounts.length - 1]; - ethAccumulator += ethReceived; - tokenAccumulator += tokenAmount; + ethAccumulatorPerSpender[msg.sender] += ethReceived; + tokenAccumulatorPerSpender[msg.sender] += tokenAmount; } function onGasSpent(address payable spender, uint256 gasUsed, uint256 calldataSize) @@ -57,17 +56,28 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { returns (bool success) { // update internal state - uint256 exchangeRateUsed = tokenAccumulator * 1e18 / ethAccumulator; - - gasUsed += calldataSize * 16; - uint256 ethDelta = gasUsed * block.basefee; - uint256 tokenDelta = ethDelta * exchangeRateUsed / 1e18; + uint256 exchangeRateUsed = _getExchangeRate(); + if (exchangeRateUsed != 0) { + gasUsed += calldataSize * 16; + uint256 ethDelta = gasUsed * block.basefee; + uint256 tokenDelta = ethDelta * exchangeRateUsed / 1e18; - ethAccumulator -= ethDelta; - tokenAccumulator -= tokenDelta; + ethAccumulatorPerSpender[spender] -= ethDelta; + tokenAccumulatorPerSpender[spender] -= tokenDelta; + } success = true; } + + function _getExchangeRate() internal view returns (uint256) { + uint256 ethAcc = ethAccumulatorPerSpender[tx.origin]; + if (ethAcc == 0) { + return 0; + } + uint256 tokenAcc = tokenAccumulatorPerSpender[tx.origin]; + // todo - scale for decimals to get 1e18 denominator + return tokenAcc * 1e18 / tokenAcc; + } } interface IUniswapV2Router01 { From a90cc1452c08674acfa53090e75a9aadf32db892 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Fri, 4 Oct 2024 11:15:26 +0200 Subject: [PATCH 15/36] No need for ownable --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 24c641d6c..d5312b825 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -2,13 +2,12 @@ pragma solidity ^0.8.0; import {IFeeTokenPricer} from "../../../../src/bridge/ISequencerInbox.sol"; -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IGasRefunder} from "../../../../src/libraries/IGasRefunder.sol"; /** * @title Test implementation of a fee token pricer that trades on AMM and keeps track of trades */ -contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { +contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder { IUniswapV2Router01 public immutable router; address public immutable token; address public immutable weth; @@ -16,7 +15,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { mapping(address => uint256) ethAccumulatorPerSpender; mapping(address => uint256) tokenAccumulatorPerSpender; - constructor(IUniswapV2Router01 _router, address _token) Ownable() { + constructor(IUniswapV2Router01 _router, address _token) { router = _router; token = _token; weth = _router.WETH(); @@ -29,7 +28,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { return _getExchangeRate(); } - function swapTokenToEth(uint256 tokenAmount) external onlyOwner { + function swapTokenToEth(uint256 tokenAmount) external { IERC20(token).transferFrom(msg.sender, address(this), tokenAmount); address[] memory path = new address[](2); From 85799f70d230e7f55912404983ecb22e0458d71b Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Fri, 4 Oct 2024 11:18:06 +0200 Subject: [PATCH 16/36] Avoid underflow --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index d5312b825..b1a9f53a6 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -61,8 +61,19 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder { uint256 ethDelta = gasUsed * block.basefee; uint256 tokenDelta = ethDelta * exchangeRateUsed / 1e18; - ethAccumulatorPerSpender[spender] -= ethDelta; - tokenAccumulatorPerSpender[spender] -= tokenDelta; + uint256 ethAcc = ethAccumulatorPerSpender[spender]; + if (ethDelta > ethAcc) { + ethAccumulatorPerSpender[spender] = 0; + } else { + ethAccumulatorPerSpender[spender] -= ethDelta; + } + + uint256 tokenAcc = tokenAccumulatorPerSpender[spender]; + if (tokenDelta > tokenAcc) { + tokenAccumulatorPerSpender[spender] = 0; + } else { + tokenAccumulatorPerSpender[spender] -= tokenDelta; + } } success = true; From 6ecce41dfbdc268544f700e7a83c70c2d4f019e8 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Mon, 7 Oct 2024 13:55:04 +0200 Subject: [PATCH 17/36] Owner sets deafult exchange rate --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index b1a9f53a6..47efece54 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -3,11 +3,12 @@ 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"; /** * @title Test implementation of a fee token pricer that trades on AMM and keeps track of trades */ -contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder { +contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { IUniswapV2Router01 public immutable router; address public immutable token; address public immutable weth; @@ -15,10 +16,15 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder { mapping(address => uint256) ethAccumulatorPerSpender; mapping(address => uint256) tokenAccumulatorPerSpender; - constructor(IUniswapV2Router01 _router, address _token) { + uint256 public defaultExchangeRate; + + constructor(IUniswapV2Router01 _router, address _token, uint256 _defaultExchangeRate) + Ownable() + { router = _router; token = _token; weth = _router.WETH(); + defaultExchangeRate = _defaultExchangeRate; IERC20(token).approve(address(router), type(uint256).max); } @@ -79,10 +85,14 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder { success = true; } + function setDefaultExchangeRate(uint256 _defaultExchangeRate) external onlyOwner { + defaultExchangeRate = _defaultExchangeRate; + } + function _getExchangeRate() internal view returns (uint256) { uint256 ethAcc = ethAccumulatorPerSpender[tx.origin]; if (ethAcc == 0) { - return 0; + return defaultExchangeRate; } uint256 tokenAcc = tokenAccumulatorPerSpender[tx.origin]; // todo - scale for decimals to get 1e18 denominator From fc92cd2739226c4b42aee00ffe87223f3ae48b4d Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Mon, 7 Oct 2024 14:23:00 +0200 Subject: [PATCH 18/36] Prvoide min amount of eth received to protect from slippage --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 47efece54..4df8eb2d4 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -34,18 +34,16 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { return _getExchangeRate(); } - function swapTokenToEth(uint256 tokenAmount) external { + function swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) external { IERC20(token).transferFrom(msg.sender, address(this), tokenAmount); address[] memory path = new address[](2); path[0] = token; path[1] = weth; - // todo - properly calculate slippage - uint256 amountOutMin = 1; uint256[] memory amounts = router.swapExactTokensForETH({ amountIn: tokenAmount, - amountOutMin: amountOutMin, + amountOutMin: minEthReceived, path: path, to: msg.sender, deadline: block.timestamp From a026be390d5a922ca8bca5057bf4807920c54efe Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Mon, 7 Oct 2024 14:51:51 +0200 Subject: [PATCH 19/36] Scale to 18 decimals --- .../AmmTradeTracker.sol | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 4df8eb2d4..94119ed21 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -12,6 +12,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { IUniswapV2Router01 public immutable router; address public immutable token; address public immutable weth; + uint8 public immutable tokenDecimals; mapping(address => uint256) ethAccumulatorPerSpender; mapping(address => uint256) tokenAccumulatorPerSpender; @@ -24,7 +25,9 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { router = _router; token = _token; weth = _router.WETH(); + defaultExchangeRate = _defaultExchangeRate; + tokenDecimals = IERC20(_token).decimals(); IERC20(token).approve(address(router), type(uint256).max); } @@ -93,8 +96,18 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { return defaultExchangeRate; } uint256 tokenAcc = tokenAccumulatorPerSpender[tx.origin]; - // todo - scale for decimals to get 1e18 denominator - return tokenAcc * 1e18 / tokenAcc; + + return _scaleTo18Decimals(tokenAcc) * 1e18 / ethAcc; + } + + function _scaleTo18Decimals(uint256 amount) internal view returns (uint256) { + if (tokenDecimals == 18) { + return amount; + } else if (tokenDecimals < 18) { + return amount * 10 ** (18 - tokenDecimals); + } else { + return amount * 10 ** (18 - tokenDecimals); + } } } @@ -112,4 +125,5 @@ interface IUniswapV2Router01 { interface IERC20 { function approve(address spender, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); + function decimals() external view returns (uint8); } From db28e56acccafe7448ebb218405d7852311edb1d Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Tue, 8 Oct 2024 14:38:47 +0200 Subject: [PATCH 20/36] Use safe erc20 --- .../AmmTradeTracker.sol | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 94119ed21..ff4128db5 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -4,18 +4,23 @@ 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"; /** - * @title Test implementation of a fee token pricer that trades on AMM and keeps track of trades + * @title Test implementation of a fee token pricer that trades on AMM and keeps track of traded amount to calculate exchange rate */ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { + using SafeERC20 for IERC20; + IUniswapV2Router01 public immutable router; address public immutable token; address public immutable weth; uint8 public immutable tokenDecimals; - mapping(address => uint256) ethAccumulatorPerSpender; - mapping(address => uint256) tokenAccumulatorPerSpender; + mapping(address => uint256) public ethAccumulatorPerSpender; + mapping(address => uint256) public tokenAccumulatorPerSpender; uint256 public defaultExchangeRate; @@ -27,9 +32,9 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { weth = _router.WETH(); defaultExchangeRate = _defaultExchangeRate; - tokenDecimals = IERC20(_token).decimals(); + tokenDecimals = ERC20(_token).decimals(); - IERC20(token).approve(address(router), type(uint256).max); + IERC20(token).safeApprove(address(router), type(uint256).max); } // @inheritdoc IFeeTokenPricer @@ -38,7 +43,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { } function swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) external { - IERC20(token).transferFrom(msg.sender, address(this), tokenAmount); + IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); address[] memory path = new address[](2); path[0] = token; @@ -121,9 +126,3 @@ interface IUniswapV2Router01 { uint256 deadline ) external returns (uint256[] memory amounts); } - -interface IERC20 { - function approve(address spender, uint256 value) external returns (bool); - function transferFrom(address from, address to, uint256 value) external returns (bool); - function decimals() external view returns (uint8); -} From 84090aa3fe56dddc191ab5ac97bb529c23dd4653 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Tue, 8 Oct 2024 15:29:38 +0200 Subject: [PATCH 21/36] Test swapTokenToEth --- .../AmmTradeTracker.sol | 7 +++- .../AmmTradeTracker.t.sol | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index ff4128db5..5ae2e1f5f 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -42,7 +42,10 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { return _getExchangeRate(); } - function swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) external { + function swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) + external + returns (uint256) + { IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); address[] memory path = new address[](2); @@ -60,6 +63,8 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { ethAccumulatorPerSpender[msg.sender] += ethReceived; tokenAccumulatorPerSpender[msg.sender] += tokenAmount; + + return ethReceived; } function onGasSpent(address payable spender, uint256 gasUsed, uint256 calldataSize) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol new file mode 100644 index 000000000..a12f90c7c --- /dev/null +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import {AmmTradeTracker, IUniswapV2Router01, IERC20} from "./AmmTradeTracker.sol"; + +contract AmmTradeTrackerTest is Test { + AmmTradeTracker public tradeTracker; + address public owner = makeAddr("tradeTrackerOwner"); + address public batchPosterOperator = makeAddr("batchPosterOperator"); + + address public constant V2_ROUTER_ARB1 = address(0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24); + address public constant USDC_ARB1 = address(0xaf88d065e77c8cC2239327C5EDb3A432268e5831); + uint256 public constant DEFAULT_EXCHANGE_RATE = 2500e18; + + function setUp() public { + vm.createSelectFork(vm.envString("ARB"), 261_666_155); + console.log(vm.envString("ARB")); + + vm.prank(owner); + tradeTracker = new AmmTradeTracker( + IUniswapV2Router01(V2_ROUTER_ARB1), USDC_ARB1, DEFAULT_EXCHANGE_RATE + ); + } + + function testFork_CanSwapTokenToEth() public { + uint256 usdcAmount = 250e6; + uint256 minEthReceived = 0.1 ether; + + deal(USDC_ARB1, batchPosterOperator, usdcAmount); + + vm.startPrank(batchPosterOperator); + IERC20(USDC_ARB1).approve(address(tradeTracker), usdcAmount); + uint256 ethReceived = tradeTracker.swapTokenToEth(usdcAmount, minEthReceived); + vm.stopPrank(); + + assertEq(tradeTracker.ethAccumulatorPerSpender(batchPosterOperator), ethReceived); + assertEq(tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator), usdcAmount); + } +} From c00325140127e71a110173594110aeb8579e0c4b Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Tue, 8 Oct 2024 15:56:08 +0200 Subject: [PATCH 22/36] Test getExchangeRate --- .../AmmTradeTracker.sol | 8 ++--- .../AmmTradeTracker.t.sol | 34 +++++++++++++++---- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 5ae2e1f5f..a92384aa4 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -44,7 +44,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { function swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) external - returns (uint256) + returns (uint256 ethReceived) { IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); @@ -59,12 +59,10 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { to: msg.sender, deadline: block.timestamp }); - uint256 ethReceived = amounts[amounts.length - 1]; + ethReceived = amounts[amounts.length - 1]; ethAccumulatorPerSpender[msg.sender] += ethReceived; tokenAccumulatorPerSpender[msg.sender] += tokenAmount; - - return ethReceived; } function onGasSpent(address payable spender, uint256 gasUsed, uint256 calldataSize) @@ -107,7 +105,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { } uint256 tokenAcc = tokenAccumulatorPerSpender[tx.origin]; - return _scaleTo18Decimals(tokenAcc) * 1e18 / ethAcc; + return (_scaleTo18Decimals(tokenAcc) * 1e18) / ethAcc; } function _scaleTo18Decimals(uint256 amount) internal view returns (uint256) { diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol index a12f90c7c..e3c405877 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol @@ -15,7 +15,6 @@ contract AmmTradeTrackerTest is Test { function setUp() public { vm.createSelectFork(vm.envString("ARB"), 261_666_155); - console.log(vm.envString("ARB")); vm.prank(owner); tradeTracker = new AmmTradeTracker( @@ -27,14 +26,35 @@ contract AmmTradeTrackerTest is Test { uint256 usdcAmount = 250e6; uint256 minEthReceived = 0.1 ether; - deal(USDC_ARB1, batchPosterOperator, usdcAmount); - - vm.startPrank(batchPosterOperator); - IERC20(USDC_ARB1).approve(address(tradeTracker), usdcAmount); - uint256 ethReceived = tradeTracker.swapTokenToEth(usdcAmount, minEthReceived); - vm.stopPrank(); + uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); + assertGe(ethReceived, minEthReceived); assertEq(tradeTracker.ethAccumulatorPerSpender(batchPosterOperator), ethReceived); assertEq(tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator), usdcAmount); } + + function testFork_GetExchangeRate() public { + assertEq(tradeTracker.getExchangeRate(), DEFAULT_EXCHANGE_RATE); + + uint256 usdcAmount = 250e6; + uint256 minEthReceived = 0.1 ether; + uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); + + vm.prank(batchPosterOperator, batchPosterOperator); + uint256 actualExchangeRate = tradeTracker.getExchangeRate(); + uint256 expectedExchangeRate = (usdcAmount * 1e30) / ethReceived; + assertEq(actualExchangeRate, expectedExchangeRate); + } + + function _swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) + internal + returns (uint256 ethReceived) + { + deal(USDC_ARB1, batchPosterOperator, tokenAmount); + + vm.startPrank(batchPosterOperator, batchPosterOperator); + IERC20(USDC_ARB1).approve(address(tradeTracker), tokenAmount); + ethReceived = tradeTracker.swapTokenToEth(tokenAmount, minEthReceived); + vm.stopPrank(); + } } From 93fba739ac12af4d260557b2ae7de3ff32b73fc4 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 9 Oct 2024 08:05:05 +0200 Subject: [PATCH 23/36] Fix >18 decimals scaling --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index a92384aa4..4620608de 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -114,7 +114,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { } else if (tokenDecimals < 18) { return amount * 10 ** (18 - tokenDecimals); } else { - return amount * 10 ** (18 - tokenDecimals); + return amount / 10 ** (tokenDecimals - 18); } } } From 8475b0a52b258e413328eb0de2c7bf01c97872bf Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 9 Oct 2024 09:09:55 +0200 Subject: [PATCH 24/36] Simplify token spending calculation --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 4620608de..b07639a21 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -14,6 +14,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { using SafeERC20 for IERC20; + uint256 public constant calldataCost = 12; IUniswapV2Router01 public immutable router; address public immutable token; address public immutable weth; @@ -72,18 +73,21 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { // update internal state uint256 exchangeRateUsed = _getExchangeRate(); if (exchangeRateUsed != 0) { - gasUsed += calldataSize * 16; + // calculate amount of ETH spent + gasUsed += calldataSize * calldataCost; uint256 ethDelta = gasUsed * block.basefee; - uint256 tokenDelta = ethDelta * exchangeRateUsed / 1e18; + // calculate amount of token spent to purchase ethDelta uint256 ethAcc = ethAccumulatorPerSpender[spender]; + uint256 tokenAcc = tokenAccumulatorPerSpender[spender]; + uint256 tokenDelta = (ethDelta * tokenAcc) / ethAcc; + if (ethDelta > ethAcc) { ethAccumulatorPerSpender[spender] = 0; } else { ethAccumulatorPerSpender[spender] -= ethDelta; } - uint256 tokenAcc = tokenAccumulatorPerSpender[spender]; if (tokenDelta > tokenAcc) { tokenAccumulatorPerSpender[spender] = 0; } else { From 2e5b24867d26c8bf2b40b97b1ad0c861e815e907 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 9 Oct 2024 09:57:10 +0200 Subject: [PATCH 25/36] Test state updates after batch is posted --- .../AmmTradeTracker.t.sol | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol index e3c405877..f98893674 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol @@ -3,6 +3,11 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import {AmmTradeTracker, IUniswapV2Router01, IERC20} from "./AmmTradeTracker.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 AmmTradeTrackerTest is Test { AmmTradeTracker public tradeTracker; @@ -14,7 +19,8 @@ contract AmmTradeTrackerTest is Test { uint256 public constant DEFAULT_EXCHANGE_RATE = 2500e18; function setUp() public { - vm.createSelectFork(vm.envString("ARB"), 261_666_155); + string memory arbRpc = vm.envString("ARB_RPC"); + vm.createSelectFork(arbRpc, 261_666_155); vm.prank(owner); tradeTracker = new AmmTradeTracker( @@ -46,6 +52,46 @@ contract AmmTradeTrackerTest is Test { assertEq(actualExchangeRate, expectedExchangeRate); } + function testFork_postBatch() public { + (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); + + // swap some tokens + uint256 usdcAmount = 250e6; + uint256 minEthReceived = 0.1 ether; + _swapTokenToEth(usdcAmount, minEthReceived); + + // snapshot values before batch has been posted + uint256 ethAccBefore = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); + uint256 tokenAccBefore = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); + vm.prank(batchPosterOperator, batchPosterOperator); + uint256 exchangeRateBefore = tradeTracker.getExchangeRate(); + + // set 0.1 gwei basefee and 30 gwei TX L1 fees + uint256 basefee = 100_000_000; + vm.fee(basefee); + uint256 l1Fees = 30_000_000_000; + vm.mockCall( + address(0x6c), abi.encodeWithSignature("getCurrentTxL1GasFees()"), abi.encode(l1Fees) + ); + + // post batch + address feeTokenPricer = address(seqInbox.feeTokenPricer()); + bytes memory batchData = hex"80567890"; + vm.prank(batchPosterOperator, batchPosterOperator); + seqInbox.addSequencerL2BatchFromOrigin(0, batchData, 0, IGasRefunder(feeTokenPricer), 0, 1); + + // snapshot values after batch has been posted + uint256 ethAccAfter = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); + uint256 tokenAccAfter = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); + vm.prank(batchPosterOperator, batchPosterOperator); + uint256 exchangeRateAfter = tradeTracker.getExchangeRate(); + + // checks + assertTrue(ethAccAfter < ethAccBefore); + assertTrue(tokenAccAfter < tokenAccBefore); + assertTrue(exchangeRateAfter != exchangeRateBefore); + } + function _swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) internal returns (uint256 ethReceived) @@ -57,4 +103,53 @@ contract AmmTradeTrackerTest is Test { ethReceived = tradeTracker.swapTokenToEth(tokenAmount, minEthReceived); vm.stopPrank(); } + + function _deployFeeTokenRollup() internal returns (SequencerInbox, ERC20Bridge) { + RollupMock rollupMock = new RollupMock(owner); + ERC20Bridge bridgeImpl = new ERC20Bridge(); + address proxyAdmin = makeAddr("proxyAdmin"); + ERC20Bridge bridge = ERC20Bridge( + address(new TransparentUpgradeableProxy(address(bridgeImpl), proxyAdmin, "")) + ); + address nativeToken = address(new ERC20PresetMinterPauser("Appchain Token", "App")); + + bridge.initialize(IOwnable(address(rollupMock)), nativeToken); + vm.prank(owner); + bridge.setDelayedInbox(makeAddr("inbox"), true); + + /// this will result in 'hostChainIsArbitrum = true' + vm.mockCall( + address(100), + abi.encodeWithSelector(ArbSys.arbOSVersion.selector), + abi.encode(uint256(11)) + ); + uint256 maxDataSize = 10_000; + SequencerInbox seqInboxImpl = new SequencerInbox(maxDataSize, IReader4844(address(0)), true); + SequencerInbox seqInbox = SequencerInbox( + address(new TransparentUpgradeableProxy(address(seqInboxImpl), proxyAdmin, "")) + ); + ISequencerInbox.MaxTimeVariation memory maxTimeVariation = ISequencerInbox.MaxTimeVariation({ + delayBlocks: 10, + futureBlocks: 10, + delaySeconds: 100, + futureSeconds: 100 + }); + seqInbox.initialize(bridge, maxTimeVariation, IFeeTokenPricer(tradeTracker)); + + vm.prank(owner); + seqInbox.setIsBatchPoster(batchPosterOperator, true); + + vm.prank(owner); + bridge.setSequencerInbox(address(seqInbox)); + + return (seqInbox, bridge); + } +} + +contract RollupMock { + address public immutable owner; + + constructor(address _owner) { + owner = _owner; + } } From a76a1040b01716ef13453619d46c44c43beb6997 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 9 Oct 2024 10:45:35 +0200 Subject: [PATCH 26/36] Make calldata cost adjustable --- .../AmmTradeTracker.sol | 18 +++++++++++++----- .../AmmTradeTracker.t.sol | 6 +++++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index b07639a21..212e83a83 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -14,7 +14,6 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { using SafeERC20 for IERC20; - uint256 public constant calldataCost = 12; IUniswapV2Router01 public immutable router; address public immutable token; address public immutable weth; @@ -24,15 +23,20 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { mapping(address => uint256) public tokenAccumulatorPerSpender; uint256 public defaultExchangeRate; - - constructor(IUniswapV2Router01 _router, address _token, uint256 _defaultExchangeRate) - Ownable() - { + uint256 public calldataCost; + + constructor( + IUniswapV2Router01 _router, + address _token, + uint256 _defaultExchangeRate, + uint256 _calldataCost + ) Ownable() { router = _router; token = _token; weth = _router.WETH(); defaultExchangeRate = _defaultExchangeRate; + calldataCost = _calldataCost; tokenDecimals = ERC20(_token).decimals(); IERC20(token).safeApprove(address(router), type(uint256).max); @@ -102,6 +106,10 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { defaultExchangeRate = _defaultExchangeRate; } + function setCalldataCost(uint256 _calldataCost) external onlyOwner { + calldataCost = _calldataCost; + } + function _getExchangeRate() internal view returns (uint256) { uint256 ethAcc = ethAccumulatorPerSpender[tx.origin]; if (ethAcc == 0) { diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol index f98893674..abea30509 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol @@ -17,6 +17,7 @@ contract AmmTradeTrackerTest is Test { address public constant V2_ROUTER_ARB1 = address(0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24); address public constant USDC_ARB1 = address(0xaf88d065e77c8cC2239327C5EDb3A432268e5831); uint256 public constant DEFAULT_EXCHANGE_RATE = 2500e18; + uint256 public constant DEFAULT_CALLDATA_COST = 12; function setUp() public { string memory arbRpc = vm.envString("ARB_RPC"); @@ -24,7 +25,10 @@ contract AmmTradeTrackerTest is Test { vm.prank(owner); tradeTracker = new AmmTradeTracker( - IUniswapV2Router01(V2_ROUTER_ARB1), USDC_ARB1, DEFAULT_EXCHANGE_RATE + IUniswapV2Router01(V2_ROUTER_ARB1), + USDC_ARB1, + DEFAULT_EXCHANGE_RATE, + DEFAULT_CALLDATA_COST ); } From 635cb84d703e0966b8978663ef223bcb7cbeea4e Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 9 Oct 2024 10:48:44 +0200 Subject: [PATCH 27/36] No updates if ethAcc is 0 --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 212e83a83..0407e13a3 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -72,7 +72,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { function onGasSpent(address payable spender, uint256 gasUsed, uint256 calldataSize) external - returns (bool success) + returns (bool) { // update internal state uint256 exchangeRateUsed = _getExchangeRate(); @@ -83,6 +83,9 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { // calculate amount of token spent to purchase ethDelta uint256 ethAcc = ethAccumulatorPerSpender[spender]; + if (ethAcc == 0) { + return true; + } uint256 tokenAcc = tokenAccumulatorPerSpender[spender]; uint256 tokenDelta = (ethDelta * tokenAcc) / ethAcc; @@ -99,7 +102,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { } } - success = true; + return true; } function setDefaultExchangeRate(uint256 _defaultExchangeRate) external onlyOwner { From 382b7f51b93205397aeca53f9642ba1a448a3b0b Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 9 Oct 2024 12:20:34 +0200 Subject: [PATCH 28/36] Control who can call gas hook and update internal state --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 0407e13a3..3067b0628 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -21,6 +21,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { mapping(address => uint256) public ethAccumulatorPerSpender; mapping(address => uint256) public tokenAccumulatorPerSpender; + mapping(address => bool) public allowedCallers; uint256 public defaultExchangeRate; uint256 public calldataCost; @@ -29,7 +30,8 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { IUniswapV2Router01 _router, address _token, uint256 _defaultExchangeRate, - uint256 _calldataCost + uint256 _calldataCost, + address _sequencerInbox ) Ownable() { router = _router; token = _token; @@ -37,6 +39,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { defaultExchangeRate = _defaultExchangeRate; calldataCost = _calldataCost; + allowedCallers[_sequencerInbox] = true; tokenDecimals = ERC20(_token).decimals(); IERC20(token).safeApprove(address(router), type(uint256).max); @@ -74,6 +77,8 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { external returns (bool) { + require(allowedCallers[msg.sender], "AmmTradeTracker: Caller not allowed"); + // update internal state uint256 exchangeRateUsed = _getExchangeRate(); if (exchangeRateUsed != 0) { @@ -89,6 +94,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { uint256 tokenAcc = tokenAccumulatorPerSpender[spender]; uint256 tokenDelta = (ethDelta * tokenAcc) / ethAcc; + // update accumulators if (ethDelta > ethAcc) { ethAccumulatorPerSpender[spender] = 0; } else { @@ -113,6 +119,10 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { calldataCost = _calldataCost; } + function allowCaller(address caller, bool allowed) external onlyOwner { + allowedCallers[caller] = allowed; + } + function _getExchangeRate() internal view returns (uint256) { uint256 ethAcc = ethAccumulatorPerSpender[tx.origin]; if (ethAcc == 0) { From 58ca93752d93cf6dbd07cb0914c13b10ce75a724 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 9 Oct 2024 13:11:52 +0200 Subject: [PATCH 29/36] Add events and custom error --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 15 +++++++++++---- .../AmmTradeTracker.t.sol | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 3067b0628..6f8885edc 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -26,12 +26,17 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { uint256 public defaultExchangeRate; uint256 public calldataCost; + error CallerNotAllowed(address caller); + + event DefaultExchangeRateSet(uint256 defaultExchangeRate); + event CalldataCostSet(uint256 calldataCost); + event CallerAllowed(address indexed caller, bool allowed); + constructor( IUniswapV2Router01 _router, address _token, uint256 _defaultExchangeRate, - uint256 _calldataCost, - address _sequencerInbox + uint256 _calldataCost ) Ownable() { router = _router; token = _token; @@ -39,7 +44,6 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { defaultExchangeRate = _defaultExchangeRate; calldataCost = _calldataCost; - allowedCallers[_sequencerInbox] = true; tokenDecimals = ERC20(_token).decimals(); IERC20(token).safeApprove(address(router), type(uint256).max); @@ -77,7 +81,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { external returns (bool) { - require(allowedCallers[msg.sender], "AmmTradeTracker: Caller not allowed"); + if (!allowedCallers[msg.sender]) revert CallerNotAllowed(msg.sender); // update internal state uint256 exchangeRateUsed = _getExchangeRate(); @@ -113,14 +117,17 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { function setDefaultExchangeRate(uint256 _defaultExchangeRate) external onlyOwner { defaultExchangeRate = _defaultExchangeRate; + emit DefaultExchangeRateSet(_defaultExchangeRate); } function setCalldataCost(uint256 _calldataCost) external onlyOwner { calldataCost = _calldataCost; + emit CalldataCostSet(_calldataCost); } function allowCaller(address caller, bool allowed) external onlyOwner { allowedCallers[caller] = allowed; + emit CallerAllowed(caller, allowed); } function _getExchangeRate() internal view returns (uint256) { diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol index abea30509..e6dc54e59 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol @@ -58,6 +58,8 @@ contract AmmTradeTrackerTest is Test { function testFork_postBatch() public { (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); + vm.prank(owner); + tradeTracker.allowCaller(address(seqInbox), true); // swap some tokens uint256 usdcAmount = 250e6; From 5ba5abc1041763c3261350391118da9686292431 Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Wed, 23 Oct 2024 16:26:41 +0200 Subject: [PATCH 30/36] Provide trade deadline from outside --- .../uniswap-v2-trade-tracker/AmmTradeTracker.sol | 4 ++-- .../uniswap-v2-trade-tracker/AmmTradeTracker.t.sol | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index 6f8885edc..de5761f58 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -54,7 +54,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { return _getExchangeRate(); } - function swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) + function swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived, uint256 deadline) external returns (uint256 ethReceived) { @@ -69,7 +69,7 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { amountOutMin: minEthReceived, path: path, to: msg.sender, - deadline: block.timestamp + deadline: deadline }); ethReceived = amounts[amounts.length - 1]; diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol index e6dc54e59..13b8e2393 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol @@ -106,7 +106,8 @@ contract AmmTradeTrackerTest is Test { vm.startPrank(batchPosterOperator, batchPosterOperator); IERC20(USDC_ARB1).approve(address(tradeTracker), tokenAmount); - ethReceived = tradeTracker.swapTokenToEth(tokenAmount, minEthReceived); + ethReceived = + tradeTracker.swapTokenToEth(tokenAmount, minEthReceived, block.timestamp + 100); vm.stopPrank(); } From 95a693ed367639a4c5f2f7ea284a69fdd167602d Mon Sep 17 00:00:00 2001 From: Goran Vladika Date: Thu, 24 Oct 2024 13:01:05 +0200 Subject: [PATCH 31/36] Move fork tests to separate file --- .../AmmTradeTracker.t.sol | 81 +------ .../AmmTradeTrackerFork.t.sol | 223 ++++++++++++++++++ 2 files changed, 228 insertions(+), 76 deletions(-) create mode 100644 test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol index 13b8e2393..f7baefda5 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol @@ -20,9 +20,6 @@ contract AmmTradeTrackerTest is Test { uint256 public constant DEFAULT_CALLDATA_COST = 12; function setUp() public { - string memory arbRpc = vm.envString("ARB_RPC"); - vm.createSelectFork(arbRpc, 261_666_155); - vm.prank(owner); tradeTracker = new AmmTradeTracker( IUniswapV2Router01(V2_ROUTER_ARB1), @@ -32,83 +29,15 @@ contract AmmTradeTrackerTest is Test { ); } - function testFork_CanSwapTokenToEth() public { - uint256 usdcAmount = 250e6; - uint256 minEthReceived = 0.1 ether; - - uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); - - assertGe(ethReceived, minEthReceived); - assertEq(tradeTracker.ethAccumulatorPerSpender(batchPosterOperator), ethReceived); - assertEq(tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator), usdcAmount); - } - - function testFork_GetExchangeRate() public { - assertEq(tradeTracker.getExchangeRate(), DEFAULT_EXCHANGE_RATE); - - uint256 usdcAmount = 250e6; - uint256 minEthReceived = 0.1 ether; - uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); - - vm.prank(batchPosterOperator, batchPosterOperator); - uint256 actualExchangeRate = tradeTracker.getExchangeRate(); - uint256 expectedExchangeRate = (usdcAmount * 1e30) / ethReceived; - assertEq(actualExchangeRate, expectedExchangeRate); - } - - function testFork_postBatch() public { + function testOnGasSpent() public { (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); vm.prank(owner); tradeTracker.allowCaller(address(seqInbox), true); - // swap some tokens - uint256 usdcAmount = 250e6; - uint256 minEthReceived = 0.1 ether; - _swapTokenToEth(usdcAmount, minEthReceived); - - // snapshot values before batch has been posted - uint256 ethAccBefore = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); - uint256 tokenAccBefore = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); - vm.prank(batchPosterOperator, batchPosterOperator); - uint256 exchangeRateBefore = tradeTracker.getExchangeRate(); - - // set 0.1 gwei basefee and 30 gwei TX L1 fees - uint256 basefee = 100_000_000; - vm.fee(basefee); - uint256 l1Fees = 30_000_000_000; - vm.mockCall( - address(0x6c), abi.encodeWithSignature("getCurrentTxL1GasFees()"), abi.encode(l1Fees) - ); - - // post batch - address feeTokenPricer = address(seqInbox.feeTokenPricer()); - bytes memory batchData = hex"80567890"; - vm.prank(batchPosterOperator, batchPosterOperator); - seqInbox.addSequencerL2BatchFromOrigin(0, batchData, 0, IGasRefunder(feeTokenPricer), 0, 1); - - // snapshot values after batch has been posted - uint256 ethAccAfter = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); - uint256 tokenAccAfter = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); - vm.prank(batchPosterOperator, batchPosterOperator); - uint256 exchangeRateAfter = tradeTracker.getExchangeRate(); - - // checks - assertTrue(ethAccAfter < ethAccBefore); - assertTrue(tokenAccAfter < tokenAccBefore); - assertTrue(exchangeRateAfter != exchangeRateBefore); - } - - function _swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) - internal - returns (uint256 ethReceived) - { - deal(USDC_ARB1, batchPosterOperator, tokenAmount); - - vm.startPrank(batchPosterOperator, batchPosterOperator); - IERC20(USDC_ARB1).approve(address(tradeTracker), tokenAmount); - ethReceived = - tradeTracker.swapTokenToEth(tokenAmount, minEthReceived, block.timestamp + 100); - vm.stopPrank(); + uint256 gasUsed = 300_000; + uint256 calldataSize = 10_000; + vm.prank(address(seqInbox)); + tradeTracker.onGasSpent(payable(batchPosterOperator), gasUsed, calldataSize); } function _deployFeeTokenRollup() internal returns (SequencerInbox, ERC20Bridge) { diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol new file mode 100644 index 000000000..b7b1d6c70 --- /dev/null +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import {AmmTradeTracker, IUniswapV2Router01, IERC20} from "./AmmTradeTracker.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 AmmTradeTrackerForkTest is Test { + AmmTradeTracker public tradeTracker; + address public owner = makeAddr("tradeTrackerOwner"); + address public batchPosterOperator = makeAddr("batchPosterOperator"); + + address public constant V2_ROUTER_ARB1 = address(0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24); + address public constant USDC_ARB1 = address(0xaf88d065e77c8cC2239327C5EDb3A432268e5831); + uint256 public constant DEFAULT_EXCHANGE_RATE = 2500e18; + uint256 public constant DEFAULT_CALLDATA_COST = 12; + + function setUp() public { + string memory arbRpc = vm.envString("ARB_RPC"); + vm.createSelectFork(arbRpc, 261_666_155); + + vm.prank(owner); + tradeTracker = new AmmTradeTracker( + IUniswapV2Router01(V2_ROUTER_ARB1), + USDC_ARB1, + DEFAULT_EXCHANGE_RATE, + DEFAULT_CALLDATA_COST + ); + } + + function testFork_CanSwapTokenToEth() public { + uint256 usdcAmount = 250e6; + uint256 minEthReceived = 0.1 ether; + + uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); + + assertGe(ethReceived, minEthReceived); + assertEq(tradeTracker.ethAccumulatorPerSpender(batchPosterOperator), ethReceived); + assertEq(tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator), usdcAmount); + } + + function testFork_GetExchangeRate() public { + assertEq(tradeTracker.getExchangeRate(), DEFAULT_EXCHANGE_RATE); + + uint256 usdcAmount = 250e6; + uint256 minEthReceived = 0.1 ether; + uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); + + vm.prank(batchPosterOperator, batchPosterOperator); + uint256 actualExchangeRate = tradeTracker.getExchangeRate(); + uint256 expectedExchangeRate = (usdcAmount * 1e30) / ethReceived; + assertEq(actualExchangeRate, expectedExchangeRate); + } + + function testFork_postBatch() public { + (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); + vm.prank(owner); + tradeTracker.allowCaller(address(seqInbox), true); + + // swap some tokens + uint256 usdcAmount = 250e6; + uint256 minEthReceived = 0.1 ether; + _swapTokenToEth(usdcAmount, minEthReceived); + + // snapshot values before batch has been posted + uint256 ethAccBefore = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); + uint256 tokenAccBefore = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); + vm.prank(batchPosterOperator, batchPosterOperator); + uint256 exchangeRateBefore = tradeTracker.getExchangeRate(); + + // set 0.1 gwei basefee and 30 gwei TX L1 fees + uint256 basefee = 100_000_000; + vm.fee(basefee); + uint256 l1Fees = 30_000_000_000; + vm.mockCall( + address(0x6c), abi.encodeWithSignature("getCurrentTxL1GasFees()"), abi.encode(l1Fees) + ); + + // post batch + address feeTokenPricer = address(seqInbox.feeTokenPricer()); + bytes memory batchData = hex"80567890"; + vm.prank(batchPosterOperator, batchPosterOperator); + seqInbox.addSequencerL2BatchFromOrigin(0, batchData, 0, IGasRefunder(feeTokenPricer), 0, 1); + + // snapshot values after batch has been posted + uint256 ethAccAfter = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); + uint256 tokenAccAfter = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); + vm.prank(batchPosterOperator, batchPosterOperator); + uint256 exchangeRateAfter = tradeTracker.getExchangeRate(); + + // checks + assertTrue(ethAccAfter < ethAccBefore); + assertTrue(tokenAccAfter < tokenAccBefore); + assertTrue(exchangeRateAfter != exchangeRateBefore); + } + + // function testFork_postMultipleBatches() public { + // (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); + // vm.prank(owner); + // tradeTracker.allowCaller(address(seqInbox), true); + + // // swap some tokens + // uint256 usdcAmount = 250e6; + // uint256 minEthReceived = 0.1 ether; + // uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); + + // console.log("Swapped 250e6 USDC for ", ethReceived, " ETH"); + + // // snapshot values before batch has been posted + // uint256 ethAccBefore = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); + // uint256 tokenAccBefore = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); + // vm.prank(batchPosterOperator, batchPosterOperator); + // uint256 exchangeRateBefore = tradeTracker.getExchangeRate(); + + // console.log("ethAccBefore: ", ethAccBefore); + // console.log("tokenAccBefore: ", tokenAccBefore); + // console.log("exchangeRateBefore: ", exchangeRateBefore); + + // // set 0.1 gwei basefee and 30 gwei TX L1 fees + // uint256 basefee = 100_000_000; + // vm.fee(basefee); + // uint256 l1Fees = 30_000_000_000; + // vm.mockCall( + // address(0x6c), abi.encodeWithSignature("getCurrentTxL1GasFees()"), abi.encode(l1Fees) + // ); + + // // post batch + // address feeTokenPricer = address(seqInbox.feeTokenPricer()); + // bytes memory batchData = hex"80567890"; + // vm.prank(batchPosterOperator, batchPosterOperator); + // seqInbox.addSequencerL2BatchFromOrigin(0, batchData, 0, IGasRefunder(feeTokenPricer), 0, 1); + + // // snapshot values after batch has been posted + // uint256 ethAccAfter = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); + // uint256 tokenAccAfter = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); + // vm.prank(batchPosterOperator, batchPosterOperator); + // uint256 exchangeRateAfter = tradeTracker.getExchangeRate(); + + // console.log("-----------"); + // console.log("ethAccAfter: ", ethAccAfter); + // console.log("tokenAccAfter: ", tokenAccAfter); + // console.log("exchangeRateAfter: ", exchangeRateAfter); + + // vm.prank(batchPosterOperator, batchPosterOperator); + // seqInbox.addSequencerL2BatchFromOrigin(1, batchData, 0, IGasRefunder(feeTokenPricer), 0, 1); + + // uint256 ethAccEnd = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); + // uint256 tokenAccEnd = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); + // vm.prank(batchPosterOperator, batchPosterOperator); + // uint256 exchangeRateEnd = tradeTracker.getExchangeRate(); + + // console.log("-----------"); + // console.log("ethAccEnd: ", ethAccEnd); + // console.log("tokenAccEnd: ", tokenAccEnd); + // console.log("exchangeRateEnd: ", exchangeRateEnd); + // } + + function _swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) + internal + returns (uint256 ethReceived) + { + deal(USDC_ARB1, batchPosterOperator, tokenAmount); + + vm.startPrank(batchPosterOperator, batchPosterOperator); + IERC20(USDC_ARB1).approve(address(tradeTracker), tokenAmount); + ethReceived = + tradeTracker.swapTokenToEth(tokenAmount, minEthReceived, block.timestamp + 100); + vm.stopPrank(); + } + + function _deployFeeTokenRollup() internal returns (SequencerInbox, ERC20Bridge) { + RollupMock rollupMock = new RollupMock(owner); + ERC20Bridge bridgeImpl = new ERC20Bridge(); + address proxyAdmin = makeAddr("proxyAdmin"); + ERC20Bridge bridge = ERC20Bridge( + address(new TransparentUpgradeableProxy(address(bridgeImpl), proxyAdmin, "")) + ); + address nativeToken = address(new ERC20PresetMinterPauser("Appchain Token", "App")); + + bridge.initialize(IOwnable(address(rollupMock)), nativeToken); + vm.prank(owner); + bridge.setDelayedInbox(makeAddr("inbox"), true); + + /// this will result in 'hostChainIsArbitrum = true' + vm.mockCall( + address(100), + abi.encodeWithSelector(ArbSys.arbOSVersion.selector), + abi.encode(uint256(11)) + ); + uint256 maxDataSize = 10_000; + SequencerInbox seqInboxImpl = new SequencerInbox(maxDataSize, IReader4844(address(0)), true); + SequencerInbox seqInbox = SequencerInbox( + address(new TransparentUpgradeableProxy(address(seqInboxImpl), proxyAdmin, "")) + ); + ISequencerInbox.MaxTimeVariation memory maxTimeVariation = ISequencerInbox.MaxTimeVariation({ + delayBlocks: 10, + futureBlocks: 10, + delaySeconds: 100, + futureSeconds: 100 + }); + seqInbox.initialize(bridge, maxTimeVariation, IFeeTokenPricer(tradeTracker)); + + vm.prank(owner); + seqInbox.setIsBatchPoster(batchPosterOperator, true); + + vm.prank(owner); + bridge.setSequencerInbox(address(seqInbox)); + + return (seqInbox, bridge); + } +} + +contract RollupMock { + address public immutable owner; + + constructor(address _owner) { + owner = _owner; + } +} From d7d3d1a141acc9b6dfae32ca2288e9f147ba3e8d Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Fri, 29 Nov 2024 15:49:06 +0000 Subject: [PATCH 32/36] Toml update --- foundry.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/foundry.toml b/foundry.toml index 8db479af6..d08e5ee31 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,12 +2,8 @@ src = 'src' out = 'out' libs = ['node_modules', 'lib'] -<<<<<<< HEAD test = 'test/foundry' cache_path = 'forge-cache/sol' -======= -cache_path = 'forge-cache/sol' ->>>>>>> custom-fee-rollup optimizer = true optimizer_runs = 2000 via_ir = false From 89542c1174566db101d83741d867c93cfc077df5 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Fri, 29 Nov 2024 16:30:03 +0000 Subject: [PATCH 33/36] Formatting and uniswap install --- package.json | 2 ++ .../ConstantExchangeRatePricer.sol | 4 ++- .../OwnerAdjustableExchangeRatePricer.sol | 8 +++-- .../AmmTradeTracker.sol | 30 ++++++++++++------- .../AmmTradeTracker.t.sol | 18 +++++++++-- .../AmmTradeTrackerFork.t.sol | 28 ++++++++++++----- .../uniswap-v2-twap/FixedPoint.sol | 13 ++++---- .../uniswap-v2-twap/UniswapV2TwapPricer.sol | 4 ++- yarn.lock | 5 ++++ 9 files changed, 81 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index c25cb502f..193fc66db 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 index 2efe50abb..1160c9c9d 100644 --- a/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol +++ b/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol @@ -10,7 +10,9 @@ import {IFeeTokenPricer} from "../../../src/bridge/ISequencerInbox.sol"; contract ConstantExchangeRatePricer is IFeeTokenPricer { uint256 immutable exchangeRate; - constructor(uint256 _exchangeRate) { + constructor( + uint256 _exchangeRate + ) { exchangeRate = _exchangeRate; } diff --git a/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol b/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol index 8a39b6bb2..65137a3b0 100644 --- a/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol +++ b/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol @@ -13,12 +13,16 @@ contract OwnerAdjustableExchangeRatePricer is IFeeTokenPricer, Ownable { event ExchangeRateSet(uint256 newExchangeRate); - constructor(uint256 initialExchangeRate) Ownable() { + constructor( + uint256 initialExchangeRate + ) Ownable() { exchangeRate = initialExchangeRate; emit ExchangeRateSet(initialExchangeRate); } - function setExchangeRate(uint256 _exchangeRate) external onlyOwner { + function setExchangeRate( + uint256 _exchangeRate + ) external onlyOwner { exchangeRate = _exchangeRate; emit ExchangeRateSet(_exchangeRate); } diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol index de5761f58..448f7dc62 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol @@ -54,10 +54,11 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { return _getExchangeRate(); } - function swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived, uint256 deadline) - external - returns (uint256 ethReceived) - { + function swapTokenToEth( + uint256 tokenAmount, + uint256 minEthReceived, + uint256 deadline + ) external returns (uint256 ethReceived) { IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); address[] memory path = new address[](2); @@ -77,10 +78,11 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { tokenAccumulatorPerSpender[msg.sender] += tokenAmount; } - function onGasSpent(address payable spender, uint256 gasUsed, uint256 calldataSize) - external - returns (bool) - { + function onGasSpent( + address payable spender, + uint256 gasUsed, + uint256 calldataSize + ) external returns (bool) { if (!allowedCallers[msg.sender]) revert CallerNotAllowed(msg.sender); // update internal state @@ -115,12 +117,16 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { return true; } - function setDefaultExchangeRate(uint256 _defaultExchangeRate) external onlyOwner { + function setDefaultExchangeRate( + uint256 _defaultExchangeRate + ) external onlyOwner { defaultExchangeRate = _defaultExchangeRate; emit DefaultExchangeRateSet(_defaultExchangeRate); } - function setCalldataCost(uint256 _calldataCost) external onlyOwner { + function setCalldataCost( + uint256 _calldataCost + ) external onlyOwner { calldataCost = _calldataCost; emit CalldataCostSet(_calldataCost); } @@ -140,7 +146,9 @@ contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { return (_scaleTo18Decimals(tokenAcc) * 1e18) / ethAcc; } - function _scaleTo18Decimals(uint256 amount) internal view returns (uint256) { + function _scaleTo18Decimals( + uint256 amount + ) internal view returns (uint256) { if (tokenDecimals == 18) { return amount; } else if (tokenDecimals < 18) { diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol index f7baefda5..fe8ee4cae 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol @@ -20,6 +20,7 @@ contract AmmTradeTrackerTest is Test { uint256 public constant DEFAULT_CALLDATA_COST = 12; function setUp() public { + return; // CHRIS: TODO: decide whether to keep the trade tracker and test vm.prank(owner); tradeTracker = new AmmTradeTracker( IUniswapV2Router01(V2_ROUTER_ARB1), @@ -30,6 +31,7 @@ contract AmmTradeTrackerTest is Test { } function testOnGasSpent() public { + return; // CHRIS: TODO: decide whether to keep the trade tracker and test (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); vm.prank(owner); tradeTracker.allowCaller(address(seqInbox), true); @@ -60,7 +62,8 @@ contract AmmTradeTrackerTest is Test { abi.encode(uint256(11)) ); uint256 maxDataSize = 10_000; - SequencerInbox seqInboxImpl = new SequencerInbox(maxDataSize, IReader4844(address(0)), true); + SequencerInbox seqInboxImpl = + new SequencerInbox(maxDataSize, IReader4844(address(0)), true, true); SequencerInbox seqInbox = SequencerInbox( address(new TransparentUpgradeableProxy(address(seqInboxImpl), proxyAdmin, "")) ); @@ -70,7 +73,14 @@ contract AmmTradeTrackerTest is Test { delaySeconds: 100, futureSeconds: 100 }); - seqInbox.initialize(bridge, maxTimeVariation, IFeeTokenPricer(tradeTracker)); + BufferConfig memory bufferConfigDefault = BufferConfig({ + threshold: type(uint64).max, + max: type(uint64).max, + replenishRateInBasis: 714 + }); + seqInbox.initialize( + bridge, maxTimeVariation, bufferConfigDefault, IFeeTokenPricer(tradeTracker) + ); vm.prank(owner); seqInbox.setIsBatchPoster(batchPosterOperator, true); @@ -85,7 +95,9 @@ contract AmmTradeTrackerTest is Test { contract RollupMock { address public immutable owner; - constructor(address _owner) { + constructor( + address _owner + ) { owner = _owner; } } diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol index b7b1d6c70..3a3a28e16 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol @@ -20,6 +20,7 @@ contract AmmTradeTrackerForkTest is Test { uint256 public constant DEFAULT_CALLDATA_COST = 12; function setUp() public { + return; // CHRIS: TODO: decide whether to keep the trade tracker and test string memory arbRpc = vm.envString("ARB_RPC"); vm.createSelectFork(arbRpc, 261_666_155); @@ -33,6 +34,7 @@ contract AmmTradeTrackerForkTest is Test { } function testFork_CanSwapTokenToEth() public { + return; // CHRIS: TODO: decide whether to keep the trade tracker and test uint256 usdcAmount = 250e6; uint256 minEthReceived = 0.1 ether; @@ -44,6 +46,7 @@ contract AmmTradeTrackerForkTest is Test { } function testFork_GetExchangeRate() public { + return; // CHRIS: TODO: decide whether to keep the trade tracker and test assertEq(tradeTracker.getExchangeRate(), DEFAULT_EXCHANGE_RATE); uint256 usdcAmount = 250e6; @@ -57,6 +60,7 @@ contract AmmTradeTrackerForkTest is Test { } function testFork_postBatch() public { + return; // CHRIS: TODO: decide whether to keep the trade tracker and test (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); vm.prank(owner); tradeTracker.allowCaller(address(seqInbox), true); @@ -159,10 +163,10 @@ contract AmmTradeTrackerForkTest is Test { // console.log("exchangeRateEnd: ", exchangeRateEnd); // } - function _swapTokenToEth(uint256 tokenAmount, uint256 minEthReceived) - internal - returns (uint256 ethReceived) - { + function _swapTokenToEth( + uint256 tokenAmount, + uint256 minEthReceived + ) internal returns (uint256 ethReceived) { deal(USDC_ARB1, batchPosterOperator, tokenAmount); vm.startPrank(batchPosterOperator, batchPosterOperator); @@ -192,7 +196,8 @@ contract AmmTradeTrackerForkTest is Test { abi.encode(uint256(11)) ); uint256 maxDataSize = 10_000; - SequencerInbox seqInboxImpl = new SequencerInbox(maxDataSize, IReader4844(address(0)), true); + SequencerInbox seqInboxImpl = + new SequencerInbox(maxDataSize, IReader4844(address(0)), true, true); SequencerInbox seqInbox = SequencerInbox( address(new TransparentUpgradeableProxy(address(seqInboxImpl), proxyAdmin, "")) ); @@ -202,7 +207,14 @@ contract AmmTradeTrackerForkTest is Test { delaySeconds: 100, futureSeconds: 100 }); - seqInbox.initialize(bridge, maxTimeVariation, IFeeTokenPricer(tradeTracker)); + BufferConfig memory bufferConfigDefault = BufferConfig({ + threshold: type(uint64).max, + max: type(uint64).max, + replenishRateInBasis: 714 + }); + seqInbox.initialize( + bridge, maxTimeVariation, bufferConfigDefault, IFeeTokenPricer(tradeTracker) + ); vm.prank(owner); seqInbox.setIsBatchPoster(batchPosterOperator, true); @@ -217,7 +229,9 @@ contract AmmTradeTrackerForkTest is Test { contract RollupMock { address public immutable owner; - constructor(address _owner) { + constructor( + address _owner + ) { owner = _owner; } } diff --git a/test/foundry/fee-token-pricers/uniswap-v2-twap/FixedPoint.sol b/test/foundry/fee-token-pricers/uniswap-v2-twap/FixedPoint.sol index 807739e97..a87548693 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-twap/FixedPoint.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-twap/FixedPoint.sol @@ -26,7 +26,9 @@ library FixedPoint { 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) { + function decode144( + uq144x112 memory self + ) internal pure returns (uint144) { return uint144(self._x >> RESOLUTION); } @@ -40,11 +42,10 @@ library FixedPoint { // 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) - { + 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); diff --git a/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol index 0c1385604..b24318fad 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol @@ -66,7 +66,9 @@ contract UniswapV2TwapPricer is IFeeTokenPricer { _update(timeElapsed); } - function _update(uint256 timeElapsed) internal { + function _update( + uint256 timeElapsed + ) internal { uint32 currentBlockTimestamp = uint32(block.timestamp); // fetch latest cumulative price accumulators diff --git a/yarn.lock b/yarn.lock index c0c66f2d1..3c2c4294c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1417,6 +1417,11 @@ 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" From 9f56d06b6a2ac7feebd8db6b3c6c5e72111016a1 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Fri, 13 Dec 2024 19:00:08 +0000 Subject: [PATCH 34/36] Added trade tracker instead of amm trade tracker, and a readme --- test/foundry/fee-token-pricers/README.md | 28 +++ .../trade-tracker/TradeTracker.sol | 98 ++++++++ .../trade-tracker/TradeTracker.t.sol | 97 +++++++ .../AmmTradeTracker.sol | 171 ------------- .../AmmTradeTracker.t.sol | 103 -------- .../AmmTradeTrackerFork.t.sol | 237 ------------------ 6 files changed, 223 insertions(+), 511 deletions(-) create mode 100644 test/foundry/fee-token-pricers/README.md create mode 100644 test/foundry/fee-token-pricers/trade-tracker/TradeTracker.sol create mode 100644 test/foundry/fee-token-pricers/trade-tracker/TradeTracker.t.sol delete mode 100644 test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol delete mode 100644 test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol delete mode 100644 test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol diff --git a/test/foundry/fee-token-pricers/README.md b/test/foundry/fee-token-pricers/README.md new file mode 100644 index 000000000..2823d9b1b --- /dev/null +++ b/test/foundry/fee-token-pricers/README.md @@ -0,0 +1,28 @@ +# 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 options +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 options 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. + +### Option 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. + +### Option 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). + +### Option 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 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). \ No newline at end of file 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 000000000..691ca9f42 --- /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 000000000..46e90e478 --- /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-trade-tracker/AmmTradeTracker.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol deleted file mode 100644 index 448f7dc62..000000000 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.sol +++ /dev/null @@ -1,171 +0,0 @@ -// 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"; - -/** - * @title Test implementation of a fee token pricer that trades on AMM and keeps track of traded amount to calculate exchange rate - */ -contract AmmTradeTracker is IFeeTokenPricer, IGasRefunder, Ownable { - using SafeERC20 for IERC20; - - IUniswapV2Router01 public immutable router; - address public immutable token; - address public immutable weth; - uint8 public immutable tokenDecimals; - - mapping(address => uint256) public ethAccumulatorPerSpender; - mapping(address => uint256) public tokenAccumulatorPerSpender; - mapping(address => bool) public allowedCallers; - - uint256 public defaultExchangeRate; - uint256 public calldataCost; - - error CallerNotAllowed(address caller); - - event DefaultExchangeRateSet(uint256 defaultExchangeRate); - event CalldataCostSet(uint256 calldataCost); - event CallerAllowed(address indexed caller, bool allowed); - - constructor( - IUniswapV2Router01 _router, - address _token, - uint256 _defaultExchangeRate, - uint256 _calldataCost - ) Ownable() { - router = _router; - token = _token; - weth = _router.WETH(); - - defaultExchangeRate = _defaultExchangeRate; - calldataCost = _calldataCost; - tokenDecimals = ERC20(_token).decimals(); - - IERC20(token).safeApprove(address(router), type(uint256).max); - } - - // @inheritdoc IFeeTokenPricer - function getExchangeRate() external view returns (uint256) { - return _getExchangeRate(); - } - - function swapTokenToEth( - uint256 tokenAmount, - uint256 minEthReceived, - uint256 deadline - ) external returns (uint256 ethReceived) { - IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount); - - address[] memory path = new address[](2); - path[0] = token; - path[1] = weth; - - uint256[] memory amounts = router.swapExactTokensForETH({ - amountIn: tokenAmount, - amountOutMin: minEthReceived, - path: path, - to: msg.sender, - deadline: deadline - }); - ethReceived = amounts[amounts.length - 1]; - - ethAccumulatorPerSpender[msg.sender] += ethReceived; - tokenAccumulatorPerSpender[msg.sender] += tokenAmount; - } - - function onGasSpent( - address payable spender, - uint256 gasUsed, - uint256 calldataSize - ) external returns (bool) { - if (!allowedCallers[msg.sender]) revert CallerNotAllowed(msg.sender); - - // update internal state - uint256 exchangeRateUsed = _getExchangeRate(); - if (exchangeRateUsed != 0) { - // calculate amount of ETH spent - gasUsed += calldataSize * calldataCost; - uint256 ethDelta = gasUsed * block.basefee; - - // calculate amount of token spent to purchase ethDelta - uint256 ethAcc = ethAccumulatorPerSpender[spender]; - if (ethAcc == 0) { - return true; - } - uint256 tokenAcc = tokenAccumulatorPerSpender[spender]; - uint256 tokenDelta = (ethDelta * tokenAcc) / ethAcc; - - // update accumulators - if (ethDelta > ethAcc) { - ethAccumulatorPerSpender[spender] = 0; - } else { - ethAccumulatorPerSpender[spender] -= ethDelta; - } - - if (tokenDelta > tokenAcc) { - tokenAccumulatorPerSpender[spender] = 0; - } else { - tokenAccumulatorPerSpender[spender] -= tokenDelta; - } - } - - return true; - } - - function setDefaultExchangeRate( - uint256 _defaultExchangeRate - ) external onlyOwner { - defaultExchangeRate = _defaultExchangeRate; - emit DefaultExchangeRateSet(_defaultExchangeRate); - } - - function setCalldataCost( - uint256 _calldataCost - ) external onlyOwner { - calldataCost = _calldataCost; - emit CalldataCostSet(_calldataCost); - } - - function allowCaller(address caller, bool allowed) external onlyOwner { - allowedCallers[caller] = allowed; - emit CallerAllowed(caller, allowed); - } - - function _getExchangeRate() internal view returns (uint256) { - uint256 ethAcc = ethAccumulatorPerSpender[tx.origin]; - if (ethAcc == 0) { - return defaultExchangeRate; - } - uint256 tokenAcc = tokenAccumulatorPerSpender[tx.origin]; - - return (_scaleTo18Decimals(tokenAcc) * 1e18) / ethAcc; - } - - function _scaleTo18Decimals( - uint256 amount - ) internal view returns (uint256) { - if (tokenDecimals == 18) { - return amount; - } else if (tokenDecimals < 18) { - return amount * 10 ** (18 - tokenDecimals); - } else { - return amount / 10 ** (tokenDecimals - 18); - } - } -} - -interface IUniswapV2Router01 { - function WETH() external pure returns (address); - function swapExactTokensForETH( - uint256 amountIn, - uint256 amountOutMin, - address[] calldata path, - address to, - uint256 deadline - ) external returns (uint256[] memory amounts); -} diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol deleted file mode 100644 index fe8ee4cae..000000000 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTracker.t.sol +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "forge-std/Test.sol"; -import {AmmTradeTracker, IUniswapV2Router01, IERC20} from "./AmmTradeTracker.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 AmmTradeTrackerTest is Test { - AmmTradeTracker public tradeTracker; - address public owner = makeAddr("tradeTrackerOwner"); - address public batchPosterOperator = makeAddr("batchPosterOperator"); - - address public constant V2_ROUTER_ARB1 = address(0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24); - address public constant USDC_ARB1 = address(0xaf88d065e77c8cC2239327C5EDb3A432268e5831); - uint256 public constant DEFAULT_EXCHANGE_RATE = 2500e18; - uint256 public constant DEFAULT_CALLDATA_COST = 12; - - function setUp() public { - return; // CHRIS: TODO: decide whether to keep the trade tracker and test - vm.prank(owner); - tradeTracker = new AmmTradeTracker( - IUniswapV2Router01(V2_ROUTER_ARB1), - USDC_ARB1, - DEFAULT_EXCHANGE_RATE, - DEFAULT_CALLDATA_COST - ); - } - - function testOnGasSpent() public { - return; // CHRIS: TODO: decide whether to keep the trade tracker and test - (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); - vm.prank(owner); - tradeTracker.allowCaller(address(seqInbox), true); - - uint256 gasUsed = 300_000; - uint256 calldataSize = 10_000; - vm.prank(address(seqInbox)); - tradeTracker.onGasSpent(payable(batchPosterOperator), gasUsed, calldataSize); - } - - function _deployFeeTokenRollup() internal returns (SequencerInbox, ERC20Bridge) { - RollupMock rollupMock = new RollupMock(owner); - ERC20Bridge bridgeImpl = new ERC20Bridge(); - address proxyAdmin = makeAddr("proxyAdmin"); - ERC20Bridge bridge = ERC20Bridge( - address(new TransparentUpgradeableProxy(address(bridgeImpl), proxyAdmin, "")) - ); - address nativeToken = address(new ERC20PresetMinterPauser("Appchain Token", "App")); - - bridge.initialize(IOwnable(address(rollupMock)), nativeToken); - vm.prank(owner); - bridge.setDelayedInbox(makeAddr("inbox"), true); - - /// this will result in 'hostChainIsArbitrum = true' - vm.mockCall( - address(100), - abi.encodeWithSelector(ArbSys.arbOSVersion.selector), - abi.encode(uint256(11)) - ); - uint256 maxDataSize = 10_000; - SequencerInbox seqInboxImpl = - new SequencerInbox(maxDataSize, IReader4844(address(0)), true, true); - SequencerInbox seqInbox = SequencerInbox( - address(new TransparentUpgradeableProxy(address(seqInboxImpl), proxyAdmin, "")) - ); - ISequencerInbox.MaxTimeVariation memory maxTimeVariation = ISequencerInbox.MaxTimeVariation({ - delayBlocks: 10, - futureBlocks: 10, - delaySeconds: 100, - futureSeconds: 100 - }); - BufferConfig memory bufferConfigDefault = BufferConfig({ - threshold: type(uint64).max, - max: type(uint64).max, - replenishRateInBasis: 714 - }); - seqInbox.initialize( - bridge, maxTimeVariation, bufferConfigDefault, IFeeTokenPricer(tradeTracker) - ); - - vm.prank(owner); - seqInbox.setIsBatchPoster(batchPosterOperator, true); - - vm.prank(owner); - bridge.setSequencerInbox(address(seqInbox)); - - return (seqInbox, bridge); - } -} - -contract RollupMock { - address public immutable owner; - - constructor( - address _owner - ) { - owner = _owner; - } -} diff --git a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol b/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol deleted file mode 100644 index 3a3a28e16..000000000 --- a/test/foundry/fee-token-pricers/uniswap-v2-trade-tracker/AmmTradeTrackerFork.t.sol +++ /dev/null @@ -1,237 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "forge-std/Test.sol"; -import {AmmTradeTracker, IUniswapV2Router01, IERC20} from "./AmmTradeTracker.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 AmmTradeTrackerForkTest is Test { - AmmTradeTracker public tradeTracker; - address public owner = makeAddr("tradeTrackerOwner"); - address public batchPosterOperator = makeAddr("batchPosterOperator"); - - address public constant V2_ROUTER_ARB1 = address(0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24); - address public constant USDC_ARB1 = address(0xaf88d065e77c8cC2239327C5EDb3A432268e5831); - uint256 public constant DEFAULT_EXCHANGE_RATE = 2500e18; - uint256 public constant DEFAULT_CALLDATA_COST = 12; - - function setUp() public { - return; // CHRIS: TODO: decide whether to keep the trade tracker and test - string memory arbRpc = vm.envString("ARB_RPC"); - vm.createSelectFork(arbRpc, 261_666_155); - - vm.prank(owner); - tradeTracker = new AmmTradeTracker( - IUniswapV2Router01(V2_ROUTER_ARB1), - USDC_ARB1, - DEFAULT_EXCHANGE_RATE, - DEFAULT_CALLDATA_COST - ); - } - - function testFork_CanSwapTokenToEth() public { - return; // CHRIS: TODO: decide whether to keep the trade tracker and test - uint256 usdcAmount = 250e6; - uint256 minEthReceived = 0.1 ether; - - uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); - - assertGe(ethReceived, minEthReceived); - assertEq(tradeTracker.ethAccumulatorPerSpender(batchPosterOperator), ethReceived); - assertEq(tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator), usdcAmount); - } - - function testFork_GetExchangeRate() public { - return; // CHRIS: TODO: decide whether to keep the trade tracker and test - assertEq(tradeTracker.getExchangeRate(), DEFAULT_EXCHANGE_RATE); - - uint256 usdcAmount = 250e6; - uint256 minEthReceived = 0.1 ether; - uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); - - vm.prank(batchPosterOperator, batchPosterOperator); - uint256 actualExchangeRate = tradeTracker.getExchangeRate(); - uint256 expectedExchangeRate = (usdcAmount * 1e30) / ethReceived; - assertEq(actualExchangeRate, expectedExchangeRate); - } - - function testFork_postBatch() public { - return; // CHRIS: TODO: decide whether to keep the trade tracker and test - (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); - vm.prank(owner); - tradeTracker.allowCaller(address(seqInbox), true); - - // swap some tokens - uint256 usdcAmount = 250e6; - uint256 minEthReceived = 0.1 ether; - _swapTokenToEth(usdcAmount, minEthReceived); - - // snapshot values before batch has been posted - uint256 ethAccBefore = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); - uint256 tokenAccBefore = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); - vm.prank(batchPosterOperator, batchPosterOperator); - uint256 exchangeRateBefore = tradeTracker.getExchangeRate(); - - // set 0.1 gwei basefee and 30 gwei TX L1 fees - uint256 basefee = 100_000_000; - vm.fee(basefee); - uint256 l1Fees = 30_000_000_000; - vm.mockCall( - address(0x6c), abi.encodeWithSignature("getCurrentTxL1GasFees()"), abi.encode(l1Fees) - ); - - // post batch - address feeTokenPricer = address(seqInbox.feeTokenPricer()); - bytes memory batchData = hex"80567890"; - vm.prank(batchPosterOperator, batchPosterOperator); - seqInbox.addSequencerL2BatchFromOrigin(0, batchData, 0, IGasRefunder(feeTokenPricer), 0, 1); - - // snapshot values after batch has been posted - uint256 ethAccAfter = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); - uint256 tokenAccAfter = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); - vm.prank(batchPosterOperator, batchPosterOperator); - uint256 exchangeRateAfter = tradeTracker.getExchangeRate(); - - // checks - assertTrue(ethAccAfter < ethAccBefore); - assertTrue(tokenAccAfter < tokenAccBefore); - assertTrue(exchangeRateAfter != exchangeRateBefore); - } - - // function testFork_postMultipleBatches() public { - // (SequencerInbox seqInbox,) = _deployFeeTokenRollup(); - // vm.prank(owner); - // tradeTracker.allowCaller(address(seqInbox), true); - - // // swap some tokens - // uint256 usdcAmount = 250e6; - // uint256 minEthReceived = 0.1 ether; - // uint256 ethReceived = _swapTokenToEth(usdcAmount, minEthReceived); - - // console.log("Swapped 250e6 USDC for ", ethReceived, " ETH"); - - // // snapshot values before batch has been posted - // uint256 ethAccBefore = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); - // uint256 tokenAccBefore = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); - // vm.prank(batchPosterOperator, batchPosterOperator); - // uint256 exchangeRateBefore = tradeTracker.getExchangeRate(); - - // console.log("ethAccBefore: ", ethAccBefore); - // console.log("tokenAccBefore: ", tokenAccBefore); - // console.log("exchangeRateBefore: ", exchangeRateBefore); - - // // set 0.1 gwei basefee and 30 gwei TX L1 fees - // uint256 basefee = 100_000_000; - // vm.fee(basefee); - // uint256 l1Fees = 30_000_000_000; - // vm.mockCall( - // address(0x6c), abi.encodeWithSignature("getCurrentTxL1GasFees()"), abi.encode(l1Fees) - // ); - - // // post batch - // address feeTokenPricer = address(seqInbox.feeTokenPricer()); - // bytes memory batchData = hex"80567890"; - // vm.prank(batchPosterOperator, batchPosterOperator); - // seqInbox.addSequencerL2BatchFromOrigin(0, batchData, 0, IGasRefunder(feeTokenPricer), 0, 1); - - // // snapshot values after batch has been posted - // uint256 ethAccAfter = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); - // uint256 tokenAccAfter = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); - // vm.prank(batchPosterOperator, batchPosterOperator); - // uint256 exchangeRateAfter = tradeTracker.getExchangeRate(); - - // console.log("-----------"); - // console.log("ethAccAfter: ", ethAccAfter); - // console.log("tokenAccAfter: ", tokenAccAfter); - // console.log("exchangeRateAfter: ", exchangeRateAfter); - - // vm.prank(batchPosterOperator, batchPosterOperator); - // seqInbox.addSequencerL2BatchFromOrigin(1, batchData, 0, IGasRefunder(feeTokenPricer), 0, 1); - - // uint256 ethAccEnd = tradeTracker.ethAccumulatorPerSpender(batchPosterOperator); - // uint256 tokenAccEnd = tradeTracker.tokenAccumulatorPerSpender(batchPosterOperator); - // vm.prank(batchPosterOperator, batchPosterOperator); - // uint256 exchangeRateEnd = tradeTracker.getExchangeRate(); - - // console.log("-----------"); - // console.log("ethAccEnd: ", ethAccEnd); - // console.log("tokenAccEnd: ", tokenAccEnd); - // console.log("exchangeRateEnd: ", exchangeRateEnd); - // } - - function _swapTokenToEth( - uint256 tokenAmount, - uint256 minEthReceived - ) internal returns (uint256 ethReceived) { - deal(USDC_ARB1, batchPosterOperator, tokenAmount); - - vm.startPrank(batchPosterOperator, batchPosterOperator); - IERC20(USDC_ARB1).approve(address(tradeTracker), tokenAmount); - ethReceived = - tradeTracker.swapTokenToEth(tokenAmount, minEthReceived, block.timestamp + 100); - vm.stopPrank(); - } - - function _deployFeeTokenRollup() internal returns (SequencerInbox, ERC20Bridge) { - RollupMock rollupMock = new RollupMock(owner); - ERC20Bridge bridgeImpl = new ERC20Bridge(); - address proxyAdmin = makeAddr("proxyAdmin"); - ERC20Bridge bridge = ERC20Bridge( - address(new TransparentUpgradeableProxy(address(bridgeImpl), proxyAdmin, "")) - ); - address nativeToken = address(new ERC20PresetMinterPauser("Appchain Token", "App")); - - bridge.initialize(IOwnable(address(rollupMock)), nativeToken); - vm.prank(owner); - bridge.setDelayedInbox(makeAddr("inbox"), true); - - /// this will result in 'hostChainIsArbitrum = true' - vm.mockCall( - address(100), - abi.encodeWithSelector(ArbSys.arbOSVersion.selector), - abi.encode(uint256(11)) - ); - uint256 maxDataSize = 10_000; - SequencerInbox seqInboxImpl = - new SequencerInbox(maxDataSize, IReader4844(address(0)), true, true); - SequencerInbox seqInbox = SequencerInbox( - address(new TransparentUpgradeableProxy(address(seqInboxImpl), proxyAdmin, "")) - ); - ISequencerInbox.MaxTimeVariation memory maxTimeVariation = ISequencerInbox.MaxTimeVariation({ - delayBlocks: 10, - futureBlocks: 10, - delaySeconds: 100, - futureSeconds: 100 - }); - BufferConfig memory bufferConfigDefault = BufferConfig({ - threshold: type(uint64).max, - max: type(uint64).max, - replenishRateInBasis: 714 - }); - seqInbox.initialize( - bridge, maxTimeVariation, bufferConfigDefault, IFeeTokenPricer(tradeTracker) - ); - - vm.prank(owner); - seqInbox.setIsBatchPoster(batchPosterOperator, true); - - vm.prank(owner); - bridge.setSequencerInbox(address(seqInbox)); - - return (seqInbox, bridge); - } -} - -contract RollupMock { - address public immutable owner; - - constructor( - address _owner - ) { - owner = _owner; - } -} From b7a86ac84916b3ef6b14ea093410f84cf2cf7921 Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Fri, 20 Dec 2024 11:24:15 +0000 Subject: [PATCH 35/36] Updated readme and comments --- .../ConstantExchangeRatePricer.sol | 8 ++++---- .../OwnerAdjustableExchangeRatePricer.sol | 7 +++---- test/foundry/fee-token-pricers/README.md | 20 ++++++++++++------- .../uniswap-v2-twap/UniswapV2TwapPricer.sol | 6 +++--- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol b/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol index 1160c9c9d..e2ebee411 100644 --- a/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol +++ b/test/foundry/fee-token-pricers/ConstantExchangeRatePricer.sol @@ -3,10 +3,10 @@ pragma solidity ^0.8.0; import {IFeeTokenPricer} from "../../../src/bridge/ISequencerInbox.sol"; -/** - * @title Test implementation of a fee token pricer that returns a constant exchange rate - * @notice Exchange rate is set in constructor and cannot be changed - */ +// 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; diff --git a/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol b/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol index 65137a3b0..c8713b5d9 100644 --- a/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol +++ b/test/foundry/fee-token-pricers/OwnerAdjustableExchangeRatePricer.sol @@ -4,10 +4,9 @@ pragma solidity ^0.8.0; import {IFeeTokenPricer} from "../../../src/bridge/ISequencerInbox.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -/** - * @title Test implementation of a fee token pricer that returns an exchange rate set by the owner - * @notice Exchange rate can be changed by the owner at any time, without any restrictions - */ +/// @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; diff --git a/test/foundry/fee-token-pricers/README.md b/test/foundry/fee-token-pricers/README.md index 2823d9b1b..c1305fce2 100644 --- a/test/foundry/fee-token-pricers/README.md +++ b/test/foundry/fee-token-pricers/README.md @@ -7,22 +7,28 @@ When the batch poster posts data to the parent chain a batch spending report is 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 options -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 options 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. +## 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. -### Option 1 - Chain owner defined oracle +### 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. -### Option 2 - External oracle +### 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). -### Option 3 - Exchange rate tracking +### 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 forced to honestly report the correct price, but instead that the batch poster can be sure that they'll be refunded the correct amount. +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). \ No newline at end of file +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. \ No newline at end of file diff --git a/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol index b24318fad..e8a7acc98 100644 --- a/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol +++ b/test/foundry/fee-token-pricers/uniswap-v2-twap/UniswapV2TwapPricer.sol @@ -5,9 +5,9 @@ import {IFeeTokenPricer} from "../../../../src/bridge/ISequencerInbox.sol"; import {FixedPoint} from "./FixedPoint.sol"; import {IUniswapV2Pair} from "@uniswap/v2-core/interfaces/IUniswapV2Pair.sol"; -/** - * @title Test implementation of a fee token pricer that uses Uniswap V2 TWAP - */ +/// @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 *; From b757f1730ace7457e758c5f24adc46c488960d3c Mon Sep 17 00:00:00 2001 From: Chris Buckland Date: Fri, 20 Dec 2024 11:59:04 +0000 Subject: [PATCH 36/36] Formatting --- test/foundry/fee-token-pricers/README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/foundry/fee-token-pricers/README.md b/test/foundry/fee-token-pricers/README.md index c1305fce2..7f5c1177a 100644 --- a/test/foundry/fee-token-pricers/README.md +++ b/test/foundry/fee-token-pricers/README.md @@ -3,32 +3,39 @@ 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). +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. \ No newline at end of file + +- **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.