172 lines
6.4 KiB
Solidity
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;
|
|
}
|
|
|
|
}
|