From 64f16457646a17b532c7dfefe9e7566c25b12fc7 Mon Sep 17 00:00:00 2001 From: tim Date: Sun, 27 Oct 2024 23:52:21 -0400 Subject: [PATCH] SwapOrder.inverted --- src/core/OrderLib.sol | 79 ++++++++++++++++++++----------------- src/core/OrderSpec.sol | 14 +++++-- src/core/Router.sol | 18 ++++----- src/core/UniswapSwapper.sol | 25 ++++++------ src/core/UniswapV3.sol | 5 +-- src/core/VaultImpl.sol | 2 +- src/interface/IRouter.sol | 8 ++-- src/more/VaultAddress.sol | 2 +- test/TestCancelOrder.sol | 2 +- test/TestIEEE754.sol | 5 ++- test/TestOrder.sol | 8 ++-- 11 files changed, 94 insertions(+), 74 deletions(-) diff --git a/src/core/OrderLib.sol b/src/core/OrderLib.sol index 4e82cf2..fe0cd7c 100644 --- a/src/core/OrderLib.sol +++ b/src/core/OrderLib.sol @@ -189,11 +189,11 @@ library OrderLib { _prepTrancheStatus(tranche,status.trancheStatus[t],startTime); if (tranche.minIsRatio || tranche.maxIsRatio) needStartPrice = true; -// require(!tranche.marketOrder || !tranche.minIntercept.isNegative(), 'NSL'); // negative slippage + require(!tranche.marketOrder || !tranche.minLine.intercept.isNegative(), 'NSL'); // negative slippage } // console2.log('fee/oco'); if (needStartPrice) - status.startPrice = router.protectedPrice(order.route.exchange, order.tokenIn, order.tokenOut, order.route.fee); + status.startPrice = router.protectedPrice(order.route.exchange, order.tokenIn, order.tokenOut, order.route.fee, order.inverted); } @@ -219,6 +219,7 @@ library OrderLib { SwapOrderStatus storage status = self.orders[orderIndex]; if (_isCanceled(self, orderIndex)) revert('NO'); // Not Open + SwapOrder storage order = status.order; Tranche storage tranche = status.order.tranches[trancheIndex]; TrancheStatus storage tStatus = status.trancheStatus[trancheIndex]; @@ -236,8 +237,8 @@ library OrderLib { // market order slippage control: we overload minLine.intercept to store slippage value if( tranche.marketOrder && !tranche.minLine.intercept.isZero() ) { // console2.log('slippage'); - uint256 protectedPrice = router.protectedPrice(status.order.route.exchange, status.order.tokenIn, - status.order.tokenOut, status.order.route.fee); + uint256 protectedPrice = router.protectedPrice(order.route.exchange, order.tokenIn, + order.tokenOut, order.route.fee, order.inverted); // minIntercept is interpreted as the slippage ratio uint256 slippage = uint256(tranche.minLine.intercept.toFixed(96)); v.limit = protectedPrice * 2**96 / (2**96+slippage); @@ -247,27 +248,29 @@ library OrderLib { } // line constraints + // price math is done in the linspace determined by order.inverted. else { v.price = 0; // check min line if( tranche.minLine.isEnabled() ) { - v.price = router.rawPrice(status.order.route.exchange, status.order.tokenIn, - status.order.tokenOut, status.order.route.fee); - // console2.log('price'); - // console2.log(v.price); - v.limit = tranche.minIsRatio ? + v.price = router.rawPrice(order.route.exchange, order.tokenIn, + order.tokenOut, order.route.fee, order.inverted); + // console2.log('price', v.price); + uint256 minPrice = tranche.minIsRatio ? tranche.minLine.ratioPrice(status.startTime, status.startPrice) : tranche.minLine.priceNow(); // console2.log('min line limit', v.limit); // console2.log('price', v.price); - require( v.price > v.limit, 'LL' ); + require( v.price > minPrice, 'LL' ); + if ((order.tokenIn < order.tokenOut) != order.inverted) + v.limit = minPrice; } // check max line if( tranche.maxLine.isEnabled()) { // price may have been already initialized by the min line if( v.price == 0 ) { // don't look it up a second time if we already have it. - v.price = router.rawPrice(status.order.route.exchange, status.order.tokenIn, - status.order.tokenOut, status.order.route.fee); + v.price = router.rawPrice(order.route.exchange, order.tokenIn, + order.tokenOut, order.route.fee, order.inverted); // console2.log('price'); // console2.log(v.price); } @@ -277,11 +280,13 @@ library OrderLib { // console2.log('max line limit'); // console2.log(maxPrice); require( v.price <= maxPrice, 'LU' ); + if ((order.tokenIn > order.tokenOut) != order.inverted) + v.limit = maxPrice; } } // compute size - v.trancheAmount = status.order.amount * tranche.fraction / MAX_FRACTION; // the most this tranche could do + v.trancheAmount = order.amount * tranche.fraction / MAX_FRACTION; // the most this tranche could do v.amount = v.trancheAmount - tStatus.filled; // minus tranche fills if (tranche.rateLimitFraction != 0) { // rate limit sizing @@ -290,36 +295,35 @@ library OrderLib { v.amount = v.limitedAmount; } // order amount remaining - v.remaining = status.order.amount - status.filled; + v.remaining = order.amount - status.filled; if (v.amount > v.remaining) // not more than the order's overall remaining amount v.amount = v.remaining; - require( v.amount >= status.order.minFillAmount, 'TF' ); - address recipient = status.order.outputDirectlyToOwner ? owner : address(this); - IERC20 outToken = IERC20(status.order.tokenOut); + require( v.amount >= order.minFillAmount, 'TF' ); + address recipient = order.outputDirectlyToOwner ? owner : address(this); + IERC20 outToken = IERC20(order.tokenOut); // this variable is only needed for calculating the amount to forward to a conditional order, so we set it to 0 otherwise - uint256 startingTokenOutBalance = status.order.conditionalOrder == NO_CONDITIONAL_ORDER ? 0 : outToken.balanceOf(address(this)); + uint256 startingTokenOutBalance = order.conditionalOrder == NO_CONDITIONAL_ORDER ? 0 : outToken.balanceOf(address(this)); // // Order has been approved. Send to router for swap execution. // // console2.log('router request:'); - // console2.log(status.order.tokenIn); - // console2.log(status.order.tokenOut); + // console2.log(order.tokenIn); + // console2.log(order.tokenOut); // console2.log(recipient); // console2.log(v.amount); - // console2.log(status.order.minFillAmount); - // console2.log(status.order.amountIsInput); + // console2.log(order.minFillAmount); + // console2.log(order.amountIsInput); // console2.log(v.limit); - // console2.log(status.order.route.fee); + // console2.log(order.route.fee); IRouter.SwapParams memory swapParams = IRouter.SwapParams( - status.order.tokenIn, status.order.tokenOut, recipient, - v.amount, status.order.minFillAmount, status.order.amountIsInput, - v.limit, status.order.route.fee); + order.route.exchange, order.tokenIn, order.tokenOut, recipient, + v.amount, order.minFillAmount, order.amountIsInput, + order.inverted, v.limit, order.route.fee); // DELEGATECALL (bool success, bytes memory result) = address(router).delegatecall( - abi.encodeWithSelector(IRouter.swap.selector, status.order.route.exchange, swapParams) - ); + abi.encodeWithSelector(IRouter.swap.selector, swapParams)); if (!success) { if (result.length > 0) { // if there was a reason given, forward it assembly ("memory-safe") { @@ -333,8 +337,10 @@ library OrderLib { // delegatecall succeeded (v.amountIn, amountOut) = abi.decode(result, (uint256, uint256)); + // console2.log('swapped'); + // Update filled amounts - v.amount = status.order.amountIsInput ? v.amountIn : amountOut; + v.amount = order.amountIsInput ? v.amountIn : amountOut; status.filled += v.amount; tStatus.filled += v.amount; @@ -348,24 +354,25 @@ library OrderLib { v.fillFee = amountOut * status.fillFeeHalfBps / 20_000; outToken.transfer(feeManager.fillFeeAccount(), v.fillFee); - emit DexorderSwapFilled(orderIndex, trancheIndex, v.amountIn, amountOut, v.fillFee, - tStatus.activationTime); + emit DexorderSwapFilled(orderIndex, trancheIndex, v.amountIn, amountOut, v.fillFee, tStatus.activationTime); // Conditional order placement // Fees for conditional orders are taken up-front by the VaultImpl and are not charged here. - if (status.order.conditionalOrder != NO_CONDITIONAL_ORDER) { + if (order.conditionalOrder != NO_CONDITIONAL_ORDER) { // the conditional order index will have been converted to an absolute index during placement - SwapOrder memory condi = self.orders[status.order.conditionalOrder].order; + SwapOrder memory condi = self.orders[order.conditionalOrder].order; // the amount forwarded will be different than amountOut due to our fee and possible token transfer taxes condi.amount = outToken.balanceOf(address(this)) - startingTokenOutBalance; // fillFee is preserved - uint64 condiOrderIndex = _createOrder(self, condi, status.fillFeeHalfBps, NO_OCO_INDEX, router, status.order.conditionalOrder); + uint64 condiOrderIndex = _createOrder( + self, condi, status.fillFeeHalfBps, + NO_OCO_INDEX, router, order.conditionalOrder); emit DexorderSwapPlaced(condiOrderIndex, 1, 0, 0); // zero fees } // Check order completion and OCO canceling - uint256 remaining = status.order.amount - status.filled; - if( remaining < status.order.minFillAmount ) { + uint256 remaining = order.amount - status.filled; + if( remaining < order.minFillAmount ) { // we already get fill events so completion may be inferred without an extra Completion event if( status.ocoGroup != NO_OCO_INDEX) _cancelOco(self, status.ocoGroup); diff --git a/src/core/OrderSpec.sol b/src/core/OrderSpec.sol index b164448..2e507d8 100644 --- a/src/core/OrderSpec.sol +++ b/src/core/OrderSpec.sol @@ -49,6 +49,13 @@ struct SwapOrder { uint256 minFillAmount; // if a tranche has less than this amount available to fill, it is considered completed bool amountIsInput; // whether amount is an in or out amount bool outputDirectlyToOwner; // whether the swap proceeds should go to the vault, or directly to the vault owner + + // Tranche prices are expressed as either inToken/outToken or outToken/inToken depending on this `inverted` flag. + // A line in one space is a curve in the other, so the specification of e.g. WETH/USDC or USDC/WETH is essential. + // The "natural" ordering of inverted=false follows Uniswap: the lower-address token is the base currency and the + // higher-address token is the quote. + bool inverted; + uint64 conditionalOrder; // use NO_CONDITIONAL_ORDER for normal orders. If the high bit is set, the order number is relative to the currently placed group of orders. e.g. `CONDITIONAL_ORDER_IN_CURRENT_GROUP & 2` refers to the third item in the order group currently being placed. Tranche[] tranches; // see Tranche below } @@ -87,8 +94,9 @@ struct Tranche { uint32 endTime; // use DISTANT_FUTURE to effectively disable // If intercept and slope are both 0, the line is disabled - // Prices are always in terms of outputToken as the quote currency: the output amount per input amount. This is - // equivalent to saying all orders are viewed as sells relative to the price. + // Prices are expressed as either inToken/outToken or outToken/inToken depending on the order `inverted` flag. + // A line in one space is a curve in the other, so the specification of e.g. WETH/USDC or USDC/WETH is critical + // The minLine is equivalent to a traditional limit order constraint, except this limit line can be diagonal. Line minLine; // The maxLine will be relatively unused, since it represents a boundry on TOO GOOD of a price. @@ -117,4 +125,4 @@ struct OcoGroup { OcoMode mode; uint64 startIndex; // starting orderIndex of the group uint8 num; // number of orders in the group -} \ No newline at end of file +} diff --git a/src/core/Router.sol b/src/core/Router.sol index d5d4fa3..be0230e 100644 --- a/src/core/Router.sol +++ b/src/core/Router.sol @@ -16,25 +16,25 @@ contract Router is IRouter, UniswapV3Swapper { } - function rawPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee) external view + function rawPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee, bool inverted) external view returns (uint256) { if (exchange == Exchange.UniswapV3) - return _univ3_rawPrice(tokenIn, tokenOut, maxFee); - revert('UR'); + return _univ3_rawPrice(tokenIn, tokenOut, maxFee, inverted); + revert('UR'); // Unknown Route } - function protectedPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee) external view + function protectedPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee, bool inverted) external view returns (uint256) { if (exchange == Exchange.UniswapV3) - return _univ3_protectedPrice(tokenIn, tokenOut, maxFee); - revert('UR'); + return _univ3_protectedPrice(tokenIn, tokenOut, maxFee, inverted); + revert('UR'); // Unknown Route } - function swap( Exchange exchange, SwapParams memory params ) external + function swap( SwapParams memory params ) external returns (uint256 amountIn, uint256 amountOut) { - if (exchange == Exchange.UniswapV3) + if (params.exchange == Exchange.UniswapV3) return _univ3_swap(params); - revert('UR'); + revert('UR'); // Unknown Route } } diff --git a/src/core/UniswapSwapper.sol b/src/core/UniswapSwapper.sol index a2871fb..84c8856 100644 --- a/src/core/UniswapSwapper.sol +++ b/src/core/UniswapSwapper.sol @@ -28,21 +28,22 @@ contract UniswapV3Swapper { oracleSeconds = oracleSeconds_; } - function _univ3_rawPrice(address tokenIn, address tokenOut, uint24 maxFee) internal view + function _univ3_rawPrice(address tokenIn, address tokenOut, uint24 maxFee, bool inverted) internal view returns (uint256 price) { - (IUniswapV3Pool pool, bool inverted) = UniswapV3.getPool(factory, tokenIn, tokenOut, maxFee); + IUniswapV3Pool pool = UniswapV3.getPool(factory, tokenIn, tokenOut, maxFee); (uint160 sqrtPriceX96,,,,,,) = pool.slot0(); return Util.sqrtToPrice(sqrtPriceX96, inverted); } // Returns the stabilized (oracle) price - function _univ3_protectedPrice(address tokenIn, address tokenOut, uint24 maxFee) internal view + function _univ3_protectedPrice(address tokenIn, address tokenOut, uint24 maxFee, bool inverted) internal view returns (uint256) { // console2.log('oracle'); // console2.log(oracleSeconds); + IUniswapV3Pool pool = UniswapV3.getPool(factory, tokenIn, tokenOut, maxFee); + uint160 sqrtPriceX96; if (oracleSeconds!=0){ - (IUniswapV3Pool pool, bool inverted) = UniswapV3.getPool(factory, tokenIn, tokenOut, maxFee); uint32[] memory secondsAgos = new uint32[](2); secondsAgos[0] = oracleSeconds; secondsAgos[1] = 0; @@ -53,7 +54,7 @@ contract UniswapV3Swapper { if (delta < 0 && (delta % secsI != 0)) mean--; // use Uniswap's tick-to-sqrt-price because it's verified - uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(mean); + sqrtPriceX96 = TickMath.getSqrtRatioAtTick(mean); return Util.sqrtToPrice(sqrtPriceX96, inverted); } catch Error( string memory /*reason*/ ) { @@ -61,20 +62,22 @@ contract UniswapV3Swapper { // console2.log('oracle broken'); } } - return _univ3_rawPrice(tokenIn, tokenOut, maxFee); + // no oracle available. use the raw pool price. + (sqrtPriceX96,,,,,,) = pool.slot0(); + return Util.sqrtToPrice(sqrtPriceX96, inverted); } function _univ3_swap(IRouter.SwapParams memory params) internal returns (uint256 amountIn, uint256 amountOut) { if( params.limitPriceX96 != 0 ) { - bool inverted = params.tokenIn > params.tokenOut; - if (inverted) { + // convert to output/input which is what the _univ3_* methods expect + bool inputInverted = params.tokenIn > params.tokenOut; + if (params.inverted!=inputInverted) { // console2.log('inverting params.limitPriceX96'); // console2.log(params.limitPriceX96); - params.limitPriceX96 = 2**96 * 2**96 / params.limitPriceX96; + params.limitPriceX96 = Util.invertX96(params.limitPriceX96); } - // console2.log('params.limitPriceX96'); - // console2.log(params.limitPriceX96); + // console2.log('params.limitPriceX96', params.limitPriceX96); } if (params.amountIsInput) (amountIn, amountOut) = _univ3_swapExactInput(params); diff --git a/src/core/UniswapV3.sol b/src/core/UniswapV3.sol index 49c3f9e..c39612a 100644 --- a/src/core/UniswapV3.sol +++ b/src/core/UniswapV3.sol @@ -10,10 +10,9 @@ import {IWETH9} from "../../lib_uniswap/v3-periphery/contracts/interfaces/extern library UniswapV3 { function getPool( IUniswapV3Factory factory, address tokenA, address tokenB, uint24 fee) internal pure - returns (IUniswapV3Pool pool, bool inverted) { + returns (IUniswapV3Pool) { PoolAddress.PoolKey memory key = PoolAddress.getPoolKey(tokenA, tokenB, fee); - pool = IUniswapV3Pool(PoolAddress.computeAddress(address(factory), key)); - inverted = tokenA > tokenB; + return IUniswapV3Pool(PoolAddress.computeAddress(address(factory), key)); } } diff --git a/src/core/VaultImpl.sol b/src/core/VaultImpl.sol index 1353ddc..fb463ce 100644 --- a/src/core/VaultImpl.sol +++ b/src/core/VaultImpl.sol @@ -21,7 +21,7 @@ import {UniswapV3Arbitrum} from "./UniswapV3.sol"; contract VaultImpl is IVaultImpl, VaultStorage { - uint256 constant public version = 1; + uint256 constant public version = 2; IFeeManager public immutable feeManager; IRouter private immutable router; diff --git a/src/interface/IRouter.sol b/src/interface/IRouter.sol index ba336d0..1b6b263 100644 --- a/src/interface/IRouter.sol +++ b/src/interface/IRouter.sol @@ -6,25 +6,27 @@ import "../core/OrderSpec.sol"; interface IRouter { // Returns the current price of the pool for comparison with limit lines. - function rawPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee) external view + function rawPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee, bool inverted) external view returns (uint256); // Returns the oracle price, with protections against fast moving price changes (typically used in comparisons to slippage price) - function protectedPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee) external view + function protectedPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee, bool inverted) external view returns (uint256); struct SwapParams { + Exchange exchange; address tokenIn; address tokenOut; address recipient; uint256 amount; uint256 minAmount; bool amountIsInput; + bool inverted; uint256 limitPriceX96; uint24 maxFee; } - function swap( Exchange exchange, SwapParams memory params ) external + function swap( SwapParams memory params ) external returns (uint256 amountIn, uint256 amountOut); } diff --git a/src/more/VaultAddress.sol b/src/more/VaultAddress.sol index 28da8c7..31ac9b4 100644 --- a/src/more/VaultAddress.sol +++ b/src/more/VaultAddress.sol @@ -6,7 +6,7 @@ import "@forge-std/console2.sol"; library VaultAddress { // keccak-256 hash of the Vault's bytecode (not the deployed bytecode but the initialization bytecode) - bytes32 public constant VAULT_INIT_CODE_HASH = 0x8b1347850b0b1f2e05548c065af07c78f2c0617f70a2915b3cb7e0ba1bd20630; + bytes32 public constant VAULT_INIT_CODE_HASH = 0xda672cdca096de00f3fed8150430564c059a59ad30cb2c824902097e25cd8b3a; // the contract being constructed must not have any constructor arguments or the determinism will be broken. // instead, use a callback to get construction arguments diff --git a/test/TestCancelOrder.sol b/test/TestCancelOrder.sol index ab24390..7093e51 100644 --- a/test/TestCancelOrder.sol +++ b/test/TestCancelOrder.sol @@ -39,7 +39,7 @@ contract TestCancelOrder is MockEnv, Test { SwapOrder memory order = SwapOrder( 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9, 0x82aF49447D8a07e3bd95BD0d56f35241523fBab1, - Route(Exchange.UniswapV3, 500), amount, amount/100, true, false, + Route(Exchange.UniswapV3, 500), amount, amount/100, true, false, false, NO_CONDITIONAL_ORDER, tranches ); vault.placeDexorder(order); diff --git a/test/TestIEEE754.sol b/test/TestIEEE754.sol index b66c93c..10e8943 100644 --- a/test/TestIEEE754.sol +++ b/test/TestIEEE754.sol @@ -67,17 +67,18 @@ contract TestIEEE754 is Test { Item(float.wrap(0x80200000), 128, int256(uint256(int256(-0x1)))), // smallest negative is subnormal Item(float.wrap(0x7effffff), 128, int256(uint256(0x7fffff8000000000000000000000000000000000000000000000000000000000))), // largest positive Item(float.wrap(0xff7fffff), 128, -int256(uint256(0xffffff0000000000000000000000000000000000000000000000000000000000))), // largest negative - Item(float.wrap(0x7f7fffff), 120, int256(uint256(0xffffff0000000000000000000000000000000000000000000000000000000000))) + Item(float.wrap(0x7f7fffff), 128, int256(uint256(0xffffff0000000000000000000000000000000000000000000000000000000000))) ]; for (uint i=0; i