Files
contract/src/TimedOrder.sol
2023-08-22 11:18:05 -04:00

172 lines
6.4 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.7.6;
pragma abicoder v2;
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./OrderStatus.sol";
import "./UniswapSwapper.sol";
contract TimedOrder is Ownable {
struct Spec {
address tokenIn;
address tokenOut;
uint24 fee;
uint32 deadline; // uint32 is big enough to hold dates through the year 2105
uint32 leeway; // if a tranche is not traded within this number of seconds of its scheduled time, it is skipped. If 0, then a reasonable value is generated.
uint160 minSqrtPriceX96; // must be in terms of token1/token0 regardless of which token is in/out
uint160 maxSqrtPriceX96;
uint8 numTranches;
uint256 amount; // amount PER TRANCHE
bool amountIsInput;
}
struct Status {
// includes Spec but has additional status fields
OrderStatus status;
uint32 start;
uint8 tranche;
uint8 tranchesExecuted; // may be less than tranche if a tranche was skipped
uint256 filledIn;
uint256 filledOut;
}
event TimedOrderCreated (address owner, uint64 index, Spec spec);
event TimedOrderFilled (address owner, uint64 index, uint256 amountIn, uint256 amountOut);
event TimedOrderCompleted (address owner, uint64 index);
event TimedOrderError (address owner, uint64 index, string reason);
Spec[] public timedOrderSpecs;
Status[] public timedOrderStatuses;
function timedOrder(Spec memory spec) public onlyOwner returns (uint64 index) {
uint32 start = uint32(block.timestamp);
require(spec.deadline >= 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;
}
}