From 2925a1cc0c14c08ce8b04622ece16b7933806eb0 Mon Sep 17 00:00:00 2001 From: Tim Olson <> Date: Thu, 21 Sep 2023 16:51:57 -0400 Subject: [PATCH] vault/orderlib initial impl compiles --- docs/errors.md | 4 + foundry.toml | 15 ++- script/Deploy.sol | 3 + src/Factory.sol | 5 +- src/OrderLib.sol | 250 ++++++++++++++++++++++++++++++++++++++--- src/TimedOrder.sol | 171 ---------------------------- src/UniswapSwapper.sol | 2 +- src/Vault.sol | 37 ++++-- src/VaultDeployer.sol | 2 +- 9 files changed, 285 insertions(+), 204 deletions(-) create mode 100644 docs/errors.md delete mode 100644 src/TimedOrder.sol diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..b9e5f80 --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,4 @@ +| Code | Name | Description | +|------|--------------------|------------------------------------------------------------------------| +| UC | Unknown Constraint | The constraint specification did not have a recognized Constraint Mode | +| OCOM | Invalid OCO Mode | The OCO mode provided to placeOrder() is invalid. | \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 16189a3..fc1de4c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,12 +6,21 @@ remappings = [ '@uniswap/v3-core/=lib/v3-core/', '@uniswap/v3-periphery/=lib/v3-periphery/', ] -optimizer=true -optimizer_runs=999999999 sizes = true -via_ir = false gas_reports = ['*'] gas_reports_ignore = [] +via_ir = false +optimizer=true +optimizer_runs=999999999 + +[profile.default.optimizer_details] +constantOptimizer = true +yul = true + +[profile.default.optimizer_details.yulDetails] +stackAllocation = true +#optimizerSteps = 'dhfoDgvulfnTUtnIf' +optimizerSteps = 'dhfoD[xarrscLMcCTU]uljmul' [profile.default.rpc_endpoints] # todo put these into a secrets file diff --git a/script/Deploy.sol b/script/Deploy.sol index 68fcd84..eb960d0 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -3,6 +3,7 @@ pragma solidity =0.7.6; import "forge-std/Script.sol"; import "../src/VaultDeployer.sol"; +import "forge-std/console2.sol"; contract Deploy is Script { function run() external { @@ -10,5 +11,7 @@ contract Deploy is Script { vm.startBroadcast(deployerPrivateKey); VaultDeployer deployer = new VaultDeployer{salt:keccak256(abi.encode(1))}(); vm.stopBroadcast(); + console2.log('VaultDeployer'); + console2.log(address(deployer)); } } diff --git a/src/Factory.sol b/src/Factory.sol index e6d0d80..d1e2083 100644 --- a/src/Factory.sol +++ b/src/Factory.sol @@ -3,9 +3,8 @@ pragma solidity =0.7.6; import "v3-core/contracts/UniswapV3Factory.sol"; import "./VaultDeployer.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; pragma abicoder v2; -contract Factory is VaultDeployer, Ownable { - +contract Factory is VaultDeployer { + // todo owner } diff --git a/src/OrderLib.sol b/src/OrderLib.sol index 0cbe646..57cb804 100644 --- a/src/OrderLib.sol +++ b/src/OrderLib.sol @@ -1,10 +1,23 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity =0.7.6; +pragma abicoder v2; + +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "./UniswapSwapper.sol"; + library OrderLib { uint64 internal constant NO_CHAIN = type(uint64).max; - uint8 internal constant NUM_OCO_GROUPS = 6; + uint64 internal constant NO_OCO = type(uint64).max; + + event DexorderPlaced (uint64 startOrderIndex, uint8 numOrders); + + event DexorderSwapFilled (uint64 orderIndex, uint8 trancheIndex, uint256 amountIn, uint256 amountOut); + + event DexorderCompleted (uint64 orderIndex); + + event DexorderError (uint64 orderIndex, string reason); enum SwapOrderState { Open, Canceled, Filled, Template @@ -17,35 +30,48 @@ library OrderLib { uint256 amount; bool amountIsInput; bool outputDirectlyToOwner; - Tranche[] tranches; uint64 chainOrder; // use NO_CHAIN for no chaining. chainOrder index must be < than this order's index for safety (written first) and chainOrder state must be Template + Tranche[] tranches; } struct SwapOrderStatus { - SwapOrderState state; SwapOrder order; - uint256 filled; - uint256 net; // received after fees, conversions, taxes, etc - bool[NUM_OCO_GROUPS] ocoTriggered; // if true then the group has been canceled + SwapOrderState state; + uint32 start; + uint64 ocoGroup; + uint256 filledIn; + uint256 filledOut; } enum ConstraintMode { + Time, Limit, - Barrier, Trailing, - Time + Barrier, + Line + } + + struct Constraint { + ConstraintMode mode; + bytes constraint; // abi-encoded constraint struct } struct PriceConstraint { - PriceConstraintMode mode; bool isAbove; bool isRatio; uint160 valueSqrtX96; } + struct LineConstraint { + bool isAbove; + bool isRatio; + uint32 time; + uint160 valueSqrtX96; + int160 slopeSqrtX96; // price change per second + } enum TimeMode { - Timestamp, // absolute timestamp + Timestamp, // absolute timestamp SinceOrderStart // relative to order creation (useful for chained orders) } @@ -54,17 +80,207 @@ library OrderLib { uint32 time; } - Time constant DISTANT_PAST = Time(TimeMode.Timestamp, 0); - Time constant DISTANT_FUTURE = Time(TimeMode.Timestamp, type(uint32).max); + uint32 constant DISTANT_PAST = 0; + uint32 constant DISTANT_FUTURE = type(uint32).max; - uint8 internal constant NO_OCO = 255; + struct TimeConstraint { + Time earliest; + Time latest; + } struct Tranche { uint64 fraction; // 18-decimal fraction of the order amount which is available to this tranche. must be <= 1 - uint8 ocoGroup; // 0-5 are six valid groups, indexing ocoTriggered. use NO_OCO to disable oco functionality. - Time earliest; // earliest block timestamp for execution. use DISTANT_PAST to disable - Time latest; // latest block timestamp for execution (inclusive). use DISTANT_FUTURE to disable - PriceConstraint[] constraints; + Constraint[] constraints; } + struct PriceProof { + // todo + uint proof; + } + + enum OcoMode { + NO_OCO, + CANCEL_ON_PARTIAL_FILL, + CANCEL_ON_COMPLETION + } + + struct OcoGroup { + OcoMode mode; + uint64 startIndex; // starting orderIndex of the group + uint8 num; // number of orders in the group + } + + struct OrdersInfo { + bool _ignored; // workaround for Solidity bug where a public struct member cannot start with an array of uncertain size + SwapOrderStatus[] orders; + OcoGroup[] ocoGroups; // each indexed OCO group is an array of orderIndexes of orders in the oco group. + } + + + function _placeOrder(OrdersInfo storage self, SwapOrder memory order) internal { + SwapOrder[] memory orders = new SwapOrder[](1); + orders[0] = order; + return _placeOrders(self,orders,OcoMode.NO_OCO); + } + + function _placeOrders(OrdersInfo storage self, SwapOrder[] memory orders, OcoMode ocoMode) internal { + require(orders.length < type(uint8).max); + uint64 startIndex = uint64(self.orders.length); + require(startIndex < type(uint64).max); + uint64 ocoGroup; + if( ocoMode == OcoMode.NO_OCO ) + ocoGroup = NO_OCO; + else if ( ocoMode == OcoMode.CANCEL_ON_PARTIAL_FILL || ocoMode == OcoMode.CANCEL_ON_COMPLETION ){ + ocoGroup = uint64(self.ocoGroups.length); + self.ocoGroups.push(OcoGroup(ocoMode, startIndex, uint8(orders.length))); + } + else + revert('OCOM'); + for( uint8 o = 0; o < orders.length; o++ ) { + SwapOrder memory order = orders[o]; + // we must explicitly copy into storage because Solidity doesn't implement copying the double-nested + // tranches constraints array :( + uint orderIndex = self.orders.length; + self.orders.push(); + SwapOrderStatus storage status = self.orders[orderIndex]; + status.order.amount = order.amount; + status.order.amountIsInput = order.amountIsInput; + status.order.tokenIn = order.tokenIn; + status.order.tokenOut = order.tokenOut; + status.order.fee = order.fee; + status.order.chainOrder = order.chainOrder; + status.order.outputDirectlyToOwner = order.outputDirectlyToOwner; + for( uint t=0; t block.timestamp) + return 'TE'; + time = tc.latest.mode == TimeMode.Timestamp ? tc.latest.time : status.start + tc.latest.time; + if (time < block.timestamp) + return 'TL'; + } + else if (constraint.mode == ConstraintMode.Limit) { + if( sqrtPriceX96 == 0 ) { + (sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0(); + } + PriceConstraint memory pc = abi.decode(constraint.constraint, (PriceConstraint)); + 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 ) + return 'L'; + } + else if (constraint.mode == ConstraintMode.Barrier) { + return 'NI'; + } + else if (constraint.mode == ConstraintMode.Trailing) { + return 'NI'; + } + else if (constraint.mode == ConstraintMode.Line) { + return 'NI'; + } + else + return 'NI'; + // unknown constraint + } + uint256 amount = status.order.amount * tranche.fraction / 10 ** 18; + uint256 remaining = status.order.amount - (status.order.amountIsInput ? status.filledIn : status.filledOut); + if (amount > remaining) + amount = remaining; + uint256 amountIn; + uint256 amountOut; + (error, amountIn, amountOut) = _do_execute_univ3(status.order, pool, amount, sqrtPriceLimitX96); + if( bytes(error).length == 0 ) { + status.filledIn += amountIn; + status.filledOut += amountOut; + emit DexorderSwapFilled(orderIndex, tranche_index, amountIn, amountOut); + _checkCompleted(self, orderIndex, status); + } + } + + + function _do_execute_univ3( SwapOrder storage order, address pool, uint256 amount, uint160 sqrtPriceLimitX96) private + returns (string memory error, uint256 amountIn, uint256 amountOut) + { + // todo refactor this signature to be more low-level, taking only the in/out amounts and limit prices. doesnt need self/status/index + if (sqrtPriceLimitX96 == 0) + // check pool inversion to see if the price should be high or low + sqrtPriceLimitX96 = order.tokenIn < order.tokenOut ? 0 : type(uint160).max; + // todo swap direct to owner + if (order.amountIsInput) { + amountIn = amount; + (error, amountOut) = UniswapSwapper.swapExactInput(UniswapSwapper.SwapParams( + pool, order.tokenIn, order.tokenOut, order.fee, amount, sqrtPriceLimitX96)); + } + else { + amountOut = amount; + (error, amountIn) = UniswapSwapper.swapExactOutput(UniswapSwapper.SwapParams( + pool, order.tokenIn, order.tokenOut, order.fee, amount, sqrtPriceLimitX96)); + } + } + + function _checkCompleted(OrdersInfo storage self, uint64 orderIndex, SwapOrderStatus storage status) internal { + uint256 remaining = status.order.amount - (status.order.amountIsInput ? status.filledIn : status.filledOut); + if( remaining == 0 ) { // todo dust leeway? + status.state = SwapOrderState.Filled; + emit DexorderCompleted(orderIndex); + if( status.ocoGroup != NO_OCO ) + _cancelOco(self, status.ocoGroup); + } + else if( status.ocoGroup != NO_OCO && self.ocoGroups[status.ocoGroup].mode == OcoMode.CANCEL_ON_PARTIAL_FILL ) + _cancelOco(self, status.ocoGroup); + } + + function _cancelOco(OrdersInfo storage self, uint64 ocoIndex) internal { + OcoGroup storage group = self.ocoGroups[ocoIndex]; + uint64 endIndex = group.startIndex + group.num; + for( uint64 i=group.startIndex; i= start); - require(spec.numTranches >= 1); - Status memory status = Status(OrderStatus.ACTIVE, start, 0, 0, 0, 0); - require(timedOrderStatuses.length < type(uint64).max); - index = uint64(timedOrderStatuses.length); - timedOrderStatuses.push(status); - uint32 trancheInterval = (spec.deadline - uint32(block.timestamp)) / spec.numTranches; - spec.leeway = spec.leeway > 0 ? spec.leeway : trancheInterval / 10; - if (spec.leeway < 60) // todo configure per chain? - spec.leeway = 60; - timedOrderSpecs.push(spec); - emit TimedOrderCreated(address(this), index, spec); - } - - - function cancelTimedOrder(uint64 index) public onlyOwner { - require(index < timedOrderStatuses.length); - Status storage s = timedOrderStatuses[index]; - if (s.status == OrderStatus.ACTIVE) - s.status = OrderStatus.CANCELED; - } - - - function triggerTimedOrder(uint64 index) public returns (bool changed) { - return _triggerTimedOrder(index); - } - - - function triggerTimedOrders(uint64[] calldata indexes) public returns (bool[] memory changed) { - changed = new bool[](indexes.length); - for (uint256 i = 0; i < indexes.length; i++) { - changed[i] = _triggerTimedOrder(indexes[i]); - } - } - - struct _TriggerTimedOrderVars { - uint32 interval; - uint32 triggerTime; - address pool; - uint160 sqrtPriceX96; - uint160 limit; - uint256 amountIn; - uint256 amountOut; - string error; - } - - function _triggerTimedOrder(uint64 index) internal returns (bool changed) { - if (!(index < timedOrderStatuses.length)) // ensure valid order index - return false; - Status storage s = timedOrderStatuses[index]; - if (!(s.status == OrderStatus.ACTIVE)) // ensure order is active - return false; - Spec storage c = timedOrderSpecs[index]; - _TriggerTimedOrderVars memory v; - // compute trigger times. try to find a tranche which starts before this block but hasnt expired yet - v.interval = (c.deadline - s.start) / c.numTranches; - v.triggerTime = s.start + s.tranche * v.interval; - while (s.tranche < c.numTranches) { - if (v.triggerTime > block.timestamp) - return false; // not time yet to trigger - if (block.timestamp <= v.triggerTime + c.leeway) - break; // triggerTime <= block.timestamp <= triggerTime + intervalLeeway - // we have not yet found a tranche which hasn't expired - s.tranche++; - v.triggerTime += v.interval; - } - if (_checkCompleted(index, s, c.numTranches)) - return true; - // we have found a valid tranche - // check prices - v.pool = Constants.uniswapV3Factory.getPool(c.tokenIn, c.tokenOut, c.fee); - (v.sqrtPriceX96, , , , , ,) = IUniswapV3Pool(v.pool).slot0(); - require(v.sqrtPriceX96 >= c.minSqrtPriceX96); - require(v.sqrtPriceX96 <= c.maxSqrtPriceX96); - // todo swap - v.limit = c.tokenIn < c.tokenOut ? c.minSqrtPriceX96 : c.maxSqrtPriceX96; - if (c.amountIsInput) { - v.amountIn = c.amount; - (v.error, v.amountOut) = UniswapSwapper.swapExactInput(UniswapSwapper.SwapParams( - v.pool, c.tokenIn, c.tokenOut, c.fee, c.amount, v.limit)); - if(!_checkSwapError(index, v.error)) - return false; - } - else { - v.amountOut = c.amount; - (v.error, v.amountIn) = UniswapSwapper.swapExactOutput(UniswapSwapper.SwapParams( - v.pool, c.tokenIn, c.tokenOut, c.fee, c.amount, v.limit)); - if(!_checkSwapError(index, v.error)) - return false; - } - - s.filledIn += v.amountIn; - s.filledOut += v.amountOut; - s.tranchesExecuted++; - s.tranche++; - emit TimedOrderFilled(address(this), index, v.amountIn, v.amountOut); - _checkCompleted(index, s, c.numTranches); - return true; - } - - - function _checkCompleted(uint64 index, Status storage s, uint8 numTranches) internal returns (bool completed) { - if (s.tranche >= numTranches) { - // last tranche has finished - s.status = s.tranchesExecuted == numTranches ? OrderStatus.FILLED : OrderStatus.EXPIRED; - emit TimedOrderCompleted(address(this), index); - return true; - } - return false; - } - - function _checkSwapError( uint64 index, string memory status ) internal returns (bool ok) { - if( bytes(status).length == 0 ) - return true; - emit TimedOrderError(address(this), index, status); - return false; - } - -} diff --git a/src/UniswapSwapper.sol b/src/UniswapSwapper.sol index f51047f..37acf3a 100644 --- a/src/UniswapSwapper.sol +++ b/src/UniswapSwapper.sol @@ -3,7 +3,7 @@ pragma solidity =0.7.6; pragma abicoder v2; import "./Constants.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; library UniswapSwapper { diff --git a/src/Vault.sol b/src/Vault.sol index 5ac7759..734100c 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -5,17 +5,20 @@ pragma abicoder v2; import "./Constants.sol"; import "./interface/IVaultDeployer.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "./OrderLib.sol"; contract Vault { + using OrderLib for OrderLib.OrdersInfo; + uint8 public immutable version; address public immutable owner; + OrderLib.OrdersInfo public orderList; constructor() { - (address owner_) = IVaultDeployer(msg.sender).parameters(); + owner = IVaultDeployer(msg.sender).parameters(); version = Constants.VERSION; - owner = owner_; } event DexorderReceived(address, uint256); @@ -28,12 +31,11 @@ contract Vault { _withdrawNative(msg.sender, amount); } - function withdraw(address payable recipient, uint256 amount) public { + function withdrawTo(address payable recipient, uint256 amount) public { _withdrawNative(recipient, amount); } - function _withdrawNative(address payable reipient, uint256 amount) internal { - require(msg.sender == owner); + function _withdrawNative(address payable reipient, uint256 amount) internal onlyOwner { reipient.transfer(amount); } @@ -41,13 +43,32 @@ contract Vault { _withdraw(token, msg.sender, amount); } - function withdraw(IERC20 token, address recipient, uint256 amount) public { + function withdrawTo(IERC20 token, address recipient, uint256 amount) public { _withdraw(token, recipient, amount); } - function _withdraw(IERC20 token, address recipient, uint256 amount) internal { - require(msg.sender == owner); + function _withdraw(IERC20 token, address recipient, uint256 amount) internal onlyOwner { token.transfer(recipient, amount); } + + function placeOrder(OrderLib.SwapOrder memory order) public onlyOwner { + orderList._placeOrder(order); + } + + function placeOrders(OrderLib.SwapOrder[] memory orders, OrderLib.OcoMode ocoMode) public onlyOwner { + orderList._placeOrders(orders, ocoMode); + } + + function execute(uint64 orderIndex, uint8 tranche_index, OrderLib.PriceProof memory proof) public + returns (string memory error) + { + return orderList.execute(orderIndex, tranche_index, proof); + } + + modifier onlyOwner() { + require(msg.sender == owner); + _; + } + } diff --git a/src/VaultDeployer.sol b/src/VaultDeployer.sol index 39d05ba..856c832 100644 --- a/src/VaultDeployer.sol +++ b/src/VaultDeployer.sol @@ -14,7 +14,7 @@ contract VaultDeployer { Parameters public parameters; - function deployVault(address owner) public returns (address vault) { + function deployVault(address owner) public returns (address payable vault) { parameters = Parameters(owner); vault = address(new Vault{salt: keccak256(abi.encode(owner))}()); delete parameters;