timed order

This commit is contained in:
Tim Olson
2023-08-22 11:18:05 -04:00
parent 007135d232
commit 75150949dd
16 changed files with 568 additions and 124 deletions

22
src/Constants.sol Normal file
View File

@@ -0,0 +1,22 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.7.6;
pragma abicoder v2;
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
library Constants {
uint8 internal constant VERSION = 0;
IUniswapV3Factory internal constant uniswapV3Factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
INonfungiblePositionManager internal constant uniswapV3NonfungiblePositionManager
= INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
ISwapRouter internal constant uniswapV3SwapRouter
= ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
// Swap statuses
string internal constant SWAP_OK = ''; // fastest comparison
// other errors may be passed through from Uniswap
}

11
src/Factory.sol Normal file
View File

@@ -0,0 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
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 {
}

11
src/OrderStatus.sol Normal file
View File

@@ -0,0 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.7.6;
pragma abicoder v2;
enum OrderStatus {
ACTIVE, // the only status while the order is still executing
CANCELED, // canceled by the owner
FILLED, // full trade amount was filled
EXPIRED // trade ended without completing its fills
}

171
src/TimedOrder.sol Normal file
View File

@@ -0,0 +1,171 @@
// 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;
}
}

79
src/UniswapSwapper.sol Normal file
View File

@@ -0,0 +1,79 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.7.6;
pragma abicoder v2;
import "./Constants.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
library UniswapSwapper {
struct SwapParams {
address pool;
address tokenIn;
address tokenOut;
uint24 fee;
uint256 amount;
uint160 sqrtPriceLimitX96;
}
function swapExactInput(SwapParams memory params) internal returns (string memory error, uint256 amountOut)
{
// struct ExactInputSingleParams {
// address tokenIn;
// address tokenOut;
// uint24 fee;
// address recipient;
// uint256 deadline;
// uint256 amountIn;
// uint256 amountOutMinimum;
// uint160 sqrtPriceLimitX96;
// }
try Constants.uniswapV3SwapRouter.exactInputSingle(ISwapRouter.ExactInputSingleParams({
tokenIn: params.tokenIn, tokenOut: params.tokenOut, fee: params.fee, recipient: address(this), // todo return directly to wallet?
deadline: block.timestamp, amountIn: params.amount, amountOutMinimum: 0, sqrtPriceLimitX96: params.sqrtPriceLimitX96
})) returns (uint256 filledOut) {
amountOut = filledOut;
error = Constants.SWAP_OK;
}
catch Error(string memory reason) {
amountOut = 0;
error = reason;
}
}
function swapExactOutput(SwapParams memory params) internal returns (string memory error, uint256 amountIn)
{
// struct ExactOutputSingleParams {
// address tokenIn;
// address tokenOut;
// uint24 fee;
// address recipient;
// uint256 deadline;
// uint256 amountOut;
// uint256 amountInMaximum;
// uint160 sqrtPriceLimitX96;
// }
address t = address(this);
uint256 balance = IERC20(params.tokenIn).balanceOf(t);
if( balance == 0 ) {
// todo dust?
return ('IIA', 0);
}
try Constants.uniswapV3SwapRouter.exactOutputSingle(ISwapRouter.ExactOutputSingleParams({
tokenIn: params.tokenIn, tokenOut: params.tokenOut, fee: params.fee, recipient: t, // todo return directly to wallet?
deadline: block.timestamp, amountOut: params.amount, amountInMaximum: balance, // todo use only the committed allocation?
sqrtPriceLimitX96: params.sqrtPriceLimitX96
})) returns (uint256 filledIn) {
amountIn = filledIn;
error = Constants.SWAP_OK;
}
catch Error(string memory reason) {
amountIn = 0;
error = reason;
}
}
}

44
src/Vault.sol Normal file
View File

@@ -0,0 +1,44 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.7.6;
pragma abicoder v2;
import "./Constants.sol";
import "./interface/IVaultDeployer.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Vault {
uint8 public immutable version;
address public immutable owner;
constructor()
{
(address owner_) = IVaultDeployer(msg.sender).parameters();
version = Constants.VERSION;
owner = owner_;
}
function transfer(address payable recipient, uint256 amount) public {
require(msg.sender == owner);
recipient.transfer(amount);
}
function transfer(uint256 amount) public {
require(msg.sender == owner);
msg.sender.transfer(amount);
}
function withdraw(IERC20 token, uint256 amount) public {
_withdraw(token, msg.sender, amount);
}
function withdraw(IERC20 token, address recipient, uint256 amount) public {
_withdraw(token, recipient, amount);
}
function _withdraw(IERC20 token, address recipient, uint256 amount) internal {
require(msg.sender == owner);
token.transfer(recipient, amount);
}
}

32
src/VaultAddress.sol Normal file
View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.7.6;
pragma abicoder v2;
import "./Constants.sol";
library VaultAddress {
// keccak-256 hash of the Vault's bytecode (not the deployed bytecode but the initialization bytecode)
// can paste into:
// https://emn178.github.io/online-tools/keccak_256.html
bytes32 internal constant VAULT_INIT_CODE_HASH = 0xbf043f7035d5aa3be2b3c94df5b256fbe24675689327af4ab71c48194c463031;
// the contract being constructed must not have any constructor arguments or the determinism will be broken. instead, use a callback to
// get construction arguments
// Uniswap example
// https://github.com/Uniswap/v3-periphery/blob/6cce88e63e176af1ddb6cc56e029110289622317/contracts/libraries/PoolAddress.sol#L33C5-L47C6
function computeAddress(address factory, address owner) internal pure returns (address vault) {
vault = address(
uint256(
keccak256(
abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encode(owner)),
VAULT_INIT_CODE_HASH
)
)
)
);
}
}

20
src/VaultDeployer.sol Normal file
View File

@@ -0,0 +1,20 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.7.6;
import "./Vault.sol";
pragma abicoder v2;
contract VaultDeployer {
struct Parameters {
address owner;
}
Parameters public parameters;
function deployVault(address owner) public returns (address vault) {
parameters = Parameters(owner);
vault = address(new Vault{salt: keccak256(abi.encode(owner))}());
delete parameters;
}
}

View File

@@ -0,0 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity =0.7.6;
pragma abicoder v2;
interface IVaultDeployer {
function parameters() external view returns (address owner);
}