SwapOrder.inverted

This commit is contained in:
tim
2024-10-27 23:52:21 -04:00
parent 1d118931cf
commit 64f1645764
11 changed files with 94 additions and 74 deletions

View File

@@ -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);

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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<items.length; i++) {
console2.log("index", i);
console2.log("exp: %x", uint256(items[i].fixedPoint));
int256 fixedPoint = IEEE754.toFixed(
items[i].floatingPoint, items[i].fixedBits
);
console2.log("got: %x", uint256(fixedPoint));
console2.log(IEEE754.isPositive(items[i].floatingPoint)?' positive':' negative');
require(items[i].fixedPoint == fixedPoint);
require(items[i].fixedPoint == fixedPoint, 'conversion mismatch!');
}
}

View File

@@ -44,7 +44,7 @@ contract TestOrder is MockEnv, Test {
uint256 amount = 100000000000000000000;
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
);
console2.logBytes(abi.encode(order));
@@ -81,7 +81,7 @@ contract TestExecute is TestOrder {
SwapOrder memory order = SwapOrder(
address(COIN), address(USD), // sell COIN for USD
Route(Exchange.UniswapV3, 500), amount, amount/100, false, false,
NO_CONDITIONAL_ORDER, tranches
false, NO_CONDITIONAL_ORDER, tranches
);
exactOutputOrderIndex = vault.numSwapOrders();
vault.placeDexorder(order);
@@ -95,7 +95,7 @@ contract TestExecute is TestOrder {
order = SwapOrder(
address(COIN), address(USD), // sell COIN for USD
Route(Exchange.UniswapV3, fee), amount, amount/100, true, false,
NO_CONDITIONAL_ORDER, tranches
false, NO_CONDITIONAL_ORDER, tranches
);
exactInputOrderIndex = vault.numSwapOrders();
vault.placeDexorder(order);
@@ -126,7 +126,7 @@ contract TestExecute is TestOrder {
SwapOrder memory order = SwapOrder(
token0, token1, // sell
Route(Exchange.UniswapV3, fee), amount, amount/100, true, false,
NO_CONDITIONAL_ORDER, tranches
false, NO_CONDITIONAL_ORDER, tranches
);
limitOrderIndex = vault.numSwapOrders();
vault.placeDexorder(order);