timed order
This commit is contained in:
22
src/Constants.sol
Normal file
22
src/Constants.sol
Normal 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
11
src/Factory.sol
Normal 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
11
src/OrderStatus.sol
Normal 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
171
src/TimedOrder.sol
Normal 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
79
src/UniswapSwapper.sol
Normal 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
44
src/Vault.sol
Normal 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
32
src/VaultAddress.sol
Normal 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
20
src/VaultDeployer.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
7
src/interface/IVaultDeployer.sol
Normal file
7
src/interface/IVaultDeployer.sol
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user