diff --git a/.github/workflows/forge-test.yaml b/.github/workflows/forge-test.yaml index 2b31fa3..f8b79f0 100644 --- a/.github/workflows/forge-test.yaml +++ b/.github/workflows/forge-test.yaml @@ -7,7 +7,7 @@ jobs: fail-fast: true runs-on: ubuntu-latest env: - RPC_URL_POLYGON: ${{ secrets. RPC_URL_POLYGON }} + RPC_URL_POLYGON: ${{ secrets. RPC_URL_POLYGON}} steps: - uses: actions/checkout@v4 with: @@ -18,7 +18,9 @@ jobs: uses: DeterminateSystems/nix-installer-action@v4 - uses: DeterminateSystems/magic-nix-cache-action@v2 + - run: nix run .#uniswap-prelude - run: nix run .#rainix-sol-prelude - run: nix run .#i9r-prelude - working-directory: lib/rain.interpreter - - run: nix run .#rainix-sol-test \ No newline at end of file + working-directory: lib/rain.orderbook/lib/rain.interpreter + - run: mkdir -p test/csvs + - run: nix run .#rainix-sol-test diff --git a/.gitignore b/.gitignore index 995dbc2..ad302f5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ out cache broadcast .env +test/csvs/* \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 3d92788..83c3e9f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,15 @@ [submodule "lib/rain.intorastring"] path = lib/rain.intorastring url = https://github.com/rainlanguage/rain.intorastring -[submodule "lib/rain.interpreter"] - path = lib/rain.interpreter - url = https://github.com/rainlanguage/rain.interpreter +[submodule "lib/rain.orderbook"] + path = lib/rain.orderbook + url = https://github.com/rainlanguage/rain.orderbook.git +[submodule "lib/rain.uniswap"] + path = lib/rain.uniswap + url = https://github.com/rainlanguage/rain.uniswap.git +[submodule "lib/h20.pubstrats"] + path = lib/h20.pubstrats + url = https://github.com/h20liquidity/h20.pubstrats.git +[submodule "lib/view-quoter-v3"] + path = lib/view-quoter-v3 + url = https://github.com/Uniswap/view-quoter-v3 diff --git a/flake.lock b/flake.lock index b5dfc73..7711e00 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -95,11 +95,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1705309234, - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "lastModified": 1709126324, + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", "owner": "numtide", "repo": "flake-utils", - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "rev": "d465f4819400de7c8d874d50b982301f28a84605", "type": "github" }, "original": { @@ -351,17 +351,16 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1706634492, - "narHash": "sha256-9wg1OQET7oCzzMsktGufJmfr2ylecL8T8YYqQo+qNSc=", + "lastModified": 1709953170, + "narHash": "sha256-No8j4LmDbFmYfh0NXapQDQeoGHXhvnwBkW1usamu8oQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "dad88c029e2644adfde882f73e9338fd39058a3f", + "rev": "de66856bf823415928efe2088c1cc125fce7fffc", "type": "github" }, "original": { "owner": "nixos", "repo": "nixpkgs", - "rev": "dad88c029e2644adfde882f73e9338fd39058a3f", "type": "github" } }, @@ -477,11 +476,11 @@ "rainix": "rainix_2" }, "locked": { - "lastModified": 1707161449, - "narHash": "sha256-NGBr8Vumu/fwGLbgiAxxtdEAadN8j3IGYmb578r4NIY=", + "lastModified": 1709330445, + "narHash": "sha256-fNBo/pmPOkTbkKZRFnxR6QBQEk1QSL13iIdfPQN6jpg=", "owner": "rainlanguage", "repo": "rain.cli", - "rev": "98cc102f01bcbdab878a2e565198fb50ea470ff6", + "rev": "0f882121f9f26208c1ae062c368ee817b2ca1363", "type": "github" }, "original": { @@ -538,11 +537,11 @@ "rust-overlay": "rust-overlay_3" }, "locked": { - "lastModified": 1707161503, - "narHash": "sha256-u/k8zB4jslihIxLhRiELvzLzdqGgLijNDR+4v8uWOBY=", + "lastModified": 1710225842, + "narHash": "sha256-wfRe+fwM2pKQehO5WlRIv07dT8/IhyfDwWphcfk1x1w=", "owner": "rainprotocol", "repo": "rainix", - "rev": "dc1477c759bdebe4ee19931781b56d3e8988dbdc", + "rev": "1a927a908746931deb2c79c5cedb72f0f1b6f922", "type": "github" }, "original": { @@ -645,11 +644,11 @@ "nixpkgs": "nixpkgs_11" }, "locked": { - "lastModified": 1707099356, - "narHash": "sha256-ph483MDKLi9I/gndYOieVP41es633DOOmPjEI50x5KU=", + "lastModified": 1709950089, + "narHash": "sha256-JjZINymxtnDY9EDdxnVPideZvHPR2Cm/GdKAptCWLI4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "61dfa5a8129f7edbe9150253c68f673f87b16fb1", + "rev": "d3a05d053b145349b8ad395741c5951f332280ef", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 0623718..47e4b45 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,18 @@ let pkgs = rainix.pkgs.${system}; in { - packages = rainix.packages.${system}; + packages = rec { + uniswap-prelude = rainix.mkTask.${system} { + name = "uniswap-prelude"; + body = '' + set -euxo pipefail + + FOUNDRY_PROFILE=reference forge build --force + FOUNDRY_PROFILE=quoter forge build --force + ''; + additionalBuildInputs = rainix.sol-build-inputs.${system}; + }; + } // rainix.packages.${system}; devShells = rainix.devShells.${system}; } ); diff --git a/foundry.toml b/foundry.toml index c753d31..d245b85 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,6 +5,9 @@ libs = ['lib'] # See more config options https://github.com/foundry-rs/foundry/tree/master/config +# Needed for dotrain compose +ffi = true + solc = "0.8.19" optimizer = true optimizer_runs = 100000 @@ -14,7 +17,45 @@ cbor_metadata = false # Build metadata used for testing rain meta aware contracts in this folder rather # than expose ffi to forge. -fs_permissions = [{ access = "read", path = "./meta"}] +fs_permissions = [ + { access = "read", path = "out/reference/UniswapV2LibraryConcrete.sol/UniswapV2LibraryConcrete.json" }, + { access = "read", path = "out/quoter/Quoter.sol/Quoter.json" }, + { access = "read", path = "./lib/rain.orderbook/lib/rain.interpreter/meta/RainterpreterExpressionDeployerNPE2.rain.meta"}, + { access = "read-write", path = "test/csvs"} +] + +remappings = [ + "rain.interpreter/=lib/rain.orderbook/lib/rain.interpreter/src", + "rain.metadata/=lib/rain.orderbook/lib/rain.metadata/src" +] + [fuzz] runs = 1024 + +[profile.reference] + +solc = "0.6.6" +src = "reference/src" +test = 'reference/test' +script = 'reference/script' +out = "out/reference/" +libs = ["reference/lib"] +remappings = [ + "v2-core/=reference/lib/v2-core/", + "v2-periphery/=reference/lib/v2-periphery/", + "@uniswap/v2-core/=reference/lib/v2-core/", + "@uniswap/v2-periphery/=reference/lib/v2-periphery/" +] + +[profile.quoter] + +solc = "0.7.6" +src = "lib/view-quoter-v3/contracts" +test = "lib/view-quoter-v3/test" +script = "lib/view-quoter-v3/script" +out = "out/quoter/" +libs = ["lib/view-quoter-v3/lib"] +remappings = [ + "v3-periphery/=lib/view-quoter-v3/lib/v3-periphery/", +] \ No newline at end of file diff --git a/lib/h20.pubstrats b/lib/h20.pubstrats new file mode 160000 index 0000000..4d75dba --- /dev/null +++ b/lib/h20.pubstrats @@ -0,0 +1 @@ +Subproject commit 4d75dbad007907064500934d4a0f00039b8c7af0 diff --git a/lib/rain.interpreter b/lib/rain.interpreter deleted file mode 160000 index 8206727..0000000 --- a/lib/rain.interpreter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 820672713e0c607f4f11e9d50525c8cce096ca46 diff --git a/lib/rain.intorastring b/lib/rain.intorastring index 58fa175..d554a28 160000 --- a/lib/rain.intorastring +++ b/lib/rain.intorastring @@ -1 +1 @@ -Subproject commit 58fa175b63fd475db51105590f899136e606c583 +Subproject commit d554a28725fad16cb72402dc175b91ca41a41d18 diff --git a/lib/rain.orderbook b/lib/rain.orderbook new file mode 160000 index 0000000..fc241e4 --- /dev/null +++ b/lib/rain.orderbook @@ -0,0 +1 @@ +Subproject commit fc241e4a5886eaece71cb82df3a1994b6de3e9a3 diff --git a/lib/rain.uniswap b/lib/rain.uniswap new file mode 160000 index 0000000..c71ab24 --- /dev/null +++ b/lib/rain.uniswap @@ -0,0 +1 @@ +Subproject commit c71ab2465c26064453f11dde942d89f80d56f740 diff --git a/lib/view-quoter-v3 b/lib/view-quoter-v3 new file mode 160000 index 0000000..0649670 --- /dev/null +++ b/lib/view-quoter-v3 @@ -0,0 +1 @@ +Subproject commit 064967017bd1a0df11e9d71ff024b29584aafaa5 diff --git a/src/SushiV2UniV3TwapTrendStrat.rain b/src/SushiV2UniV3TwapTrendStrat.rain deleted file mode 100644 index ea63393..0000000 --- a/src/SushiV2UniV3TwapTrendStrat.rain +++ /dev/null @@ -1,92 +0,0 @@ -#trade-token-address 0x692AC1e363ae34b6B489148152b12e2785a3d8d6 -#trade-token-decimals 18 -#usdt-token-address 0xc2132D05D31c914a87C6611C10748AEb04B58e8F -#usdt-token-decimals 6 -#mean-amount 166666666666666666666 -#mean-cooldown 1440e18 -#randomized-seed 0x844298f03374ebab272d6aea77dd06a67ca29d81adbe996adfd748ae279abd97 -#initial-tracker-value 0x5555555555555555555555555555555555555555555555555555555555555555 -#bounty 8e16 - -#twap-long-time 14400 -#twap-short-time 1800 -#skew-factor 3e17 - -#ensure-cooldown - last-time target-cooldown18:, - cooldown-random-multiplier18: call<'jittery-binomial 1>(hash(last-time)), - cooldown18: decimal18-mul(decimal18-mul(target-cooldown18 2e18) cooldown-random-multiplier18), - cooldown: decimal18-scale-n<0>(cooldown18), - :ensure<1>(less-than(int-add(last-time cooldown) block-timestamp())); - -#jittery-binomial - input:, - binomial18-10: decimal18-scale18<0>(bitwise-count-ones(bitwise-decode<0 10>(hash(input)))), - noise18-1: int-mod(hash(input 0) 1e18), - jittery-11: decimal18-add(binomial18-10 noise18-1), - jittery-1: decimal18-div(jittery-11 11e18); - -#target-usdt - last-time target-usdt18:, - amount-random-multiplier18: call<'jittery-binomial 1>(last-time), - amount-usdt18: decimal18-mul(decimal18-mul(target-usdt18 2e18) amount-random-multiplier18), - amount-usdt6: decimal18-scale-n<6>(amount-usdt18); - -#prelude - last-time: get(order-hash()), - :set( - order-hash() - block-timestamp() - ), - :call<'ensure-cooldown 0>(last-time mean-cooldown), - jittered-amount-usdt18 jittered-amount-usdt6: call<'target-usdt 2>(last-time mean-amount), - short-twap-output-ratio: uniswap-v3-twap-output-ratio( - usdt-token-address usdt-token-decimals - trade-token-address trade-token-decimals - twap-short-time 0 - [uniswap-v3-fee-high] - ), - long-twap-output-ratio: uniswap-v3-twap-output-ratio( - usdt-token-address usdt-token-decimals - trade-token-address trade-token-decimals - twap-long-time 0 - [uniswap-v3-fee-high] - ); - -#sell-order-calculate-io - jittered-amount-usdt18 _ - short-twap-output-ratio - long-twap-output-ratio: call<'prelude 4>(), - sell-ratio: decimal18-div(short-twap-output-ratio long-twap-output-ratio), - skewed-sell-ratio: if( - less-than(sell-ratio 1e18) - decimal18-power(sell-ratio decimal18-sub(1e18 skew-factor)) - decimal18-power(sell-ratio decimal18-add(1e18 skew-factor)) - ), - amount-usdt18: decimal18-mul(jittered-amount-usdt18 skewed-sell-ratio), - amount-usdt6: decimal18-scale-n<6>(amount-usdt18), - trade-amount18: uniswap-v2-quote-exact-output(trade-token-address usdt-token-address amount-usdt6), - order-output-max18: trade-amount18, - io-ratio: decimal18-div(decimal18-sub(amount-usdt18 bounty) order-output-max18); - -#sell-order-handle-io - :ensure<5>(greater-than-or-equal-to(output-vault-balance-decrease() calculated-max-output())); - -#buy-order-calculate-io - jittered-amount-usdt18 _ - short-twap-output-ratio - long-twap-output-ratio: call<'prelude 4>(), - buy-ratio: decimal18-div(long-twap-output-ratio short-twap-output-ratio), - skewed-buy-ratio: if( - less-than(buy-ratio 1e18) - decimal18-power(buy-ratio decimal18-add(1e18 skew-factor)) - decimal18-power(buy-ratio decimal18-sub(1e18 skew-factor)) - ), - amount-usdt18: decimal18-mul(jittered-amount-usdt18 skewed-buy-ratio), - amount-usdt6: decimal18-scale-n<6>(amount-usdt18), - trade-amount18: uniswap-v2-quote-exact-input(usdt-token-address trade-token-address amount-usdt6), - order-output-max18: decimal18-add(amount-usdt18 bounty), - io-ratio: decimal18-div(trade-amount18 order-output-max18); - -#buy-order-handle-io - :ensure<9>(greater-than-or-equal-to(output-vault-balance-decrease() decimal18-scale-n<6>(calculated-max-output()))); diff --git a/src/TrancheMirror.sol b/src/TrancheMirror.sol new file mode 100644 index 0000000..2d52cd2 --- /dev/null +++ b/src/TrancheMirror.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.19; + +import {Vm} from "forge-std/Vm.sol"; +import {IRouteProcessor} from "src/interface/IRouteProcessor.sol"; +import {SafeERC20, IERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import { + IOrderBookV3, + IO, + OrderV2, + OrderConfigV2, + TakeOrderConfigV2, + TakeOrdersConfigV2 +} from "rain.orderbook/src/interface/unstable/IOrderBookV3.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; + +// Strategy Params +uint256 constant TRANCHE_RESERVE_BASE_AMOUNT = 1000e18 ; +uint256 constant TRANCHE_RESERVE_BASE_IO_RATIO = 315e18; +uint256 constant SPREAD_RATIO = 101e16; +uint256 constant TRANCHE_EDGE_THRESHOLD = 2e17; +uint256 constant INITIAL_TRANCHE_SPACE = 0; +uint256 constant TRANCHE_SPACE_SNAP_THRESHOLD = 1e12; + + +/// @dev https://polygonscan.com/address/0xE7eb31f23A5BefEEFf76dbD2ED6AdC822568a5d2 +IRouteProcessor constant ROUTE_PROCESSOR = IRouteProcessor(address(0xE7eb31f23A5BefEEFf76dbD2ED6AdC822568a5d2)); + +uint256 constant VAULT_ID = uint256(keccak256("vault")); + +// IEON token holder. +address constant POLYGON_IEON_HOLDER = 0xd6756f5aF54486Abda6bd9b1eee4aB0dBa7C3ef2; +// USDT token holder. +address constant POLYGON_USDT_HOLDER = 0xF977814e90dA44bFA03b6295A0616a897441aceC; +// Wrapped native token holder. +address constant POLYGON_WETH_HOLDER = 0xbAd24a42b621eED9033409736219c01bF0d8500F; + +address constant POLYGON_IEON_ADMIN = 0x3a7bD65AB95678eB2A3a8d37962E89f42a6968c7; + + +/// @dev https://polygonscan.com/address/0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32 +address constant UNI_V2_FACTORY = 0x5757371414417b8C6CAad45bAeF941aBc7d3Ab32; + +/// @dev https://polygonscan.com/address/0xd0e9c8f5Fae381459cf07Ec506C1d2896E8b5df6 +IERC20 constant IEON_TOKEN = IERC20(0xd0e9c8f5Fae381459cf07Ec506C1d2896E8b5df6); + +/// @dev https://polygonscan.com/address/0xc2132D05D31c914a87C6611C10748AEb04B58e8F +IERC20 constant USDT_TOKEN = IERC20(0xc2132D05D31c914a87C6611C10748AEb04B58e8F); + +/// @dev https://polygonscan.com/address/0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270 +IERC20 constant WETH_TOKEN = IERC20(0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270); + +/// @dev https://docs.sushi.com/docs/Products/Classic%20AMM/Deployment%20Addresses +address constant POLYGON_SUSHI_V2_ROUTER = 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506; + +function getUniV3TradeSellRoute(address toAddress) pure returns (bytes memory){ + bytes memory ROUTE_PRELUDE = + hex"02d0e9c8f5fae381459cf07ec506c1d2896e8b5df601ffff00316bc12871c807020ef8c1bc7771061c4e7a04ed00"; + return abi.encode(bytes.concat(ROUTE_PRELUDE, abi.encodePacked(toAddress))); +} + +function getUniV3TradeBuyRoute(address toAddress) pure returns (bytes memory){ + bytes memory ROUTE_PRELUDE = + hex"020d500b1d8e8ef31e21c99d1db9a6444d3adf127001ffff00316bc12871c807020ef8c1bc7771061c4e7a04ed01"; + return abi.encode(bytes.concat(ROUTE_PRELUDE, abi.encodePacked(toAddress))); +} + +function polygonIeonIo() pure returns (IO memory) { + return IO(address(IEON_TOKEN), 18, VAULT_ID); +} + +function polygonWethIo() pure returns (IO memory) { + return IO(address(WETH_TOKEN), 18, VAULT_ID); +} + +library LibTrancheSpreadOrders { + using Strings for address; + using Strings for uint256; + + + function getTrancheTestSpreadOrder( + Vm vm, + address orderBookSubparser, + uint256 testTrancheSpace, + uint256 spreadRatio + ) + internal + returns (bytes memory trancheRefill) + { + string[] memory ffi = new string[](35); + ffi[0] = "rain"; + ffi[1] = "dotrain"; + ffi[2] = "compose"; + ffi[3] = "-i"; + ffi[4] = "lib/h20.pubstrats/src/tranche-spread.rain"; + ffi[5] = "--entrypoint"; + ffi[6] = "calculate-io"; + ffi[7] = "--entrypoint"; + ffi[8] = "handle-io"; + ffi[9] = "--bind"; + ffi[10] = "distribution-token=0xd0e9c8f5Fae381459cf07Ec506C1d2896E8b5df6"; + ffi[11] = "--bind"; + ffi[12] = "reserve-token=0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270"; + ffi[13] = "--bind"; + ffi[14] = "get-tranche-space='get-test-tranche-space"; + ffi[15] = "--bind"; + ffi[16] = "set-tranche-space='set-test-tranche-space"; + ffi[17] = "--bind"; + ffi[18] = string.concat("test-tranche-space=", testTrancheSpace.toString()); + ffi[19] = "--bind"; + ffi[20] = "tranche-reserve-amount-growth='tranche-reserve-amount-growth-constant"; + ffi[21] = "--bind"; + ffi[22] = string.concat("tranche-reserve-amount-base=", TRANCHE_RESERVE_BASE_AMOUNT.toString()); + ffi[23] = "--bind"; + ffi[24] = "tranche-reserve-io-ratio-growth='tranche-reserve-io-ratio-linear"; + ffi[25] = "--bind"; + ffi[26] = string.concat("tranche-reserve-io-ratio-base=", TRANCHE_RESERVE_BASE_IO_RATIO.toString()); + ffi[27] = "--bind"; + ffi[28] = string.concat("spread-ratio=", spreadRatio.toString()); + ffi[29] = "--bind"; + ffi[30] = string.concat("tranche-space-edge-guard-threshold=", TRANCHE_EDGE_THRESHOLD.toString()); + ffi[31] = "--bind"; + ffi[32] = string.concat("initial-tranche-space=", INITIAL_TRANCHE_SPACE.toString()); + ffi[33] = "--bind"; + ffi[34] = string.concat("tranche-space-snap-threshold=", TRANCHE_SPACE_SNAP_THRESHOLD.toString()); + + + trancheRefill = bytes.concat(getSubparserPrelude(orderBookSubparser), vm.ffi(ffi)); + } + + function getTrancheSpreadOrder( + Vm vm, + address orderBookSubparser + ) + internal + returns (bytes memory trancheRefill) + { + string[] memory ffi = new string[](33); + ffi[0] = "rain"; + ffi[1] = "dotrain"; + ffi[2] = "compose"; + ffi[3] = "-i"; + ffi[4] = "lib/h20.pubstrats/src/tranche-spread.rain"; + ffi[5] = "--entrypoint"; + ffi[6] = "calculate-io"; + ffi[7] = "--entrypoint"; + ffi[8] = "handle-io"; + ffi[9] = "--bind"; + ffi[10] = "distribution-token=0xd0e9c8f5Fae381459cf07Ec506C1d2896E8b5df6"; + ffi[11] = "--bind"; + ffi[12] = "reserve-token=0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270"; + ffi[13] = "--bind"; + ffi[14] = "get-tranche-space='get-real-tranche-space"; + ffi[15] = "--bind"; + ffi[16] = "set-tranche-space='set-real-tranche-space"; + ffi[17] = "--bind"; + ffi[18] = "tranche-reserve-amount-growth='tranche-reserve-amount-growth-constant"; + ffi[19] = "--bind"; + ffi[20] = string.concat("tranche-reserve-amount-base=", TRANCHE_RESERVE_BASE_AMOUNT.toString()); + ffi[21] = "--bind"; + ffi[22] = "tranche-reserve-io-ratio-growth='tranche-reserve-io-ratio-linear"; + ffi[23] = "--bind"; + ffi[24] = string.concat("tranche-reserve-io-ratio-base=", TRANCHE_RESERVE_BASE_IO_RATIO.toString()); + ffi[25] = "--bind"; + ffi[26] = string.concat("spread-ratio=", SPREAD_RATIO.toString()); + ffi[27] = "--bind"; + ffi[28] = string.concat("tranche-space-edge-guard-threshold=", TRANCHE_EDGE_THRESHOLD.toString()); + ffi[29] = "--bind"; + ffi[30] = string.concat("initial-tranche-space=", INITIAL_TRANCHE_SPACE.toString()); + ffi[31] = "--bind"; + ffi[32] = string.concat("tranche-space-snap-threshold=", TRANCHE_SPACE_SNAP_THRESHOLD.toString()); + + + trancheRefill = bytes.concat(getSubparserPrelude(orderBookSubparser), vm.ffi(ffi)); + } + + function getSubparserPrelude(address obSubparser) internal pure returns (bytes memory) { + bytes memory RAINSTRING_OB_SUBPARSER = + bytes(string.concat("using-words-from ", obSubparser.toHexString(), " ")); + return RAINSTRING_OB_SUBPARSER; + } +} + diff --git a/src/TwapTrend.sol b/src/TwapTrend.sol deleted file mode 100644 index a128f17..0000000 --- a/src/TwapTrend.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: CAL -pragma solidity =0.8.19; - -import "src/interface/IOrderBookV3ArbOrderTaker.sol"; -import {RainterpreterExpressionDeployerNPE2} from - "rain.interpreter/src/concrete/RainterpreterExpressionDeployerNPE2.sol"; -import {RainterpreterParserNPE2} from "rain.interpreter/src/concrete/RainterpreterParserNPE2.sol"; -import { - SourceIndexV2, - IInterpreterV2, - IInterpreterStoreV1 -} from "rain.interpreter/src/interface/unstable/IInterpreterV2.sol"; -import {EvaluableConfigV3} from "rain.interpreter/src/interface/IInterpreterCallerV2.sol"; -import {IRouteProcessor} from "src/interface/IRouteProcessor.sol"; -import {SafeERC20, IERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -/// @dev https://polygonscan.com/address/0x0a6e511Fe663827b9cA7e2D2542b20B37fC217A6 -IRouteProcessor constant ROUTE_PROCESSOR = IRouteProcessor(address(0x0a6e511Fe663827b9cA7e2D2542b20B37fC217A6)); - -uint256 constant VAULT_ID = uint256(keccak256("vault")); - -// IEON token holder. -address constant POLYGON_IEON_HOLDER = 0xd6756f5aF54486Abda6bd9b1eee4aB0dBa7C3ef2; -// USDT token holder. -address constant POLYGON_USDT_HOLDER = 0xF977814e90dA44bFA03b6295A0616a897441aceC; - -/// @dev https://docs.sushi.com/docs/Products/Classic%20AMM/Deployment%20Addresses -/// @dev https://polygonscan.com/address/0xc35DADB65012eC5796536bD9864eD8773aBc74C4 -address constant POLYGON_SUSHI_V2_FACTORY = 0xc35DADB65012eC5796536bD9864eD8773aBc74C4; - -/// @dev https://docs.sushi.com/docs/Products/Classic%20AMM/Deployment%20Addresses -address constant POLYGON_SUSHI_V2_ROUTER = 0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506; - -/// @dev https://polygonscan.com/address/0xd0e9c8f5Fae381459cf07Ec506C1d2896E8b5df6 -IERC20 constant POLYGON_IEON_TOKEN_ADDRESS = IERC20(0xd0e9c8f5Fae381459cf07Ec506C1d2896E8b5df6); - -/// @dev https://polygonscan.com/address/0xc2132D05D31c914a87C6611C10748AEb04B58e8F -IERC20 constant POLYGON_USDT_TOKEN_ADDRESS = IERC20(0xc2132D05D31c914a87C6611C10748AEb04B58e8F); - -IOrderBookV3ArbOrderTaker constant POLYGON_ARB_CONTRACT = - IOrderBookV3ArbOrderTaker(0x0D7896d70FE84e88CC8e8BaDcB14D612Eee4Bbe0); - -address constant APPROVED_EOA = 0x669845c29D9B1A64FFF66a55aA13EB4adB889a88; -address constant APPROVED_COUNTERPARTY = address(POLYGON_ARB_CONTRACT); - -RainterpreterExpressionDeployerNPE2 constant POLYGON_DEPLOYER_NPE2 = - RainterpreterExpressionDeployerNPE2(0xE1E250a234aF6F343062873bf89c9D1a0a659c0b); - -RainterpreterParserNPE2 constant POLYGON_PARSER_NPE2 = - RainterpreterParserNPE2(0xc2D7890077F3EA75c2798D8624E1E0E6ef8C41e6); - -address constant POLYGON_INTERPRETER_NPE2 = 0xB7d691B7E3676cb70dB0cDae95797F24Eab6980D; -address constant POLYGON_STORE_NPE2 = 0x0b5a2b0aCFc5B52bf341FAD638B63C9A6f82dcb9; -IOrderBookV3 constant POLYGON_ORDERBOOK = IOrderBookV3(0xDE5aBE2837bc042397D80E37fb7b2C850a8d5a6C); -address constant POLYGON_ORDERBOOKSUBPARSER = 0x8A99456dD0E1CaA187CF6B779cA42EFE94E9C42b; -address constant UNISWAP_WORDS = 0xd97e8e581393055521F813D6889CfcCEDF7847C6; - -function polygonIeonIo() pure returns (IO memory) { - return IO(address(POLYGON_IEON_TOKEN_ADDRESS), 18, VAULT_ID); -} - -function polygonIeonIo() pure returns (IO memory) { - return IO(address(POLYGON_USDT_TOKEN_ADDRESS), 6, VAULT_ID); -} - diff --git a/src/abstract/RainContracts.sol b/src/abstract/RainContracts.sol new file mode 100644 index 0000000..fad94a9 --- /dev/null +++ b/src/abstract/RainContracts.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: CAL +pragma solidity ^0.8.19; + +import {Vm} from "forge-std/Vm.sol"; +import {console2} from "forge-std/console2.sol"; +import {RainterpreterParserNPE2} from "rain.orderbook/lib/rain.interpreter/src/concrete/RainterpreterParserNPE2.sol"; +import {RainterpreterStoreNPE2} from "rain.orderbook/lib/rain.interpreter/src/concrete/RainterpreterStoreNPE2.sol"; +import {RainterpreterNPE2} from "rain.orderbook/lib/rain.interpreter/src/concrete/RainterpreterNPE2.sol"; +import { + RainterpreterExpressionDeployerNPE2, + RainterpreterExpressionDeployerNPE2ConstructionConfig +} from "rain.orderbook/lib/rain.interpreter/src/concrete/RainterpreterExpressionDeployerNPE2.sol"; +import {IParserV1} from "rain.orderbook/lib/rain.interpreter/src/interface/IParserV1.sol"; +import {IInterpreterStoreV1} from "rain.orderbook/lib/rain.interpreter/src/interface/IInterpreterStoreV1.sol"; +import {IInterpreterStoreV2} from "rain.orderbook/lib/rain.interpreter/src/interface/unstable/IInterpreterStoreV2.sol"; +import { + IInterpreterV2, + SourceIndexV2 +} from "rain.orderbook/lib/rain.interpreter/src/interface/unstable/IInterpreterV2.sol"; +import {IExpressionDeployerV3} from + "rain.orderbook/lib/rain.interpreter/src/interface/unstable/IExpressionDeployerV3.sol"; +import {OrderBook} from "rain.orderbook/src/concrete/ob/OrderBook.sol"; +import {ISubParserV2} from "rain.orderbook/lib/rain.interpreter/src/interface/unstable/ISubParserV2.sol"; +import {OrderBookSubParser} from "rain.orderbook/src/concrete/parser/OrderBookSubParser.sol"; +import {UniswapWords} from "rain.uniswap/src/concrete/UniswapWords.sol"; +import {RouteProcessorOrderBookV3ArbOrderTaker} from + "rain.orderbook/src/concrete/arb/RouteProcessorOrderBookV3ArbOrderTaker.sol"; +import {IOrderBookV3ArbOrderTaker} from "rain.orderbook/src/interface/unstable/IOrderBookV3ArbOrderTaker.sol"; +import {EvaluableConfigV3, SignedContextV1} from "rain.interpreter/interface/IInterpreterCallerV2.sol"; +import {OrderBookV3ArbOrderTakerConfigV1} from "rain.orderbook/src/abstract/OrderBookV3ArbOrderTaker.sol"; +import {ICloneableFactoryV2} from "src/interface/ICloneableFactoryV2.sol"; +import {IOrderBookV3, ROUTE_PROCESSOR} from "src/TrancheMirror.sol"; +import "rain.uniswap/src/lib/v3/LibDeploy.sol"; + +abstract contract RainContracts { + IParserV1 public PARSER; + IInterpreterV2 public INTERPRETER; + IInterpreterStoreV2 public STORE; + IExpressionDeployerV3 public EXPRESSION_DEPLOYER; + IOrderBookV3 public ORDERBOOK; + ISubParserV2 public ORDERBOOK_SUPARSER; + ISubParserV2 public UNISWAP_WORDS; + IOrderBookV3ArbOrderTaker public ARB_IMPLEMENTATION; + IOrderBookV3ArbOrderTaker public ARB_INSTANCE; + ICloneableFactoryV2 public CLONE_FACTORY; + + function deployParser() public { + PARSER = new RainterpreterParserNPE2(); + } + + function deployStore() public { + STORE = new RainterpreterStoreNPE2(); + } + + function deployInterpreter() public { + INTERPRETER = new RainterpreterNPE2(); + } + + function deployExpressionDeployer(Vm vm, address interpreter, address store, address parser) public { + bytes memory constructionMeta = vm.readFileBinary( + "lib/rain.orderbook/lib/rain.interpreter/meta/RainterpreterExpressionDeployerNPE2.rain.meta" + ); + + EXPRESSION_DEPLOYER = new RainterpreterExpressionDeployerNPE2( + RainterpreterExpressionDeployerNPE2ConstructionConfig( + address(interpreter), address(store), address(parser), constructionMeta + ) + ); + } + + function deployOrderBook() public { + ORDERBOOK = new OrderBook(); + } + + function deployOrderBookSubparser() public { + ORDERBOOK_SUPARSER = new OrderBookSubParser(); + } + + function deployUniswapWords(Vm vm) public { + UNISWAP_WORDS = LibDeploy.newUniswapWords(vm); + } + + function deployArbInstance(Vm vm, address cloneFactory) public { + ARB_IMPLEMENTATION = new RouteProcessorOrderBookV3ArbOrderTaker(); + CLONE_FACTORY = ICloneableFactoryV2(cloneFactory); + + address ARB_INSTANCE_ADDRESS; + { + bytes memory ungatedArbExpression = ""; + (bytes memory bytecode, uint256[] memory constants) = PARSER.parse(ungatedArbExpression); + bytes memory implementationData = abi.encode(address(ROUTE_PROCESSOR)); + EvaluableConfigV3 memory evaluableConfig = EvaluableConfigV3(EXPRESSION_DEPLOYER, bytecode, constants); + OrderBookV3ArbOrderTakerConfigV1 memory cloneConfig = + OrderBookV3ArbOrderTakerConfigV1(address(ORDERBOOK), evaluableConfig, implementationData); + bytes memory encodedConfig = abi.encode(cloneConfig); + + vm.recordLogs(); + CLONE_FACTORY.clone(address(ARB_IMPLEMENTATION), encodedConfig); + Vm.Log[] memory entries = vm.getRecordedLogs(); + for (uint256 j = 0; j < entries.length; j++) { + if (entries[j].topics[0] == keccak256("NewClone(address,address,address)")) { + (,, ARB_INSTANCE_ADDRESS) = abi.decode(entries[j].data, (address, address, address)); + } + } + ARB_INSTANCE = IOrderBookV3ArbOrderTaker(ARB_INSTANCE_ADDRESS); + } + } +} diff --git a/src/interface/ICloneableFactoryV2.sol b/src/interface/ICloneableFactoryV2.sol new file mode 100644 index 0000000..b685b38 --- /dev/null +++ b/src/interface/ICloneableFactoryV2.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: CAL +pragma solidity ^0.8.18; + +/// @title ICloneableFactoryV2 +/// @notice A minimal interface to create proxy clones of a reference bytecode +/// and emit events so that indexers can discover it. `ICloneableFactoryV2` knows +/// nothing about the contracts that it clones, instead relying only on the +/// minimal `ICloneableV2` interface being implemented on the reference bytecode. +interface ICloneableFactoryV2 { + /// Emitted upon each `clone`. + /// @param sender The `msg.sender` that called `clone`. + /// @param implementation The reference bytecode to clone as a proxy. + /// @param clone The address of the new proxy contract. + event NewClone(address sender, address implementation, address clone); + + /// Clones an implementation using a proxy. EIP1167 proxy is recommended but + /// the exact cloning procedure is not specified by this interface. + /// + /// The factory MUST call `ICloneableV2.initialize` atomically with the + /// cloning process and MUST NOT call any other functions on the cloned proxy + /// before `initialize` completes successfully. The factory MUST ONLY + /// consider the clone to be successfully created if `initialize` returns the + /// keccak256 hash of the string "ICloneableV2.initialize". + /// + /// MUST emit `NewClone` with the implementation and clone address. + /// + /// @param implementation The contract to clone. + /// @param data As per `ICloneableV2`. + /// @return New child contract address. + function clone(address implementation, bytes calldata data) external returns (address); +} diff --git a/src/interface/IERC3156FlashBorrower.sol b/src/interface/IERC3156FlashBorrower.sol deleted file mode 100644 index c79c875..0000000 --- a/src/interface/IERC3156FlashBorrower.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: CC0 -// Alberto Cuesta Cañada, Fiona Kobayashi, fubuloubu, Austin Williams, "EIP-3156: Flash Loans," Ethereum Improvement Proposals, no. 3156, November 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3156. -pragma solidity ^0.8.18; - -/// @dev The ERC3156 spec mandates this hash be returned by `onFlashLoan` if it -/// succeeds. -bytes32 constant ON_FLASH_LOAN_CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan"); - -interface IERC3156FlashBorrower { - /** - * @dev Receive a flash loan. - * @param initiator The initiator of the loan. - * @param token The loan currency. - * @param amount The amount of tokens lent. - * @param fee The additional amount of tokens to repay. - * @param data Arbitrary data structure, intended to contain user-defined parameters. - * @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" - */ - function onFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) - external - returns (bytes32); -} diff --git a/src/interface/IERC3156FlashLender.sol b/src/interface/IERC3156FlashLender.sol deleted file mode 100644 index cbbe253..0000000 --- a/src/interface/IERC3156FlashLender.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: CC0 -// Alberto Cuesta Cañada, Fiona Kobayashi, fubuloubu, Austin Williams, "EIP-3156: Flash Loans," Ethereum Improvement Proposals, no. 3156, November 2020. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-3156. -pragma solidity ^0.8.18; - -import {IERC3156FlashBorrower} from "./IERC3156FlashBorrower.sol"; - -interface IERC3156FlashLender { - /** - * @dev The amount of currency available to be lent. - * @param token The loan currency. - * @return The amount of `token` that can be borrowed. - */ - function maxFlashLoan(address token) external view returns (uint256); - - /** - * @dev The fee to be charged for a given loan. - * @param token The loan currency. - * @param amount The amount of tokens lent. - * @return The amount of `token` to be charged for the loan, on top of the returned principal. - */ - function flashFee(address token, uint256 amount) external view returns (uint256); - - /** - * @dev Initiate a flash loan. - * @param receiver The receiver of the tokens in the loan, and the receiver of the callback. - * @param token The loan currency. - * @param amount The amount of tokens lent. - * @param data Arbitrary data structure, intended to contain user-defined parameters. - */ - function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) - external - returns (bool); -} diff --git a/src/interface/IOrderBookV2.sol b/src/interface/IOrderBookV2.sol deleted file mode 100644 index 2231943..0000000 --- a/src/interface/IOrderBookV2.sol +++ /dev/null @@ -1,524 +0,0 @@ -// SPDX-License-Identifier: CAL -pragma solidity ^0.8.18; - -import {IERC3156FlashLender} from "./IERC3156FlashLender.sol"; -import {EvaluableConfig, Evaluable} from "rain.interpreter/src/interface/deprecated/IInterpreterCallerV1.sol"; -import {SignedContextV1, IInterpreterCallerV2} from "rain.interpreter/src/interface/IInterpreterCallerV2.sol"; -import {IExpressionDeployerV2} from "rain.interpreter/src/interface/deprecated/IExpressionDeployerV2.sol"; - -/// Configuration for a deposit. All deposits are processed by and for -/// `msg.sender` so the vaults are unambiguous here. -/// @param token The token to deposit. -/// @param vaultId The vault ID for the token to deposit. -/// @param amount The amount of the token to deposit. -struct DepositConfig { - address token; - uint256 vaultId; - uint256 amount; -} - -/// Configuration for a withdrawal. All withdrawals are processed by and for -/// `msg.sender` so the vaults are unambiguous here. -/// @param token The token to withdraw. -/// @param vaultId The vault ID for the token to withdraw. -/// @param amount The amount of the token to withdraw. -struct WithdrawConfig { - address token; - uint256 vaultId; - uint256 amount; -} - -/// Configuration for a single input or output on an `Order`. -/// @param token The token to either send from the owner as an output or receive -/// from the counterparty to the owner as an input. The tokens are not moved -/// during an order, only internal vault balances are updated, until a separate -/// withdraw step. -/// @param decimals The decimals to use for internal scaling calculations for -/// `token`. This is provided directly in IO to save gas on external lookups and -/// to respect the ERC20 spec that mandates NOT assuming or using the `decimals` -/// method for onchain calculations. Ostensibly the decimals exists so that all -/// calculate order entrypoints can treat amounts and ratios as 18 decimal fixed -/// point values. Order max amounts MUST be rounded down and IO ratios rounded up -/// to compensate for any loss of precision during decimal rescaling. -/// @param vaultId The vault ID that tokens will move into if this is an input -/// or move out from if this is an output. -struct IO { - address token; - uint8 decimals; - uint256 vaultId; -} - -/// Config the order owner may provide to define their order. The `msg.sender` -/// that adds an order cannot modify the owner nor bypass the integrity check of -/// the expression deployer that they specify. However they MAY specify a -/// deployer with a corrupt integrity check, so counterparties and clearers MUST -/// check the DISpair of the order and avoid untrusted pairings. -/// @param validInputs As per `validInputs` on the `Order`. -/// @param validOutputs As per `validOutputs` on the `Order`. -/// @param evaluableConfig Standard `EvaluableConfig` used to produce the -/// `Evaluable` on the order. -/// @param meta Arbitrary bytes that will NOT be used in the order evaluation -/// but MUST be emitted as a Rain `MetaV1` when the order is placed so can be -/// used by offchain processes. -struct OrderConfig { - IO[] validInputs; - IO[] validOutputs; - EvaluableConfig evaluableConfig; - bytes meta; -} - -/// Defines a fully deployed order ready to evaluate by Orderbook. -/// @param owner The owner of the order is the `msg.sender` that added the order. -/// @param handleIO true if there is a "handle IO" entrypoint to run. If false -/// the order book MAY skip calling the interpreter to save gas. -/// @param evaluable Standard `Evaluable` with entrypoints for both -/// "calculate order" and "handle IO". The latter MAY be empty bytes, in which -/// case it will be skipped at runtime to save gas. -/// @param validInputs A list of input tokens that are economically equivalent -/// for the purpose of processing this order. Inputs are relative to the order -/// so these tokens will be sent to the owners vault. -/// @param validOutputs A list of output tokens that are economically equivalent -/// for the purpose of processing this order. Outputs are relative to the order -/// so these tokens will be sent from the owners vault. -struct Order { - address owner; - bool handleIO; - Evaluable evaluable; - IO[] validInputs; - IO[] validOutputs; -} - -/// Config for a list of orders to take sequentially as part of a `takeOrders` -/// call. -/// @param output Output token from the perspective of the order taker. -/// @param input Input token from the perspective of the order taker. -/// @param minimumInput Minimum input from the perspective of the order taker. -/// @param maximumInput Maximum input from the perspective of the order taker. -/// @param maximumIORatio Maximum IO ratio as calculated by the order being -/// taken. The input is from the perspective of the order so higher ratio means -/// worse deal for the order taker. -/// @param orders Ordered list of orders that will be taken until the limit is -/// hit. Takers are expected to prioritise orders that appear to be offering -/// better deals i.e. lower IO ratios. This prioritisation and sorting MUST -/// happen offchain, e.g. via. some simulator. -struct TakeOrdersConfig { - address output; - address input; - uint256 minimumInput; - uint256 maximumInput; - uint256 maximumIORatio; - TakeOrderConfig[] orders; -} - -/// Config for an individual take order from the overall list of orders in a -/// call to `takeOrders`. -/// @param order The order being taken this iteration. -/// @param inputIOIndex The index of the input token in `order` to match with the -/// take order output. -/// @param outputIOIndex The index of the output token in `order` to match with -/// the take order input. -/// @param signedContext Optional additional signed context relevant to the -/// taken order. -struct TakeOrderConfig { - Order order; - uint256 inputIOIndex; - uint256 outputIOIndex; - SignedContextV1[] signedContext; -} - -/// Additional config to a `clear` that allows two orders to be fully matched to -/// a specific token moment. Also defines the bounty for the clearer. -/// @param aliceInputIOIndex The index of the input token in order A. -/// @param aliceOutputIOIndex The index of the output token in order A. -/// @param bobInputIOIndex The index of the input token in order B. -/// @param bobOutputIOIndex The index of the output token in order B. -/// @param aliceBountyVaultId The vault ID that the bounty from order A should -/// move to for the clearer. -/// @param bobBountyVaultId The vault ID that the bounty from order B should move -/// to for the clearer. -struct ClearConfig { - uint256 aliceInputIOIndex; - uint256 aliceOutputIOIndex; - uint256 bobInputIOIndex; - uint256 bobOutputIOIndex; - uint256 aliceBountyVaultId; - uint256 bobBountyVaultId; -} - -/// Summary of the vault state changes due to clearing an order. NOT the state -/// changes sent to the interpreter store, these are the LOCAL CHANGES in vault -/// balances. Note that the difference in inputs/outputs overall between the -/// counterparties is the bounty paid to the entity that cleared the order. -/// @param aliceOutput Amount of counterparty A's output token that moved out of -/// their vault. -/// @param bobOutput Amount of counterparty B's output token that moved out of -/// their vault. -/// @param aliceInput Amount of counterparty A's input token that moved into -/// their vault. -/// @param bobInput Amount of counterparty B's input token that moved into their -/// vault. -struct ClearStateChange { - uint256 aliceOutput; - uint256 bobOutput; - uint256 aliceInput; - uint256 bobInput; -} - -/// @title IOrderBookV2 -/// @notice An orderbook that deploys _strategies_ represented as interpreter -/// expressions rather than individual orders. The order book contract itself -/// behaves similarly to an `ERC4626` vault but with much more fine grained -/// control over how tokens are allocated and moved internally by their owners, -/// and without any concept of "shares". Token owners MAY deposit and withdraw -/// their tokens under arbitrary vault IDs on a per-token basis, then define -/// orders that specify how tokens move between vaults according to an expression. -/// The expression returns a maximum amount and a token input/output ratio from -/// the perpective of the order. When two expressions intersect, as in their -/// ratios are the inverse of each other, then tokens can move between vaults. -/// -/// For example, consider order A with input TKNA and output TKNB with a constant -/// ratio of 100:1. This order in isolation has no ability to move tokens. If -/// an order B appears with input TKNB and output TKNA and a ratio of 1:100 then -/// this is a perfect match with order A. In this case 100 TKNA will move from -/// order B to order A and 1 TKNB will move from order A to order B. -/// -/// IO ratios are always specified as input:output and are 18 decimal fixed point -/// values. The maximum amount that can be moved in the current clearance is also -/// set by the order expression as an 18 decimal fixed point value. -/// -/// Typically orders will not clear when their match is exactly 1:1 as the -/// clearer needs to pay gas to process the match. Each order will get exactly -/// the ratio it calculates when it does clear so if there is _overlap_ in the -/// ratios then the clearer keeps the difference. In our above example, consider -/// order B asking a ratio of 1:110 instead of 1:100. In this case 100 TKNA will -/// move from order B to order A and 10 TKNA will move to the clearer's vault and -/// 1 TKNB will move from order A to order B. In the case of fixed prices this is -/// not very interesting as order B could more simply take order A directly for -/// cheaper rather than involving a third party. Indeed, Orderbook supports a -/// direct "take orders" method that works similar to a "market buy". In the case -/// of dynamic expression based ratios, it allows both order A and order B to -/// clear non-interactively according to their strategy, trading off active -/// management, dealing with front-running, MEV, etc. for zero-gas and -/// exact-ratio clearance. -/// -/// The general invariant for clearing and take orders is: -/// -/// ``` -/// ratioA = InputA / OutputA -/// ratioB = InputB / OutputB -/// ratioA * ratioB = ( InputA * InputB ) / ( OutputA * OutputB ) -/// OutputA >= InputB -/// OutputB >= InputA -/// -/// ∴ ratioA * ratioB <= 1 -/// ``` -/// -/// Orderbook is `IERC3156FlashLender` compliant with a 0 fee flash loan -/// implementation to allow external liquidity from other onchain DEXes to match -/// against orderbook expressions. All deposited tokens across all vaults are -/// available for flashloan, the flashloan MAY BE REPAID BY CALLING TAKE ORDER -/// such that Orderbook's liability to its vaults is decreased by an incoming -/// trade from the flashloan borrower. See `ZeroExOrderBookFlashBorrower` for -/// an example of how this works in practise. -/// -/// Orderbook supports many to many input/output token relationship, for example -/// some order can specify an array of stables it would be willing to accept in -/// return for some ETH. This removes the need for a combinatorial explosion of -/// order strategies between like assets but introduces the issue of token -/// decimal handling. End users understand that "one" USDT is roughly equal to -/// "one" DAI, but onchain this is incorrect by _12 orders of magnitude_. This -/// is because "one" DAI is `1e18` tokens and "one" USDT is `1e6` tokens. The -/// orderbook is allowing orders to deploy expressions that define _economic -/// equivalence_ but this doesn't map 1:1 with numeric equivalence in a many to -/// many setup behind token decimal convensions. The solution is to require that -/// end users who place orders provide the decimals of each token they include -/// in their valid IO lists, and to calculate all amounts and ratios in their -/// expressions _as though they were 18 decimal fixed point values_. Orderbook -/// will then automatically rescale the expression values before applying the -/// final vault movements. If an order provides the "wrong" decimal values for -/// some token then it will simply calculate its own ratios and amounts -/// incorrectly which will either lead to no matching orders or a very bad trade -/// for the order owner. There is no way that misrepresenting decimals can attack -/// some other order by a counterparty. Orderbook DOES NOT read decimals from -/// tokens onchain because A. this would be gas for an external call to a cold -/// token contract and B. the ERC20 standard specifically states NOT to read -/// decimals from the interface onchain. -/// -/// Token amounts and ratios returned by calculate order MUST be 18 decimal fixed -/// point values. Token amounts input to handle IO MUST be the exact absolute -/// values that move between the vaults, i.e. NOT rescaled to 18 decimals. The -/// author of the handle IO expression MUST use the token decimals and amounts to -/// rescale themselves if they want that logic, notably the expression author -/// will need to specify the desired rounding behaviour in the rescaling process. -/// -/// When two orders clear there are NO TOKEN MOVEMENTS, only internal vault -/// balances are updated from the input and output vaults. Typically this results -/// in less gas per clear than calling external token transfers and also avoids -/// issues with reentrancy, allowances, external balances etc. This also means -/// that REBASING TOKENS AND TOKENS WITH DYNAMIC BALANCE ARE NOT SUPPORTED. -/// Orderbook ONLY WORKS IF TOKEN BALANCES ARE 1:1 WITH ADDITION/SUBTRACTION PER -/// VAULT MOVEMENT. -/// -/// Dust due to rounding errors always favours the order. Output max is rounded -/// down and IO ratios are rounded up. Input and output amounts are always -/// converted to absolute values before applying to vault balances such that -/// orderbook always retains fully collateralised inventory of underlying token -/// balances to support withdrawals, with the caveat that dynamic token balanes -/// are not supported. -/// -/// When an order clears it is NOT removed. Orders remain active until the owner -/// deactivates them. This is gas efficient as order owners MAY deposit more -/// tokens in a vault with an order against it many times and the order strategy -/// will continue to be clearable according to its expression. As vault IDs are -/// `uint256` values there are effectively infinite possible vaults for any token -/// so there is no limit to how many active orders any address can have at one -/// time. This also allows orders to be daisy chained arbitrarily where output -/// vaults for some order are the input vaults for some other order. -/// -/// Expression storage is namespaced by order owner, so gets and sets are unique -/// to each onchain address. Order owners MUST TAKE CARE not to override their -/// storage sets globally across all their orders, which they can do most simply -/// by hashing the order hash into their get/set keys inside the expression. This -/// gives maximum flexibility for shared state across orders without allowing -/// order owners to attack and overwrite values stored by orders placed by their -/// counterparty. -/// -/// Note that each order specifies its own interpreter and deployer so the -/// owner is responsible for not corrupting their own calculations with bad -/// interpreters. This also means the Orderbook MUST assume the interpreter, and -/// notably the interpreter's store, is malicious and guard against reentrancy -/// etc. -/// -/// As Orderbook supports any expression that can run on any `IInterpreterV1` and -/// counterparties are available to the order, order strategies are free to -/// implement KYC/membership, tracking, distributions, stock, buybacks, etc. etc. -interface IOrderBookV2 is IERC3156FlashLender, IInterpreterCallerV2 { - /// Some tokens have been deposited to a vault. - /// @param sender `msg.sender` depositing tokens. Delegated deposits are NOT - /// supported. - /// @param config All config sent to the `deposit` call. - event Deposit(address sender, DepositConfig config); - - /// Some tokens have been withdrawn from a vault. - /// @param sender `msg.sender` withdrawing tokens. Delegated withdrawals are - /// NOT supported. - /// @param config All config sent to the `withdraw` call. - /// @param amount The amount of tokens withdrawn, can be less than the - /// config amount if the vault does not have the funds available to cover - /// the config amount. For example an active order might move tokens before - /// the withdraw completes. - event Withdraw(address sender, WithdrawConfig config, uint256 amount); - - /// An order has been added to the orderbook. The order is permanently and - /// always active according to its expression until/unless it is removed. - /// @param sender `msg.sender` adding the order and is owner of the order. - /// @param expressionDeployer The expression deployer that ran the integrity - /// check for this order. This is NOT included in the `Order` itself but is - /// important for offchain processes to ignore untrusted deployers before - /// interacting with them. - /// @param order The newly added order. MUST be handed back as-is when - /// clearing orders and contains derived information in addition to the order - /// config that was provided by the order owner. - /// @param orderHash The hash of the order as it is recorded onchain. Only - /// the hash is stored in Orderbook storage to avoid paying gas to store the - /// entire order. - event AddOrder(address sender, IExpressionDeployerV2 expressionDeployer, Order order, uint256 orderHash); - - /// An order has been removed from the orderbook. This effectively - /// deactivates it. Orders can be added again after removal. - /// @param sender `msg.sender` removing the order and is owner of the order. - /// @param order The removed order. - /// @param orderHash The hash of the removed order. - event RemoveOrder(address sender, Order order, uint256 orderHash); - - /// Some order has been taken by `msg.sender`. This is the same as them - /// placing inverse orders then immediately clearing them all, but costs less - /// gas and is more convenient and reliable. Analogous to a market buy - /// against the specified orders. Each order that is matched within a the - /// `takeOrders` loop emits its own individual event. - /// @param sender `msg.sender` taking the orders. - /// @param config All config defining the orders to attempt to take. - /// @param input The input amount from the perspective of sender. - /// @param output The output amount from the perspective of sender. - event TakeOrder(address sender, TakeOrderConfig config, uint256 input, uint256 output); - - /// Emitted when attempting to match an order that either never existed or - /// was removed. An event rather than an error so that we allow attempting - /// many orders in a loop and NOT rollback on "best effort" basis to clear. - /// @param sender `msg.sender` clearing the order that wasn't found. - /// @param owner Owner of the order that was not found. - /// @param orderHash Hash of the order that was not found. - event OrderNotFound(address sender, address owner, uint256 orderHash); - - /// Emitted when an order evaluates to a zero amount. An event rather than an - /// error so that we allow attempting many orders in a loop and NOT rollback - /// on a "best effort" basis to clear. - /// @param sender `msg.sender` clearing the order that had a 0 amount. - /// @param owner Owner of the order that evaluated to a 0 amount. - /// @param orderHash Hash of the order that evaluated to a 0 amount. - event OrderZeroAmount(address sender, address owner, uint256 orderHash); - - /// Emitted when an order evaluates to a ratio exceeding the counterparty's - /// maximum limit. An error rather than an error so that we allow attempting - /// many orders in a loop and NOT rollback on a "best effort" basis to clear. - /// @param sender `msg.sender` clearing the order that had an excess ratio. - /// @param owner Owner of the order that had an excess ratio. - /// @param orderHash Hash of the order that had an excess ratio. - event OrderExceedsMaxRatio(address sender, address owner, uint256 orderHash); - - /// Emitted before two orders clear. Covers both orders and includes all the - /// state before anything is calculated. - /// @param sender `msg.sender` clearing both orders. - /// @param alice One of the orders. - /// @param bob The other order. - /// @param clearConfig Additional config required to process the clearance. - event Clear(address sender, Order alice, Order bob, ClearConfig clearConfig); - - /// Emitted after two orders clear. Includes all final state changes in the - /// vault balances, including the clearer's vaults. - /// @param sender `msg.sender` clearing the order. - /// @param clearStateChange The final vault state changes from the clearance. - event AfterClear(address sender, ClearStateChange clearStateChange); - - /// Get the current balance of a vault for a given owner, token and vault ID. - /// @param owner The owner of the vault. - /// @param token The token the vault is for. - /// @param id The vault ID to read. - /// @return balance The current balance of the vault. - function vaultBalance(address owner, address token, uint256 id) external view returns (uint256 balance); - - /// `msg.sender` deposits tokens according to config. The config specifies - /// the vault to deposit tokens under. Delegated depositing is NOT supported. - /// Depositing DOES NOT mint shares (unlike ERC4626) so the overall vaulted - /// experience is much simpler as there is always a 1:1 relationship between - /// deposited assets and vault balances globally and individually. This - /// mitigates rounding/dust issues, speculative behaviour on derived assets, - /// possible regulatory issues re: whether a vault share is a security, code - /// bloat on the vault, complex mint/deposit/withdraw/redeem 4-way logic, - /// the need for preview functions, etc. etc. - /// At the same time, allowing vault IDs to be specified by the depositor - /// allows much more granular and direct control over token movements within - /// Orderbook than either ERC4626 vault shares or mere contract-level ERC20 - /// allowances can facilitate. - /// @param config All config for the deposit. - function deposit(DepositConfig calldata config) external; - - /// Allows the sender to withdraw any tokens from their own vaults. If the - /// withrawer has an active flash loan debt denominated in the same token - /// being withdrawn then Orderbook will merely reduce the debt and NOT send - /// the amount of tokens repaid to the flashloan debt. - /// @param config All config required to withdraw. Notably if the amount - /// is less than the current vault balance then the vault will be cleared - /// to 0 rather than the withdraw transaction reverting. - function withdraw(WithdrawConfig calldata config) external; - - /// Given an order config, deploys the expression and builds the full `Order` - /// for the config, then records it as an active order. Delegated adding an - /// order is NOT supported. The `msg.sender` that adds an order is ALWAYS - /// the owner and all resulting vault movements are their own. - /// @param config All config required to build an `Order`. - function addOrder(OrderConfig calldata config) external; - - /// Order owner can remove their own orders. Delegated order removal is NOT - /// supported and will revert. Removing an order multiple times or removing - /// an order that never existed are valid, the event will be emitted and the - /// transaction will complete with that order hash definitely, redundantly - /// not live. - /// @param order The `Order` data exactly as it was added. - function removeOrder(Order calldata order) external; - - /// Allows `msg.sender` to attempt to fill a list of orders in sequence - /// without needing to place their own order and clear them. This works like - /// a market buy but against a specific set of orders. Every order will - /// looped over and calculated individually then filled maximally until the - /// request input is reached for the `msg.sender`. The `msg.sender` is - /// responsible for selecting the best orders at the time according to their - /// criteria and MAY specify a maximum IO ratio to guard against an order - /// spiking the ratio beyond what the `msg.sender` expected and is - /// comfortable with. As orders may be removed and calculate their ratios - /// dynamically, all issues fulfilling an order other than misconfiguration - /// by the `msg.sender` are no-ops and DO NOT revert the transaction. This - /// allows the `msg.sender` to optimistically provide a list of orders that - /// they aren't sure will completely fill at a good price, and fallback to - /// more reliable orders further down their list. Misconfiguration such as - /// token mismatches are errors that revert as this is known and static at - /// all times to the `msg.sender` so MUST be provided correctly. `msg.sender` - /// MAY specify a minimum input that MUST be reached across all orders in the - /// list, otherwise the transaction will revert, this MAY be set to zero. - /// - /// Exactly like withdraw, if there is an active flash loan for `msg.sender` - /// they will have their outstanding loan reduced by the final input amount - /// preferentially before sending any tokens. Notably this allows arb bots - /// implemented as flash loan borrowers to connect orders against external - /// liquidity directly by paying back the loan with a `takeOrders` call and - /// outputting the result of the external trade. - /// - /// Rounding errors always favour the order never the `msg.sender`. - /// - /// @param config The constraints and list of orders to take, orders are - /// processed sequentially in order as provided, there is NO ATTEMPT onchain - /// to predict/filter/sort these orders other than evaluating them as - /// provided. Inputs and outputs are from the perspective of `msg.sender` - /// except for values specified by the orders themselves which are the from - /// the perspective of that order. - /// @return totalInput Total tokens sent to `msg.sender`, taken from order - /// vaults processed. - /// @return totalOutput Total tokens taken from `msg.sender` and distributed - /// between vaults. - function takeOrders(TakeOrdersConfig calldata config) external returns (uint256 totalInput, uint256 totalOutput); - - /// Allows `msg.sender` to match two live orders placed earlier by - /// non-interactive parties and claim a bounty in the process. The clearer is - /// free to select any two live orders on the order book for matching and as - /// long as they have compatible tokens, ratios and amounts, the orders will - /// clear. Clearing the orders DOES NOT remove them from the orderbook, they - /// remain live until explicitly removed by their owner. Even if the input - /// vault balances are completely emptied, the orders remain live until - /// removed. This allows order owners to deploy a strategy over a long period - /// of time and periodically top up the input vaults. Clearing two orders - /// from the same owner is disallowed. - /// - /// Any mismatch in the ratios between the two orders will cause either more - /// inputs than there are available outputs (transaction will revert) or less - /// inputs than there are available outputs. In the latter case the excess - /// outputs are given to the `msg.sender` of clear, to the vaults they - /// specify in the clear config. This not only incentivises "automatic" clear - /// calls for both alice and bob, but incentivises _prioritising greater - /// ratio differences_ with a larger bounty. The second point is important - /// because it implicitly prioritises orders that are further from the - /// current market price, thus putting constant increasing pressure on the - /// entire system the further it drifts from the norm, no matter how esoteric - /// the individual order expressions and sizings might be. - /// - /// All else equal there are several factors that would impact how reliably - /// some order clears relative to the wider market, such as: - /// - /// - Bounties are effectively percentages of cleared amounts so larger - /// orders have larger bounties and cover gas costs more easily - /// - High gas on the network means that orders are harder to clear - /// profitably so the negative spread of the ratios will need to be larger - /// - Complex and stateful expressions cost more gas to evalulate so the - /// negative spread will need to be larger - /// - Erratic behavior of the order owner could reduce the willingness of - /// third parties to interact if it could result in wasted gas due to - /// orders suddently being removed before clearance etc. - /// - Dynamic and highly volatile words used in the expression could be - /// ignored or low priority by clearers who want to be sure that they can - /// accurately predict the ratios that they include in their clearance - /// - Geopolitical issues such as sanctions and regulatory restrictions could - /// cause issues for certain owners and clearers - /// - /// @param alice Some order to clear. - /// @param bob Another order to clear. - /// @param clearConfig Additional configuration for the clearance such as - /// how to handle the bounty payment for the `msg.sender`. - /// @param aliceSignedContext Optional signed context that is relevant to A. - /// @param bobSignedContext Optional signed context that is relevant to B. - function clear( - Order memory alice, - Order memory bob, - ClearConfig calldata clearConfig, - SignedContextV1[] memory aliceSignedContext, - SignedContextV1[] memory bobSignedContext - ) external; -} diff --git a/src/interface/IOrderBookV3.sol b/src/interface/IOrderBookV3.sol deleted file mode 100644 index 23d1bd8..0000000 --- a/src/interface/IOrderBookV3.sol +++ /dev/null @@ -1,550 +0,0 @@ -// SPDX-License-Identifier: CAL -pragma solidity ^0.8.18; - -import "./IERC3156FlashLender.sol"; -import {IExpressionDeployerV3, EvaluableV2} from "rain.interpreter/src/lib/caller/LibEvaluable.sol"; -import { - EvaluableConfigV3, - IInterpreterCallerV2, - SignedContextV1 -} from "rain.interpreter/src/interface/IInterpreterCallerV2.sol"; - -/// Import unmodified structures from older versions of `IOrderBook`. -import {IO, ClearConfig, ClearStateChange} from "./IOrderBookV2.sol"; - -/// Thrown when take orders is called with no orders. -error NoOrders(); - -/// Thrown when take orders is called with a zero maximum input. -error ZeroMaximumInput(); - -/// Config the order owner may provide to define their order. The `msg.sender` -/// that adds an order cannot modify the owner nor bypass the integrity check of -/// the expression deployer that they specify. However they MAY specify a -/// deployer with a corrupt integrity check, so counterparties and clearers MUST -/// check the DISpair of the order and avoid untrusted pairings. -/// @param validInputs As per `validInputs` on the `Order`. -/// @param validOutputs As per `validOutputs` on the `Order`. -/// @param evaluableConfig Standard `EvaluableConfig` used to produce the -/// `Evaluable` on the order. -/// @param meta Arbitrary bytes that will NOT be used in the order evaluation -/// but MUST be emitted as a Rain `MetaV1` when the order is placed so can be -/// used by offchain processes. -struct OrderConfigV2 { - IO[] validInputs; - IO[] validOutputs; - EvaluableConfigV3 evaluableConfig; - bytes meta; -} - -/// Config for an individual take order from the overall list of orders in a -/// call to `takeOrders`. -/// @param order The order being taken this iteration. -/// @param inputIOIndex The index of the input token in `order` to match with the -/// take order output. -/// @param outputIOIndex The index of the output token in `order` to match with -/// the take order input. -/// @param signedContext Optional additional signed context relevant to the -/// taken order. -struct TakeOrderConfigV2 { - OrderV2 order; - uint256 inputIOIndex; - uint256 outputIOIndex; - SignedContextV1[] signedContext; -} - -/// Defines a fully deployed order ready to evaluate by Orderbook. Identical to -/// `Order` except for the newer `EvaluableV2`. -/// @param owner The owner of the order is the `msg.sender` that added the order. -/// @param handleIO true if there is a "handle IO" entrypoint to run. If false -/// the order book MAY skip calling the interpreter to save gas. -/// @param evaluable Standard `EvaluableV2` with entrypoints for both -/// "calculate order" and "handle IO". The latter MAY be empty bytes, in which -/// case it will be skipped at runtime to save gas. -/// @param validInputs A list of input tokens that are economically equivalent -/// for the purpose of processing this order. Inputs are relative to the order -/// so these tokens will be sent to the owners vault. -/// @param validOutputs A list of output tokens that are economically equivalent -/// for the purpose of processing this order. Outputs are relative to the order -/// so these tokens will be sent from the owners vault. -struct OrderV2 { - address owner; - bool handleIO; - EvaluableV2 evaluable; - IO[] validInputs; - IO[] validOutputs; -} - -/// Config for a list of orders to take sequentially as part of a `takeOrders` -/// call. -/// @param minimumInput Minimum input from the perspective of the order taker. -/// @param maximumInput Maximum input from the perspective of the order taker. -/// @param maximumIORatio Maximum IO ratio as calculated by the order being -/// taken. The input is from the perspective of the order so higher ratio means -/// worse deal for the order taker. -/// @param orders Ordered list of orders that will be taken until the limit is -/// hit. Takers are expected to prioritise orders that appear to be offering -/// better deals i.e. lower IO ratios. This prioritisation and sorting MUST -/// happen offchain, e.g. via. some simulator. -/// @param data If nonzero length, triggers `onTakeOrders` on the caller of -/// `takeOrders` with this data. This allows the caller to perform arbitrary -/// onchain actions between receiving their input tokens, before having to send -/// their output tokens. -struct TakeOrdersConfigV2 { - uint256 minimumInput; - uint256 maximumInput; - uint256 maximumIORatio; - TakeOrderConfigV2[] orders; - bytes data; -} - -/// @title IOrderBookV3 -/// @notice An orderbook that deploys _strategies_ represented as interpreter -/// expressions rather than individual orders. The order book contract itself -/// behaves similarly to an `ERC4626` vault but with much more fine grained -/// control over how tokens are allocated and moved internally by their owners, -/// and without any concept of "shares". Token owners MAY deposit and withdraw -/// their tokens under arbitrary vault IDs on a per-token basis, then define -/// orders that specify how tokens move between vaults according to an expression. -/// The expression returns a maximum amount and a token input/output ratio from -/// the perpective of the order. When two expressions intersect, as in their -/// ratios are the inverse of each other, then tokens can move between vaults. -/// -/// For example, consider order A with input TKNA and output TKNB with a constant -/// ratio of 100:1. This order in isolation has no ability to move tokens. If -/// an order B appears with input TKNB and output TKNA and a ratio of 1:100 then -/// this is a perfect match with order A. In this case 100 TKNA will move from -/// order B to order A and 1 TKNB will move from order A to order B. -/// -/// IO ratios are always specified as input:output and are 18 decimal fixed point -/// values. The maximum amount that can be moved in the current clearance is also -/// set by the order expression as an 18 decimal fixed point value. -/// -/// Typically orders will not clear when their match is exactly 1:1 as the -/// clearer needs to pay gas to process the match. Each order will get exactly -/// the ratio it calculates when it does clear so if there is _overlap_ in the -/// ratios then the clearer keeps the difference. In our above example, consider -/// order B asking a ratio of 1:110 instead of 1:100. In this case 100 TKNA will -/// move from order B to order A and 10 TKNA will move to the clearer's vault and -/// 1 TKNB will move from order A to order B. In the case of fixed prices this is -/// not very interesting as order B could more simply take order A directly for -/// cheaper rather than involving a third party. Indeed, Orderbook supports a -/// direct "take orders" method that works similar to a "market buy". In the case -/// of dynamic expression based ratios, it allows both order A and order B to -/// clear non-interactively according to their strategy, trading off active -/// management, dealing with front-running, MEV, etc. for zero-gas and -/// exact-ratio clearance. -/// -/// The general invariant for clearing and take orders is: -/// -/// ``` -/// ratioA = InputA / OutputA -/// ratioB = InputB / OutputB -/// ratioA * ratioB = ( InputA * InputB ) / ( OutputA * OutputB ) -/// OutputA >= InputB -/// OutputB >= InputA -/// -/// ∴ ratioA * ratioB <= 1 -/// ``` -/// -/// Orderbook is `IERC3156FlashLender` compliant with a 0 fee flash loan -/// implementation to allow external liquidity from other onchain DEXes to match -/// against orderbook expressions. All deposited tokens across all vaults are -/// available for flashloan, the flashloan MAY BE REPAID BY CALLING TAKE ORDER -/// such that Orderbook's liability to its vaults is decreased by an incoming -/// trade from the flashloan borrower. See `ZeroExOrderBookFlashBorrower` for -/// an example of how this works in practise. -/// -/// Orderbook supports many to many input/output token relationship, for example -/// some order can specify an array of stables it would be willing to accept in -/// return for some ETH. This removes the need for a combinatorial explosion of -/// order strategies between like assets but introduces the issue of token -/// decimal handling. End users understand that "one" USDT is roughly equal to -/// "one" DAI, but onchain this is incorrect by _12 orders of magnitude_. This -/// is because "one" DAI is `1e18` tokens and "one" USDT is `1e6` tokens. The -/// orderbook is allowing orders to deploy expressions that define _economic -/// equivalence_ but this doesn't map 1:1 with numeric equivalence in a many to -/// many setup behind token decimal convensions. The solution is to require that -/// end users who place orders provide the decimals of each token they include -/// in their valid IO lists, and to calculate all amounts and ratios in their -/// expressions _as though they were 18 decimal fixed point values_. Orderbook -/// will then automatically rescale the expression values before applying the -/// final vault movements. If an order provides the "wrong" decimal values for -/// some token then it will simply calculate its own ratios and amounts -/// incorrectly which will either lead to no matching orders or a very bad trade -/// for the order owner. There is no way that misrepresenting decimals can attack -/// some other order by a counterparty. Orderbook DOES NOT read decimals from -/// tokens onchain because A. this would be gas for an external call to a cold -/// token contract and B. the ERC20 standard specifically states NOT to read -/// decimals from the interface onchain. -/// -/// Token amounts and ratios returned by calculate order MUST be 18 decimal fixed -/// point values. Token amounts input to handle IO MUST be the exact absolute -/// values that move between the vaults, i.e. NOT rescaled to 18 decimals. The -/// author of the handle IO expression MUST use the token decimals and amounts to -/// rescale themselves if they want that logic, notably the expression author -/// will need to specify the desired rounding behaviour in the rescaling process. -/// -/// When two orders clear there are NO TOKEN MOVEMENTS, only internal vault -/// balances are updated from the input and output vaults. Typically this results -/// in less gas per clear than calling external token transfers and also avoids -/// issues with reentrancy, allowances, external balances etc. This also means -/// that REBASING TOKENS AND TOKENS WITH DYNAMIC BALANCE ARE NOT SUPPORTED. -/// Orderbook ONLY WORKS IF TOKEN BALANCES ARE 1:1 WITH ADDITION/SUBTRACTION PER -/// VAULT MOVEMENT. -/// -/// Dust due to rounding errors always favours the order. Output max is rounded -/// down and IO ratios are rounded up. Input and output amounts are always -/// converted to absolute values before applying to vault balances such that -/// orderbook always retains fully collateralised inventory of underlying token -/// balances to support withdrawals, with the caveat that dynamic token balanes -/// are not supported. -/// -/// When an order clears it is NOT removed. Orders remain active until the owner -/// deactivates them. This is gas efficient as order owners MAY deposit more -/// tokens in a vault with an order against it many times and the order strategy -/// will continue to be clearable according to its expression. As vault IDs are -/// `uint256` values there are effectively infinite possible vaults for any token -/// so there is no limit to how many active orders any address can have at one -/// time. This also allows orders to be daisy chained arbitrarily where output -/// vaults for some order are the input vaults for some other order. -/// -/// Expression storage is namespaced by order owner, so gets and sets are unique -/// to each onchain address. Order owners MUST TAKE CARE not to override their -/// storage sets globally across all their orders, which they can do most simply -/// by hashing the order hash into their get/set keys inside the expression. This -/// gives maximum flexibility for shared state across orders without allowing -/// order owners to attack and overwrite values stored by orders placed by their -/// counterparty. -/// -/// Note that each order specifies its own interpreter and deployer so the -/// owner is responsible for not corrupting their own calculations with bad -/// interpreters. This also means the Orderbook MUST assume the interpreter, and -/// notably the interpreter's store, is malicious and guard against reentrancy -/// etc. -/// -/// As Orderbook supports any expression that can run on any `IInterpreterV1` and -/// counterparties are available to the order, order strategies are free to -/// implement KYC/membership, tracking, distributions, stock, buybacks, etc. etc. -/// -/// Main differences between `IOrderBookV2` and `IOderBookV3`: -/// - Most structs are now primitives to save gas. -/// - Order hash is `bytes32`. -/// - `deposit` and `withdraw` MUST revert if the amount is zero. -/// - adding an order MUST revert if there is no calculation entrypoint. -/// - adding an order MUST revert if there is no handle IO entrypoint. -/// - adding an order MUST revert if there are no inputs. -/// - adding an order MUST revert if there are no outputs. -/// - adding and removing orders MUST return a boolean indicating if the state -/// changed. -/// - new `orderExists` method. -interface IOrderBookV3 is IERC3156FlashLender, IInterpreterCallerV2 { - /// MUST be thrown by `deposit` if the amount is zero. - /// @param sender `msg.sender` depositing tokens. - /// @param token The token being deposited. - /// @param vaultId The vault ID the tokens are being deposited under. - error ZeroDepositAmount(address sender, address token, uint256 vaultId); - - /// MUST be thrown by `withdraw` if the amount _requested_ to withdraw is - /// zero. The withdrawal MAY still not move any tokens if the vault balance - /// is zero, or the withdrawal is used to repay a flash loan. - /// @param sender `msg.sender` withdrawing tokens. - /// @param token The token being withdrawn. - /// @param vaultId The vault ID the tokens are being withdrawn from. - error ZeroWithdrawTargetAmount(address sender, address token, uint256 vaultId); - - /// MUST be thrown by `addOrder` if the order has no associated calculation. - error OrderNoSources(); - - /// MUST be thrown by `addOrder` if the order has no associated handle IO. - error OrderNoHandleIO(); - - /// MUST be thrown by `addOrder` if the order has no inputs. - error OrderNoInputs(); - - /// MUST be thrown by `addOrder` if the order has no outputs. - error OrderNoOutputs(); - - /// Some tokens have been deposited to a vault. - /// @param sender `msg.sender` depositing tokens. Delegated deposits are NOT - /// supported. - /// @param token The token being deposited. - /// @param vaultId The vault ID the tokens are being deposited under. - /// @param amount The amount of tokens deposited. - event Deposit(address sender, address token, uint256 vaultId, uint256 amount); - - /// Some tokens have been withdrawn from a vault. - /// @param sender `msg.sender` withdrawing tokens. Delegated withdrawals are - /// NOT supported. - /// @param token The token being withdrawn. - /// @param vaultId The vault ID the tokens are being withdrawn from. - /// @param targetAmount The amount of tokens requested to withdraw. - /// @param amount The amount of tokens withdrawn, can be less than the - /// target amount if the vault does not have the funds available to cover - /// the target amount. For example an active order might move tokens before - /// the withdraw completes. - event Withdraw(address sender, address token, uint256 vaultId, uint256 targetAmount, uint256 amount); - - /// An order has been added to the orderbook. The order is permanently and - /// always active according to its expression until/unless it is removed. - /// @param sender `msg.sender` adding the order and is owner of the order. - /// @param expressionDeployer The expression deployer that ran the integrity - /// check for this order. This is NOT included in the `Order` itself but is - /// important for offchain processes to ignore untrusted deployers before - /// interacting with them. - /// @param order The newly added order. MUST be handed back as-is when - /// clearing orders and contains derived information in addition to the order - /// config that was provided by the order owner. - /// @param orderHash The hash of the order as it is recorded onchain. Only - /// the hash is stored in Orderbook storage to avoid paying gas to store the - /// entire order. - event AddOrder(address sender, IExpressionDeployerV3 expressionDeployer, OrderV2 order, bytes32 orderHash); - - /// An order has been removed from the orderbook. This effectively - /// deactivates it. Orders can be added again after removal. - /// @param sender `msg.sender` removing the order and is owner of the order. - /// @param order The removed order. - /// @param orderHash The hash of the removed order. - event RemoveOrder(address sender, OrderV2 order, bytes32 orderHash); - - /// Some order has been taken by `msg.sender`. This is the same as them - /// placing inverse orders then immediately clearing them all, but costs less - /// gas and is more convenient and reliable. Analogous to a market buy - /// against the specified orders. Each order that is matched within a the - /// `takeOrders` loop emits its own individual event. - /// @param sender `msg.sender` taking the orders. - /// @param config All config defining the orders to attempt to take. - /// @param input The input amount from the perspective of sender. - /// @param output The output amount from the perspective of sender. - event TakeOrder(address sender, TakeOrderConfigV2 config, uint256 input, uint256 output); - - /// Emitted when attempting to match an order that either never existed or - /// was removed. An event rather than an error so that we allow attempting - /// many orders in a loop and NOT rollback on "best effort" basis to clear. - /// @param sender `msg.sender` clearing the order that wasn't found. - /// @param owner Owner of the order that was not found. - /// @param orderHash Hash of the order that was not found. - event OrderNotFound(address sender, address owner, bytes32 orderHash); - - /// Emitted when an order evaluates to a zero amount. An event rather than an - /// error so that we allow attempting many orders in a loop and NOT rollback - /// on a "best effort" basis to clear. - /// @param sender `msg.sender` clearing the order that had a 0 amount. - /// @param owner Owner of the order that evaluated to a 0 amount. - /// @param orderHash Hash of the order that evaluated to a 0 amount. - event OrderZeroAmount(address sender, address owner, bytes32 orderHash); - - /// Emitted when an order evaluates to a ratio exceeding the counterparty's - /// maximum limit. An error rather than an error so that we allow attempting - /// many orders in a loop and NOT rollback on a "best effort" basis to clear. - /// @param sender `msg.sender` clearing the order that had an excess ratio. - /// @param owner Owner of the order that had an excess ratio. - /// @param orderHash Hash of the order that had an excess ratio. - event OrderExceedsMaxRatio(address sender, address owner, bytes32 orderHash); - - /// Emitted before two orders clear. Covers both orders and includes all the - /// state before anything is calculated. - /// @param sender `msg.sender` clearing both orders. - /// @param alice One of the orders. - /// @param bob The other order. - /// @param clearConfig Additional config required to process the clearance. - event Clear(address sender, OrderV2 alice, OrderV2 bob, ClearConfig clearConfig); - - /// Emitted after two orders clear. Includes all final state changes in the - /// vault balances, including the clearer's vaults. - /// @param sender `msg.sender` clearing the order. - /// @param clearStateChange The final vault state changes from the clearance. - event AfterClear(address sender, ClearStateChange clearStateChange); - - /// Get the current balance of a vault for a given owner, token and vault ID. - /// @param owner The owner of the vault. - /// @param token The token the vault is for. - /// @param id The vault ID to read. - /// @return balance The current balance of the vault. - function vaultBalance(address owner, address token, uint256 id) external view returns (uint256 balance); - - /// `msg.sender` deposits tokens according to config. The config specifies - /// the vault to deposit tokens under. Delegated depositing is NOT supported. - /// Depositing DOES NOT mint shares (unlike ERC4626) so the overall vaulted - /// experience is much simpler as there is always a 1:1 relationship between - /// deposited assets and vault balances globally and individually. This - /// mitigates rounding/dust issues, speculative behaviour on derived assets, - /// possible regulatory issues re: whether a vault share is a security, code - /// bloat on the vault, complex mint/deposit/withdraw/redeem 4-way logic, - /// the need for preview functions, etc. etc. - /// - /// At the same time, allowing vault IDs to be specified by the depositor - /// allows much more granular and direct control over token movements within - /// Orderbook than either ERC4626 vault shares or mere contract-level ERC20 - /// allowances can facilitate. - // - /// Vault IDs are namespaced by the token address so there is no risk of - /// collision between tokens. For example, vault ID 0 for token A is - /// completely different to vault ID 0 for token B. - /// - /// `0` amount deposits are unsupported as underlying token contracts - /// handle `0` value transfers differently and this would be a source of - /// confusion. The order book MUST revert with `ZeroDepositAmount` if the - /// amount is zero. - /// - /// @param token The token to deposit. - /// @param vaultId The vault ID to deposit under. - /// @param amount The amount of tokens to deposit. - function deposit(address token, uint256 vaultId, uint256 amount) external; - - /// Allows the sender to withdraw any tokens from their own vaults. If the - /// withrawer has an active flash loan debt denominated in the same token - /// being withdrawn then Orderbook will merely reduce the debt and NOT send - /// the amount of tokens repaid to the flashloan debt. - /// - /// MUST revert if the amount _requested_ to withdraw is zero. The withdrawal - /// MAY still not move any tokens (without revert) if the vault balance is - /// zero, or the withdrawal is used to repay a flash loan, or due to any - /// other internal accounting. - /// - /// @param token The token to withdraw. - /// @param vaultId The vault ID to withdraw from. - /// @param targetAmount The amount of tokens to attempt to withdraw. MAY - /// result in fewer tokens withdrawn if the vault balance is lower than the - /// target amount. MAY NOT be zero, the order book MUST revert with - /// `ZeroWithdrawTargetAmount` if the amount is zero. - function withdraw(address token, uint256 vaultId, uint256 targetAmount) external; - - /// Given an order config, deploys the expression and builds the full `Order` - /// for the config, then records it as an active order. Delegated adding an - /// order is NOT supported. The `msg.sender` that adds an order is ALWAYS - /// the owner and all resulting vault movements are their own. - /// - /// MUST revert with `OrderNoSources` if the order has no associated - /// calculation and `OrderNoHandleIO` if the order has no handle IO - /// entrypoint. The calculation MUST return at least two values from - /// evaluation, the maximum amount and the IO ratio. The handle IO entrypoint - /// SHOULD return zero values from evaluation. Either MAY revert during - /// evaluation on the interpreter, which MUST prevent the order from - /// clearing. - /// - /// MUST revert with `OrderNoInputs` if the order has no inputs. - /// MUST revert with `OrderNoOutputs` if the order has no outputs. - /// - /// If the order already exists, the order book MUST NOT change state, which - /// includes not emitting an event. Instead it MUST return false. If the - /// order book modifies state it MUST emit an `AddOrder` event and return - /// true. - /// - /// @param config All config required to build an `Order`. - /// @return stateChanged True if the order was added, false if it already - /// existed. - function addOrder(OrderConfigV2 calldata config) external returns (bool stateChanged); - - /// Returns true if the order exists, false otherwise. - /// @param orderHash The hash of the order to check. - /// @return exists True if the order exists, false otherwise. - function orderExists(bytes32 orderHash) external view returns (bool exists); - - /// Order owner can remove their own orders. Delegated order removal is NOT - /// supported and will revert. Removing an order multiple times or removing - /// an order that never existed are valid, the event will be emitted and the - /// transaction will complete with that order hash definitely, redundantly - /// not live. - /// @param order The `Order` data exactly as it was added. - /// @return stateChanged True if the order was removed, false if it did not - /// exist. - function removeOrder(OrderV2 calldata order) external returns (bool stateChanged); - - /// Allows `msg.sender` to attempt to fill a list of orders in sequence - /// without needing to place their own order and clear them. This works like - /// a market buy but against a specific set of orders. Every order will - /// looped over and calculated individually then filled maximally until the - /// request input is reached for the `msg.sender`. The `msg.sender` is - /// responsible for selecting the best orders at the time according to their - /// criteria and MAY specify a maximum IO ratio to guard against an order - /// spiking the ratio beyond what the `msg.sender` expected and is - /// comfortable with. As orders may be removed and calculate their ratios - /// dynamically, all issues fulfilling an order other than misconfiguration - /// by the `msg.sender` are no-ops and DO NOT revert the transaction. This - /// allows the `msg.sender` to optimistically provide a list of orders that - /// they aren't sure will completely fill at a good price, and fallback to - /// more reliable orders further down their list. Misconfiguration such as - /// token mismatches are errors that revert as this is known and static at - /// all times to the `msg.sender` so MUST be provided correctly. `msg.sender` - /// MAY specify a minimum input that MUST be reached across all orders in the - /// list, otherwise the transaction will revert, this MAY be set to zero. - /// - /// Exactly like withdraw, if there is an active flash loan for `msg.sender` - /// they will have their outstanding loan reduced by the final input amount - /// preferentially before sending any tokens. Notably this allows arb bots - /// implemented as flash loan borrowers to connect orders against external - /// liquidity directly by paying back the loan with a `takeOrders` call and - /// outputting the result of the external trade. - /// - /// Rounding errors always favour the order never the `msg.sender`. - /// - /// @param config The constraints and list of orders to take, orders are - /// processed sequentially in order as provided, there is NO ATTEMPT onchain - /// to predict/filter/sort these orders other than evaluating them as - /// provided. Inputs and outputs are from the perspective of `msg.sender` - /// except for values specified by the orders themselves which are the from - /// the perspective of that order. - /// @return totalInput Total tokens sent to `msg.sender`, taken from order - /// vaults processed. - /// @return totalOutput Total tokens taken from `msg.sender` and distributed - /// between vaults. - function takeOrders(TakeOrdersConfigV2 calldata config) - external - returns (uint256 totalInput, uint256 totalOutput); - - /// Allows `msg.sender` to match two live orders placed earlier by - /// non-interactive parties and claim a bounty in the process. The clearer is - /// free to select any two live orders on the order book for matching and as - /// long as they have compatible tokens, ratios and amounts, the orders will - /// clear. Clearing the orders DOES NOT remove them from the orderbook, they - /// remain live until explicitly removed by their owner. Even if the input - /// vault balances are completely emptied, the orders remain live until - /// removed. This allows order owners to deploy a strategy over a long period - /// of time and periodically top up the input vaults. Clearing two orders - /// from the same owner is disallowed. - /// - /// Any mismatch in the ratios between the two orders will cause either more - /// inputs than there are available outputs (transaction will revert) or less - /// inputs than there are available outputs. In the latter case the excess - /// outputs are given to the `msg.sender` of clear, to the vaults they - /// specify in the clear config. This not only incentivises "automatic" clear - /// calls for both alice and bob, but incentivises _prioritising greater - /// ratio differences_ with a larger bounty. The second point is important - /// because it implicitly prioritises orders that are further from the - /// current market price, thus putting constant increasing pressure on the - /// entire system the further it drifts from the norm, no matter how esoteric - /// the individual order expressions and sizings might be. - /// - /// All else equal there are several factors that would impact how reliably - /// some order clears relative to the wider market, such as: - /// - /// - Bounties are effectively percentages of cleared amounts so larger - /// orders have larger bounties and cover gas costs more easily - /// - High gas on the network means that orders are harder to clear - /// profitably so the negative spread of the ratios will need to be larger - /// - Complex and stateful expressions cost more gas to evalulate so the - /// negative spread will need to be larger - /// - Erratic behavior of the order owner could reduce the willingness of - /// third parties to interact if it could result in wasted gas due to - /// orders suddently being removed before clearance etc. - /// - Dynamic and highly volatile words used in the expression could be - /// ignored or low priority by clearers who want to be sure that they can - /// accurately predict the ratios that they include in their clearance - /// - Geopolitical issues such as sanctions and regulatory restrictions could - /// cause issues for certain owners and clearers - /// - /// @param alice Some order to clear. - /// @param bob Another order to clear. - /// @param clearConfig Additional configuration for the clearance such as - /// how to handle the bounty payment for the `msg.sender`. - /// @param aliceSignedContext Optional signed context that is relevant to A. - /// @param bobSignedContext Optional signed context that is relevant to B. - function clear( - OrderV2 memory alice, - OrderV2 memory bob, - ClearConfig calldata clearConfig, - SignedContextV1[] memory aliceSignedContext, - SignedContextV1[] memory bobSignedContext - ) external; -} diff --git a/src/interface/IOrderBookV3ArbOrderTaker.sol b/src/interface/IOrderBookV3ArbOrderTaker.sol deleted file mode 100644 index 9a1fa23..0000000 --- a/src/interface/IOrderBookV3ArbOrderTaker.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: CAL -pragma solidity ^0.8.18; - -import "./IOrderBookV3OrderTaker.sol"; -import "./IOrderBookV3.sol"; - -interface IOrderBookV3ArbOrderTaker is IOrderBookV3OrderTaker { - function arb(TakeOrdersConfigV2 calldata takeOrders, uint256 minimumSenderOutput) external payable; -} diff --git a/src/interface/IOrderBookV3OrderTaker.sol b/src/interface/IOrderBookV3OrderTaker.sol deleted file mode 100644 index 6244bcf..0000000 --- a/src/interface/IOrderBookV3OrderTaker.sol +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: CAL -pragma solidity ^0.8.18; - -interface IOrderBookV3OrderTaker { - /// @notice Called by `OrderBookV3` when `takeOrders` is called with non-zero - /// data, if it caused a non-zero input amount. I.e. if the order(s) taker - /// received some tokens. Input and output directions are relative to the - /// `IOrderBookV3OrderTaker` contract. If the order(s) taker had an active - /// debt from a flash loan then that debt will be paid _before_ calculating - /// any input amounts sent. - /// i.e. the debt is deducted from the input amount before this callback is - /// called. - /// @param inputToken The token that was sent to `IOrderBookV3OrderTaker`. - /// @param outputToken The token that `IOrderBookV3` will attempt to pull - /// from `IOrderBookV3OrderTaker` after this callback returns. - /// @param inputAmountSent The amount of `inputToken` that was sent to - /// `IOrderBookV3OrderTaker`. - /// @param totalOutputAmount The total amount of `outputToken` that - /// `IOrderBookV3` will attempt to pull from `IOrderBookV3OrderTaker` after - /// this callback returns. - /// @param takeOrdersData The data passed to `takeOrders` by the caller. - function onTakeOrders( - address inputToken, - address outputToken, - uint256 inputAmountSent, - uint256 totalOutputAmount, - bytes calldata takeOrdersData - ) external; -} diff --git a/test/TrancheMirrorModeling.t.sol b/test/TrancheMirrorModeling.t.sol new file mode 100644 index 0000000..e8b4bec --- /dev/null +++ b/test/TrancheMirrorModeling.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.19; + +import {Vm} from "forge-std/Vm.sol"; +import {console2, Test} from "forge-std/Test.sol"; +import "test/util/TrancheMirrorUtils.sol"; +import "src/TrancheMirror.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; + +contract TrancheMirrorTest is TrancheMirrorUtils { + using Strings for uint256; + + function test_trancheModelling() public { + string memory file = "./test/csvs/tranche-space.csv"; + if (vm.exists(file)) vm.removeFile(file); + + FullyQualifiedNamespace namespace = + LibNamespace.qualifyNamespace(StateNamespace.wrap(uint256(uint160(ORDER_OWNER))), address(ORDERBOOK)); + + uint256[][] memory buyOrderContext = getBuyOrderContext(11223344); + + + for (uint256 i = 0; i < 200; i++) { + uint256 trancheSpace = uint256(1e17 * i); + + address expression; + { + (bytes memory bytecode, uint256[] memory constants) = PARSER.parse( + LibTrancheSpreadOrders.getTrancheTestSpreadOrder( + vm, + address(ORDERBOOK_SUPARSER), + trancheSpace, + 101e16 + ) + ); + (,, expression,) = EXPRESSION_DEPLOYER.deployExpression2(bytecode, constants); + } + + (uint256[] memory buyStack,) = IInterpreterV2(INTERPRETER).eval2( + IInterpreterStoreV1(address(STORE)), + namespace, + LibEncodedDispatch.encode2(expression, SourceIndexV2.wrap(0), type(uint32).max), + buyOrderContext, + new uint256[](0) + ); + + string memory line = string.concat(trancheSpace.toString(), ",", buyStack[1].toString(), ",", buyStack[0].toString()); + + vm.writeLine(file, line); + + } + } +} + diff --git a/test/TrancheMirrorTest.t.sol b/test/TrancheMirrorTest.t.sol new file mode 100644 index 0000000..4bfc986 --- /dev/null +++ b/test/TrancheMirrorTest.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.19; + +import {Vm} from "forge-std/Vm.sol"; +import {console2, Test} from "forge-std/Test.sol"; +import "test/util/TrancheMirrorUtils.sol"; +import "src/TrancheMirror.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; + +interface IEONStatsTracker{ + function setStatsTracker(address statsTracker) external; +} + +contract TrancheMirrorTest is TrancheMirrorUtils { + using LibFixedPointDecimalArithmeticOpenZeppelin for uint256; + using LibFixedPointDecimalScale for uint256; + + using SafeERC20 for IERC20; + + function testSellBuyOrderHappyFork() public { + + // Deposit Tokens + { + uint256 depositAmount = 400000e18; + giveTestAccountsTokens(IEON_TOKEN, POLYGON_IEON_HOLDER, ORDER_OWNER, depositAmount); + depositTokens(ORDER_OWNER, IEON_TOKEN, VAULT_ID, depositAmount); + } + OrderV2 memory trancheOrder; + uint256 distributorTokenOut; + uint256 distributorTokenIn; + // Add Order to OrderBook + { + + IO[] memory tokenVaults = new IO[](2); + tokenVaults[0] = polygonIeonIo(); + tokenVaults[1] = polygonWethIo(); + + (bytes memory bytecode, uint256[] memory constants) = PARSER.parse( + LibTrancheSpreadOrders.getTrancheSpreadOrder( + vm, + address(ORDERBOOK_SUPARSER) + ) + ); + trancheOrder = placeOrder(ORDER_OWNER, bytecode, constants, tokenVaults, tokenVaults); + } + // Take Sell Order + { + moveExternalPrice( + address(WETH_TOKEN), + address(IEON_TOKEN), + POLYGON_WETH_HOLDER, + 1000e18, + getUniV3TradeBuyRoute(address(ARB_INSTANCE)) + ); + vm.recordLogs(); + takeOrder(trancheOrder, getUniV3TradeSellRoute(address(ARB_INSTANCE)),1,0); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + (,distributorTokenOut) = getContextInputOutput(entries); + } + // Take Buy Order + { + // Move external market so the order clears + moveExternalPrice( + address(IEON_TOKEN), + address(WETH_TOKEN), + POLYGON_IEON_HOLDER, + 10000000e18, + getUniV3TradeSellRoute(address(ARB_INSTANCE)) + ); + vm.recordLogs(); + takeOrder(trancheOrder, getUniV3TradeBuyRoute(address(ARB_INSTANCE)),0,1); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + (distributorTokenIn,) = getContextInputOutput(entries); + } + assertGe(distributorTokenIn,distributorTokenOut); + } + + function giveTestAccountsTokens(IERC20 token, address from, address to, uint256 amount) internal { + vm.startPrank(from); + token.safeTransfer(to, amount); + assertEq(token.balanceOf(to), amount); + vm.stopPrank(); + } + + function depositTokens(address depositor, IERC20 token, uint256 vaultId, uint256 amount) internal { + vm.startPrank(depositor); + token.safeApprove(address(ORDERBOOK), amount); + ORDERBOOK.deposit(address(token), vaultId, amount); + vm.stopPrank(); + } + + function placeOrder( + address orderOwner, + bytes memory bytecode, + uint256[] memory constants, + IO[] memory inputs, + IO[] memory outputs + ) internal returns (OrderV2 memory order) { + + EvaluableConfigV3 memory evaluableConfig = EvaluableConfigV3(EXPRESSION_DEPLOYER, bytecode, constants); + + OrderConfigV2 memory orderConfig = OrderConfigV2(inputs, outputs, evaluableConfig, ""); + + vm.startPrank(orderOwner); + vm.recordLogs(); + + (bool stateChanged) = ORDERBOOK.addOrder(orderConfig); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 3); + (,, order,) = abi.decode(entries[2].data, (address, address, OrderV2, bytes32)); + assertEq(order.owner, orderOwner); + assertEq(order.handleIO, true); + assertEq(address(order.evaluable.interpreter), address(INTERPRETER)); + assertEq(address(order.evaluable.store), address(STORE)); + assertEq(stateChanged, true); + } + + function takeOrder(OrderV2 memory order, bytes memory route, uint256 inputIOIndex, uint256 outputIOIndex) internal { + vm.startPrank(APPROVED_EOA); + + TakeOrderConfigV2[] memory innerConfigs = new TakeOrderConfigV2[](1); + + innerConfigs[0] = TakeOrderConfigV2(order, inputIOIndex, outputIOIndex, new SignedContextV1[](0)); + TakeOrdersConfigV2 memory takeOrdersConfig = + TakeOrdersConfigV2(0, type(uint256).max, type(uint256).max, innerConfigs, route); + ARB_INSTANCE.arb(takeOrdersConfig, 0); + vm.stopPrank(); + } + + function moveExternalPrice( + address inputToken, + address outputToken, + address tokenHolder, + uint256 amountIn, + bytes memory encodedRoute + ) public { + // An External Account + address EXTERNAL_EOA = address(0x654FEf5Fb8A1C91ad47Ba192F7AA81dd3C821427); + { + giveTestAccountsTokens(IERC20(inputToken), tokenHolder, EXTERNAL_EOA, amountIn); + } + vm.startPrank(EXTERNAL_EOA); + + IERC20(inputToken).safeApprove(address(ROUTE_PROCESSOR), amountIn); + + bytes memory decodedRoute = abi.decode(encodedRoute, (bytes)); + + ROUTE_PROCESSOR.processRoute(inputToken, amountIn, outputToken, 0, EXTERNAL_EOA, decodedRoute); + vm.stopPrank(); + } + + function getContextInputOutput(Vm.Log[] memory entries) internal pure returns(uint256 input, uint256 output){ + for (uint256 j = 0; j < entries.length; j++) { + if (entries[j].topics[0] == keccak256("Context(address,uint256[][])")) { + (, uint256[][] memory context) = abi.decode(entries[j].data, (address, uint256[][])); + input = context[3][4]; + output = context[4][4]; + } + } + } +} diff --git a/test/TwapTrendTest.t.sol b/test/TwapTrendTest.t.sol deleted file mode 100644 index fc7ab02..0000000 --- a/test/TwapTrendTest.t.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: CAL -pragma solidity =0.8.19; - -import {Vm} from "forge-std/Vm.sol"; -import {console2, Test} from "forge-std/Test.sol"; - -contract TwapTrendTest is Test { - -} - diff --git a/test/util/TrancheMirrorUtils.sol b/test/util/TrancheMirrorUtils.sol new file mode 100644 index 0000000..5e36900 --- /dev/null +++ b/test/util/TrancheMirrorUtils.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.19; + +import {console2, Test} from "forge-std/Test.sol"; +import { + StateNamespace, + LibNamespace, + FullyQualifiedNamespace +} from "rain.orderbook/lib/rain.interpreter/src/lib/ns/LibNamespace.sol"; +import {LibEncodedDispatch} from "rain.orderbook/lib/rain.interpreter/src/lib/caller/LibEncodedDispatch.sol"; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; +import "rain.orderbook/lib/rain.math.fixedpoint/src/lib/LibFixedPointDecimalArithmeticOpenZeppelin.sol"; +import "rain.orderbook/lib/rain.math.fixedpoint/src/lib/LibFixedPointDecimalScale.sol"; +import "src/abstract/RainContracts.sol"; +import "src/TrancheMirror.sol"; + +contract TrancheMirrorUtils is RainContracts, Test { + using SafeERC20 for IERC20; + using Strings for address; + + using LibFixedPointDecimalArithmeticOpenZeppelin for uint256; + using LibFixedPointDecimalScale for uint256; + + uint256 constant FORK_BLOCK_NUMBER = 54565408; + uint256 constant CONTEXT_VAULT_IO_ROWS = 5; + + function selectPolygonFork() internal { + uint256 fork = vm.createFork(vm.envString("RPC_URL_POLYGON")); + vm.selectFork(fork); + vm.rollFork(FORK_BLOCK_NUMBER); + } + + bytes32 constant ORDER_HASH = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; + address constant ORDER_OWNER = address(0x19f95a84aa1C48A2c6a7B2d5de164331c86D030C); + address constant APPROVED_EOA = 0x669845c29D9B1A64FFF66a55aA13EB4adB889a88; + address constant INPUT_ADDRESS = address(IEON_TOKEN); + address constant OUTPUT_ADDRESS = address(USDT_TOKEN); + + + + function setUp() public { + selectPolygonFork(); + + PARSER = IParserV1(0x42354C16c8FcFf044c5ee73798F250138ef0A813); + STORE = IInterpreterStoreV2(0x9Ba76481F8cF7F52e440B13981e0003De474A9f7); + INTERPRETER = IInterpreterV2(0xbbe5a04A9a20c47b1A93e755aE712cb84538cd5a); + EXPRESSION_DEPLOYER = IExpressionDeployerV3(0xc64B01aB4b5549dE91e5A4425883Dff87Ceaaf29); + ORDERBOOK_SUPARSER = ISubParserV2(0x14c5D39dE54D498aFD3C803D3B5c88bbEcadcc48); + + ORDERBOOK = IOrderBookV3(0xDE5aBE2837bc042397D80E37fb7b2C850a8d5a6C); + ARB_IMPLEMENTATION = IOrderBookV3ArbOrderTaker(0x8F29083140559bd1771eDBfB73656A9f676c00Fd); + ARB_INSTANCE = IOrderBookV3ArbOrderTaker(0x9571FAcfdeAb981E039aAb30aD3B222709F5cfB4); + CLONE_FACTORY = ICloneableFactoryV2(0x6d0c39093C21dA1230aCDD911420BBfA353A3FBA); + } + + function getSellOrderContext(uint256 orderHash) internal view returns (uint256[][] memory context) { + // Sell Order Context + context = new uint256[][](5); + { + { + uint256[] memory baseContext = new uint256[](2); + context[0] = baseContext; + } + { + uint256[] memory callingContext = new uint256[](3); + // order hash + callingContext[0] = orderHash; + // owner + callingContext[1] = uint256(uint160(address(ORDER_OWNER))); + // counterparty + callingContext[2] = uint256(uint160(address(ORDERBOOK))); + context[1] = callingContext; + } + { + uint256[] memory calculationsContext = new uint256[](0); + context[2] = calculationsContext; + } + { + uint256[] memory inputsContext = new uint256[](CONTEXT_VAULT_IO_ROWS); + inputsContext[0] = uint256(uint160(address(WETH_TOKEN))); + inputsContext[1] = 18; + context[3] = inputsContext; + } + { + uint256[] memory outputsContext = new uint256[](CONTEXT_VAULT_IO_ROWS); + outputsContext[0] = uint256(uint160(address(IEON_TOKEN))); + outputsContext[1] = 18; + context[4] = outputsContext; + } + } + } + + function getBuyOrderContext(uint256 orderHash) internal view returns (uint256[][] memory context) { + // Buy Order Context + context = new uint256[][](5); + { + { + uint256[] memory baseContext = new uint256[](2); + context[0] = baseContext; + } + { + uint256[] memory callingContext = new uint256[](3); + // order hash + callingContext[0] = orderHash; + // owner + callingContext[1] = uint256(uint160(address(ORDERBOOK))); + // counterparty + callingContext[2] = uint256(uint160(address(ARB_INSTANCE))); + context[1] = callingContext; + } + { + uint256[] memory calculationsContext = new uint256[](0); + context[2] = calculationsContext; + } + { + uint256[] memory inputsContext = new uint256[](CONTEXT_VAULT_IO_ROWS); + inputsContext[0] = uint256(uint160(address(IEON_TOKEN))); + inputsContext[1] = 18; + context[3] = inputsContext; + } + { + uint256[] memory outputsContext = new uint256[](CONTEXT_VAULT_IO_ROWS); + outputsContext[0] = uint256(uint160(address(WETH_TOKEN))); + outputsContext[1] = 18; + context[4] = outputsContext; + } + } + } + + function eval(bytes memory rainlang, uint256[][] memory context) public returns (uint256[] memory) { + (bytes memory bytecode, uint256[] memory constants) = PARSER.parse(rainlang); + (,, address expression,) = EXPRESSION_DEPLOYER.deployExpression2(bytecode, constants); + return evalDeployedExpression(expression, ORDER_HASH, context); + } + + function evalDeployedExpression(address expression, bytes32 orderHash, uint256[][] memory context) public view returns (uint256[] memory) { + + (orderHash); + FullyQualifiedNamespace namespace = + LibNamespace.qualifyNamespace(StateNamespace.wrap(uint256(uint160(ORDER_OWNER))), address(ORDERBOOK)); + + (uint256[] memory stack,) = IInterpreterV2(INTERPRETER).eval2( + IInterpreterStoreV1(address(STORE)), + namespace, + LibEncodedDispatch.encode2(expression, SourceIndexV2.wrap(0), type(uint16).max), + context, + new uint256[](0) + ); + return stack; + } + +} \ No newline at end of file diff --git a/test/vg/tranche-space-amount.vg b/test/vg/tranche-space-amount.vg new file mode 100644 index 0000000..cc8fbec --- /dev/null +++ b/test/vg/tranche-space-amount.vg @@ -0,0 +1,114 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "title": { + "text": "IOEN Tranche Space Strategy: Buy Order", + "subtitle": [ + "", + "Distributor token : Polygon IOEN", + "Reserve token : Polygon WETH", + "Tranche reserve base amount : 100 ETH", + "Tranche reserve base io ratio : 1.11", + "Tranche space threshold : 0.01", + "Spread Ratio : 1.01" + ], + "encode":{ + "title": { + "update": { + "fontSize": {"value": 15} + } + }, + "subtitle": { + "interactive": true, + "hover": { + "fontStyle": {"value": "normal"} + } + } + } + }, + "width": 800, + "height": 500, + "padding": 15, + "autosize": "pad", + "data": [{ + "name": "tranche-space", + "transform": [ + { "type": "formula","as": "trancheSpaceScaled", "expr": "(datum.trancheSpace)/(1e+18)" }, + { "type": "formula","as": "amountScaled", "expr": "(datum.amount)/(1e+18)" }, + { "type": "formula","as": "ratioScaled", "expr": "(datum.ratio)/(1e+18)" } + + ], + "format": { + "type": "csv", + "parse": { + "trancheSpace": "number", + "amount": "number", + "ratio": "number" + }, + "header": ["trancheSpace","amount","ratio"] + }, + "url": "../csvs/tranche-space.csv" + }], + "scales": [{ + "name": "x", + "type": "linear", + "range": "width", + "domain": {"data": "tranche-space", "field": "trancheSpaceScaled"} + }, { + "name": "y", + "type": "linear", + "range": "height", + "domain": {"data": "tranche-space", "field": "amountScaled"} + }], + "axes": [ + { + "orient": "bottom", + "scale": "x", + "title": "Fixed point tranche space", + "encode": { + "labels": { + "update": { + "fontSize": {"value": 14} + } + }, + "title": { + "update": { + "fontSize": {"value": 14} + } + } + } + }, + { + "orient": "left", + "scale": "y", + "grid": true, + "title": "Fixed point amount", + "encode": { + "labels": { + "update": { + "fontSize": {"value": 14} + } + }, + "title": { + "update": { + "fontSize": {"value": 14} + } + } + } + } + + ], + "marks": [{ + "type": "line", + "from": {"data": "tranche-space"}, + "encode": { + "enter": { + "x": {"type": "quantitative","scale": "x", "field": "trancheSpaceScaled"}, + "y": {"type": "quantitative","scale": "y", "field": "amountScaled"}, + + "angle": {"value": 45}, + "shape": {"value": "cross"}, + "size": {"value": 30} + } + } + }] + } \ No newline at end of file