From 03b7ac11e31f0a633768ea1c8adc911183f09dc4 Mon Sep 17 00:00:00 2001 From: Tim Olson <> Date: Wed, 1 Nov 2023 17:56:25 -0400 Subject: [PATCH] removed PriceConstraint and implemented LineConstraint; MockDeploy copied/separated from Deploy --- bin/mock.sh | 2 +- docs/errors.md | 23 ++++++++++--------- script/Deploy.sol | 7 +----- script/DeployMock.sol | 31 +++++++++++++++++++++++++ src/OrderLib.sol | 53 +++++++++++++++++++------------------------ test/MockEnv.sol | 19 ++++++++++++++++ test/TestOrder.sol | 36 ++++++++++++++++++++++++++++- 7 files changed, 122 insertions(+), 49 deletions(-) create mode 100644 script/DeployMock.sol diff --git a/bin/mock.sh b/bin/mock.sh index e44b5c2..4a587c9 100755 --- a/bin/mock.sh +++ b/bin/mock.sh @@ -11,7 +11,7 @@ anvil -f arbitrum_mock --chain-id 31337 & ANVIL_PID=$! sleep 2 -forge script script/Deploy.sol -vvvv --fork-url http://localhost:8545 --broadcast +forge script script/DeployMock.sol -vvvv --fork-url http://localhost:8545 --broadcast trap_ctrlc() { echo exiting anvil diff --git a/docs/errors.md b/docs/errors.md index ca739ea..5b6ee07 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -1,11 +1,12 @@ -| Code | Name | Description | -|------|---------------------------|------------------------------------------------------------------------| -| OCOM | Invalid OCO Mode | The OCO mode provided to placeOrder() is invalid. | -| UR | Unknown Route | The specified order route is invalid. | -| NO | Not Open | Order status state is not OPEN | -| UC | Unknown Constraint | The constraint specification did not have a recognized Constraint Mode | -| TE | Too Early | Time constraint window hasn't opened yet | -| TL | Too Late | Time constraint has expired the tranche | -| L | Limit | Price limit constraint violation | -| IIA | Insufficient Input Amount | Not enough input coin available in the vault (from Uniswap) | -| TF | Tranche Filled | The tranche has no remaining amount available to execute. | +| Code | Name | Description | +|----------------------|---------------------------|----------------------------------------------------------------------------------------------------------------------| +| OCOM | Invalid OCO Mode | The OCO mode provided to placeOrder() is invalid. | +| UR | Unknown Route | The specified order route is invalid. | +| NO | Not Open | Order status state is not OPEN | +| UC | Unknown Constraint | The constraint specification did not have a recognized Constraint Mode | +| TE | Too Early | Time constraint window hasn't opened yet | +| TL | Too Late | Time constraint has expired the tranche | +| L | Limit | Price limit constraint violation | +| IIA | Insufficient Input Amount | Not enough input coin available in the vault (from Uniswap) | +| TF | Tranche Filled | The tranche has no remaining amount available to execute. | +| Too little received | Too little received | Uniswap v3 error when min output amount is not filled. Can happen when a limit price is very near the current price. | diff --git a/script/Deploy.sol b/script/Deploy.sol index 1ad0e08..ffb1f0a 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: UNLICENSED -// pragma solidity =0.7.6; pragma solidity >=0.8.0; import "forge-std/Script.sol"; @@ -13,11 +12,9 @@ contract Deploy is Script { function run() external { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); -// Factory deployer = new Factory{salt:keccak256(abi.encode(1))}(); // version 1 - Factory deployer = new Factory(); // hardhat often breaks on the CREATE2 above :( + Factory deployer = new Factory{salt:keccak256(abi.encode(1))}(); // version 1 QueryHelper query = new QueryHelper(); Dexorder dexorder = new Dexorder(); -// MockEnv mock = new MockEnv(); vm.stopBroadcast(); console2.log('Factory'); console2.log(address(deployer)); @@ -25,7 +22,5 @@ contract Deploy is Script { console2.log(address(query)); console2.log('Dexorder'); console2.log(address(dexorder)); -// console2.log('MockEnv'); // todo no mock in production deployment -// console2.log(address(mock)); } } diff --git a/script/DeployMock.sol b/script/DeployMock.sol new file mode 100644 index 0000000..990245c --- /dev/null +++ b/script/DeployMock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +import "forge-std/Script.sol"; +import "forge-std/console2.sol"; +import "../src/QueryHelper.sol"; +import "../src/Factory.sol"; +import "../src/Dexorder.sol"; +import "../test/MockEnv.sol"; + +contract DeployMock is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + // hardhat often breaks on the CREATE2 so we disable it for mock + // Factory deployer = new Factory{salt:keccak256(abi.encode(1))}(); // version 1 + Factory deployer = new Factory(); + QueryHelper query = new QueryHelper(); + Dexorder dexorder = new Dexorder(); + MockEnv mock = new MockEnv(); + vm.stopBroadcast(); + console2.log('Factory'); + console2.log(address(deployer)); + console2.log('QueryHelper'); + console2.log(address(query)); + console2.log('Dexorder'); + console2.log(address(dexorder)); + console2.log('MockEnv'); + console2.log(address(mock)); + } +} diff --git a/src/OrderLib.sol b/src/OrderLib.sol index 0b5b8fc..0421d84 100644 --- a/src/OrderLib.sol +++ b/src/OrderLib.sol @@ -66,21 +66,13 @@ library OrderLib { enum ConstraintMode { Time, - Limit, - Trailing, - Barrier, - Line + Line, + Barrier } struct Constraint { ConstraintMode mode; // type information - bytes constraint; // abi packed-encoded constraint struct: decode according to mode - } - - struct PriceConstraint { - bool isAbove; - bool isRatio; - uint160 valueSqrtX96; + bytes constraint; // abi-encoded constraint struct: decode according to mode } struct LineConstraint { @@ -217,35 +209,36 @@ library OrderLib { if (time < block.timestamp) revert('TL'); // time late } - else if (constraint.mode == ConstraintMode.Limit) { - console2.log('limit constraint'); - if( sqrtPriceX96 == 0 ) { + else if (constraint.mode == ConstraintMode.Line) { + console2.log('line constraint'); + if( sqrtPriceX96 == 0 ) (sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0(); - } - PriceConstraint memory pc = abi.decode(constraint.constraint, (PriceConstraint)); + LineConstraint memory lc = abi.decode(constraint.constraint, (LineConstraint)); uint256 price = sqrtPriceX96; - if( pc.isRatio ) - pc.valueSqrtX96 = uint160(price * pc.valueSqrtX96 / 2**96); // todo overflow check! - if( pc.isAbove && price < pc.valueSqrtX96 || !pc.isAbove && price > pc.valueSqrtX96 ) + if( lc.isRatio ) + revert('ratio not implemented'); // todo the ratio must be computed when the order is placed + // c.valueSqrtX96 = uint160(price * c.valueSqrtX96 / 2**96); + int256 limit256 = int256(uint256(lc.valueSqrtX96)); + if( lc.slopeSqrtX96 != 0 ) { + limit256 += int256(block.timestamp - lc.time) * lc.slopeSqrtX96 / 2**96; + if( limit256 < 0 ) + limit256 = 0; + } + console2.log(limit256); + uint160 limit = uint160(uint256(limit256)); + // use <= and >= here because trading AT the limit results in 0 volume. price must exceed the limit. + if( lc.isAbove && price <= limit || !lc.isAbove && price >= limit ) revert('L'); if( sqrtPriceLimitX96 == 0 || - pc.isAbove && pc.valueSqrtX96 < sqrtPriceLimitX96 || - !pc.isAbove && pc.valueSqrtX96 > sqrtPriceLimitX96 + lc.isAbove && limit < sqrtPriceLimitX96 || + !lc.isAbove && limit > sqrtPriceLimitX96 ) - sqrtPriceLimitX96 = pc.valueSqrtX96; + sqrtPriceLimitX96 = limit; } else if (constraint.mode == ConstraintMode.Barrier) { console2.log('barrier constraint'); revert('NI'); // not implemented } - else if (constraint.mode == ConstraintMode.Trailing) { - console2.log('trailing constraint'); - revert('NI'); // not implemented - } - else if (constraint.mode == ConstraintMode.Line) { - console2.log('line constraint'); - revert('NI'); // not implemented - } else // unknown constraint revert('UC'); // not implemented } diff --git a/test/MockEnv.sol b/test/MockEnv.sol index d46b1af..c879f2d 100644 --- a/test/MockEnv.sol +++ b/test/MockEnv.sol @@ -142,4 +142,23 @@ contract MockEnv { ); return swapper.exactInputSingle(params); } + + function price() public view returns (uint160 sqrtPrice) { + (sqrtPrice,,,,,,) = pool.slot0(); + } + + function swapToPrice(uint160 sqrtPriceLimitX96) public { + console2.log('swapToPrice'); + console2.log(sqrtPriceLimitX96); + uint160 curPrice = price(); + console2.log(curPrice); + if( curPrice == sqrtPriceLimitX96 ) + return; + MockERC20 inToken = curPrice > sqrtPriceLimitX96 ? MockERC20(token0) : MockERC20(token1); + MockERC20 outToken = curPrice < sqrtPriceLimitX96 ? MockERC20(token0) : MockERC20(token1); + // instead of calculating how much we need, we just mint an absurd amount + uint256 aLot = 2**100; + inToken.mint(address(this), aLot); + swap(inToken, outToken, aLot, sqrtPriceLimitX96); + } } diff --git a/test/TestOrder.sol b/test/TestOrder.sol index a6e8960..9333460 100644 --- a/test/TestOrder.sol +++ b/test/TestOrder.sol @@ -77,7 +77,7 @@ contract TestOrder is MockEnv, Test { COIN.mint(address(vault), amount); // create COIN to sell OrderLib.SwapOrder memory order = OrderLib.SwapOrder( address(COIN), address(USD), // sell COIN for USD - OrderLib.Route(OrderLib.Exchange.UniswapV3, 500), amount, true, false, + OrderLib.Route(OrderLib.Exchange.UniswapV3, fee), amount, true, false, OrderLib.NO_CHAIN, tranches ); uint64 orderIndex = vault.numSwapOrders(); @@ -88,4 +88,38 @@ contract TestOrder is MockEnv, Test { console2.log('executed'); } + + function testExecuteLimitOrder() public { + // test selling token0 above a certain price + OrderLib.Tranche[] memory tranches = new OrderLib.Tranche[](1); + OrderLib.Constraint[] memory constraints1 = new OrderLib.Constraint[](1); + uint160 limit = price() * 10001 / 10000; // 1bp above the current price + bytes memory serialized = abi.encode( OrderLib.LineConstraint(true, false, 0, limit, 0) ); + constraints1[0] = OrderLib.Constraint(OrderLib.ConstraintMode.Line, serialized); + tranches[0] = OrderLib.Tranche(type(uint16).max,constraints1); + MockERC20 token = MockERC20(token0); + uint256 amount = 3*10**token.decimals() / 10; // selling 0.3 token0 + token.mint(address(vault), amount); + OrderLib.SwapOrder memory order = OrderLib.SwapOrder( + token0, token1, // sell + OrderLib.Route(OrderLib.Exchange.UniswapV3, fee), amount, true, false, + OrderLib.NO_CHAIN, tranches + ); + uint64 orderIndex = vault.numSwapOrders(); + vault.placeOrder(order); + console2.log('placed order'); + console2.log(uint(orderIndex)); + + vm.expectRevert(bytes('L')); + vault.execute(orderIndex, 0, OrderLib.PriceProof(0)); // should revert with code 'L' + console2.log('successfully failed to execute below limit price'); + + swapToPrice(limit); // move price to exactly the limit + vm.expectRevert(bytes('L')); // the limit is violated. no liquidity can be taken without moving the price. + vault.execute(orderIndex, 0, OrderLib.PriceProof(0)); // should work now that the price is high enough + + swapToPrice(limit*10001/10000); // move price to be 1bp abouve our limit + console2.log('successfully executed at limit price'); + } + }