From f21f35236ed9bcb7690f1598ad75a7bc503330b2 Mon Sep 17 00:00:00 2001 From: Ruslan Akhtariev Date: Wed, 4 Dec 2024 16:27:10 -0800 Subject: [PATCH] Complexity of on-chain ordering --- foundry.toml | 1 + lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- lib/v3-core | 2 +- lib/v3-periphery | 2 +- lib/v4-core | 2 +- lib/v4-periphery | 2 +- src/ClvrModel.sol | 65 +++++++++++++++++++++++--------------- test/ClvrHook.t.sol | 14 ++++---- test/ClvrModel.t.sol | 44 +++++++++++++++++++------- 10 files changed, 86 insertions(+), 50 deletions(-) diff --git a/foundry.toml b/foundry.toml index 4d02df9..31350dd 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,6 +6,7 @@ ffi = true fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] solc_version = "0.8.26" evm_version = "cancun" +viaIr = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options remappings = [ diff --git a/lib/forge-std b/lib/forge-std index 83c5d21..1eea5ba 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 83c5d212a01f8950727da4095cdfe2654baccb5b +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 0df841d..69c8def 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 0df841d2d75352786532e9e26f400efdef78d86e +Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79 diff --git a/lib/v3-core b/lib/v3-core index d8b1c63..e3589b1 160000 --- a/lib/v3-core +++ b/lib/v3-core @@ -1 +1 @@ -Subproject commit d8b1c635c275d2a9450bd6a78f3fa2484fef73eb +Subproject commit e3589b192d0be27e100cd0daaf6c97204fdb1899 diff --git a/lib/v3-periphery b/lib/v3-periphery index 0682387..80f26c8 160000 --- a/lib/v3-periphery +++ b/lib/v3-periphery @@ -1 +1 @@ -Subproject commit 0682387198a24c7cd63566a2c58398533860a5d1 +Subproject commit 80f26c86c57b8a5e4b913f42844d4c8bd274d058 diff --git a/lib/v4-core b/lib/v4-core index b619b67..362c9ca 160000 --- a/lib/v4-core +++ b/lib/v4-core @@ -1 +1 @@ -Subproject commit b619b6718e31aa5b4fa0286520c455ceb950276d +Subproject commit 362c9cab7c03ca1122795aed2e8e118de9ed186e diff --git a/lib/v4-periphery b/lib/v4-periphery index 1ba502d..2768c3d 160000 --- a/lib/v4-periphery +++ b/lib/v4-periphery @@ -1 +1 @@ -Subproject commit 1ba502d0b7fdb3e9d97fac163b29bb72cd884243 +Subproject commit 2768c3d578a5ffeec0bc51abcd8e472cb68f8a93 diff --git a/src/ClvrModel.sol b/src/ClvrModel.sol index 04d871a..4fa5fc8 100644 --- a/src/ClvrModel.sol +++ b/src/ClvrModel.sol @@ -41,23 +41,33 @@ contract ClvrModel { set_reserve_y(reserve_y); int128 lnP0 = p0.lnU256().toInt128(); + uint256 cachedY = reserveY; + uint256 cachedX = reserveX; + for (uint256 i = 1; i < o.length; ) { uint256 candidateIndex = i; - int128 unsquaredCandidateValue = lnP0 - P(o, i).lnU256().toInt128(); + (uint256 p, uint256 y_cached, uint256 x_cached) = P_cached(o, i, cachedY, cachedX); + + int256 unsquaredCandidateValue = lnP0 - p.lnU256().toInt128(); int256 candidateValue = unsquaredCandidateValue ** 2 / 1e18; for (uint256 j = i + 1; j < o.length; ) { - swap(o, i, j); + swap(o, i, j); // try the next element at position i + + (uint256 p_new, uint256 y_cached_new, uint256 x_cached_new) = P_cached(o, i, cachedY, cachedX); - int256 unsquaredValue = lnP0 - P(o, i).lnU256().toInt128(); - int256 value = unsquaredValue ** 2 / 1e18; + int256 unsquaredNewValue = lnP0 - p_new.lnU256().toInt128(); + int256 newCandidateValue = unsquaredNewValue ** 2 / 1e18; - if (value < candidateValue) { + if (newCandidateValue < candidateValue) { candidateIndex = j; - candidateValue = value; + candidateValue = newCandidateValue; + + y_cached = y_cached_new; + x_cached = x_cached_new; } - swap(o, j, i); + swap(o, j, i); // swap back unchecked { j++; @@ -68,6 +78,9 @@ contract ClvrModel { swap(o, i, candidateIndex); } + cachedY = y_cached; + cachedX = x_cached; + unchecked { i++; } @@ -100,12 +113,6 @@ contract ClvrModel { o[i2] = temp; } - // CONTRACT: note in TradeMinimal implemented! - function P(ClvrHook.SwapParamsExtended[] memory o, uint256 i) public view returns (uint256) { - uint256 base = 1e18; - return Y(o, i) * base / X(o, i); - } - function set_reserve_x(uint256 reserve_x) internal { reserveX = reserve_x; } @@ -116,42 +123,50 @@ contract ClvrModel { // INTERNAL FUNCTIONS - function y_out(ClvrHook.SwapParamsExtended[] memory o, uint256 i) private view returns (uint256) { + function y_out_cached(ClvrHook.SwapParamsExtended[] memory o, uint256 i, uint256 cachedY, uint256 cachedX) private pure returns (uint256) { if (direction(o[i]) == Direction.Sell) { - uint256 fraction = Y(o, i - 1) / (X(o, i - 1) + amountIn(o[i])); + uint256 fraction = cachedY / (cachedX + amountIn(o[i])); return fraction * amountIn(o[i]); } + return 0; } - function x_out(ClvrHook.SwapParamsExtended[] memory o, uint256 i) private view returns (uint256) { + function x_out_cached(ClvrHook.SwapParamsExtended[] memory o, uint256 i, uint256 cachedY, uint256 cachedX) private pure returns (uint256) { if (direction(o[i]) == Direction.Buy) { - uint256 fraction = X(o, i - 1) / (Y(o, i - 1) + amountIn(o[i])); + uint256 fraction = cachedX / (cachedY + amountIn(o[i])); return fraction * amountIn(o[i]); } return 0; } - function Y(ClvrHook.SwapParamsExtended[] memory o, uint256 i) private view returns (uint256) { + function Y_cached(ClvrHook.SwapParamsExtended[] memory o, uint256 i, uint256 cachedY, uint256 cachedX) private view returns (uint256) { if (i == 0) { return reserveY; } else if (i > 0 && direction(o[i]) == Direction.Buy) { - return Y(o, i - 1) + amountIn(o[i]); + return cachedY + amountIn(o[i]); } else if (i > 0 && direction(o[i]) == Direction.Sell) { - return Y(o, i - 1) - y_out(o, i); + return cachedY - y_out_cached(o, i, cachedY, cachedX); } - revert("Invalid call to Y"); + revert("Invalid call to Y_cached"); } - function X(ClvrHook.SwapParamsExtended[] memory o, uint256 i) private view returns (uint256) { + function X_cached(ClvrHook.SwapParamsExtended[] memory o, uint256 i, uint256 cachedY, uint256 cachedX) private view returns (uint256) { if (i == 0) { return reserveX; } else if (i > 0 && direction(o[i]) == Direction.Sell) { - return X(o, i - 1) + amountIn(o[i]); + return cachedX + amountIn(o[i]); } else if (i > 0 && direction(o[i]) == Direction.Buy) { - return X(o, i - 1) - x_out(o, i); + return cachedX - x_out_cached(o, i, cachedY, cachedX); } - revert("Invalid call to X"); + revert("Invalid call to X_cached"); + } + + /// @notice Returns the price, the new y and the new x after the swap + function P_cached(ClvrHook.SwapParamsExtended[] memory o, uint256 i, uint256 cachedY, uint256 cachedX) private view returns (uint256, uint256, uint256) { + uint256 y_cached_new = Y_cached(o, i, cachedY, cachedX); + uint256 x_cached_new = X_cached(o, i, cachedY, cachedX); + return (y_cached_new * 1e18 / x_cached_new, y_cached_new, x_cached_new); } function direction(ClvrHook.SwapParamsExtended memory o) private pure returns (Direction) { diff --git a/test/ClvrHook.t.sol b/test/ClvrHook.t.sol index c99b2ac..9f4d011 100644 --- a/test/ClvrHook.t.sol +++ b/test/ClvrHook.t.sol @@ -40,7 +40,9 @@ contract ClvrHookTest is Test, Deployers, Fixtures { PoolId poolId; ClvrHook hook; - address[10] users; + uint256 constant USERS_COUNT = 10; + + address[USERS_COUNT] users; uint256 tokenId; int24 tickLower; @@ -101,13 +103,9 @@ contract ClvrHookTest is Test, Deployers, Fixtures { users[0] = makeAddr("Alice"); users[1] = makeAddr("Bob"); users[2] = makeAddr("Carol"); - users[3] = makeAddr("3"); - users[4] = makeAddr("4"); - users[5] = makeAddr("5"); - users[6] = makeAddr("6"); - users[7] = makeAddr("7"); - users[8] = makeAddr("8"); - users[9] = makeAddr("9"); + for (uint256 i = 3; i < users.length; i++) { + users[i] = makeAddr(string(abi.encodePacked(i))); + } } function dealCurrencyToUsers() internal { diff --git a/test/ClvrModel.t.sol b/test/ClvrModel.t.sol index c6ca392..3681787 100644 --- a/test/ClvrModel.t.sol +++ b/test/ClvrModel.t.sol @@ -23,17 +23,15 @@ contract ClvrModelTest is Test { Box::new(Trade::new(size(2), TradeDirection::Sell)), */ function testModel() public { - ClvrHook.SwapParamsExtended[] memory o = new ClvrHook.SwapParamsExtended[](4); - o[0] = ClvrHook.SwapParamsExtended(address(0), address(0), IPoolManager.SwapParams(false, 0, 0)); // first one is mock (address = 0) - o[3] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(BUY, -10e18, 0)); - o[1] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(SELL, -5e18, 0)); - o[2] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(SELL, -2e18, 0)); - - ClvrHook.SwapParamsExtended[] memory expected = new ClvrHook.SwapParamsExtended[](4); - expected[0] = ClvrHook.SwapParamsExtended(address(0), address(0), IPoolManager.SwapParams(false, 0, 0)); - expected[1] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(SELL, -2e18, 0)); - expected[2] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(SELL, -5e18, 0)); - expected[3] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(BUY, -10e18, 0)); + ClvrHook.SwapParamsExtended[] memory o = new ClvrHook.SwapParamsExtended[](3); + o[2] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(BUY, -10e18, 0)); + o[0] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(SELL, -5e18, 0)); + o[1] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(SELL, -2e18, 0)); + + ClvrHook.SwapParamsExtended[] memory expected = new ClvrHook.SwapParamsExtended[](3); + expected[0] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(SELL, -2e18, 0)); + expected[1] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(SELL, -5e18, 0)); + expected[2] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(BUY, -10e18, 0)); uint256 gas = gasleft(); @@ -46,4 +44,28 @@ contract ClvrModelTest is Test { assertEq(o[i].params.amountSpecified, expected[i].params.amountSpecified); } } + + function testModelGasConsumption() public { + for (uint256 i = 10; i <= 400; i += 10) { + runSwaps(i); + } + } + + function runSwaps(uint256 swaps) internal { + ClvrHook.SwapParamsExtended[] memory o = new ClvrHook.SwapParamsExtended[](swaps); + + // create `swaps` alternating buys and sells + for (uint256 i = 0; i < swaps; i++) { + bool direction = i % 2 == 0 ? BUY : SELL; + o[i] = ClvrHook.SwapParamsExtended(address(1), address(1), IPoolManager.SwapParams(direction, -10e18, 0)); + } + + uint256 gas = gasleft(); + o = model.clvrReorder(1e18, o, 100e18, 100e18); + console.log("Gas (in dollars) used for ", swaps, " swaps: ", gasToDollars(gas - gasleft())); + } + + function gasToDollars(uint256 gas) internal pure returns (uint256) { + return gas * 1e9 * 30000 / 1e18; + } } \ No newline at end of file