timed order
This commit is contained in:
5
docs/design.md
Normal file
5
docs/design.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# General Design
|
||||
|
||||
Creating a separate contract for each user address allows users to deposit coins into their "account" using standard ERC20 sends without extra approvals. Withdrawals require a contract call, but again no approval step. Furthermore, this clarifies the no-custody nature of the setup, since DexOrder never has any claim to ownership of the user's contract. Of course this costs extra gas up front to create the contract for the user, but on L2's it should be minimal. What about ETH? Hmmm... The alternative is to have a single contract which keeps an accounting of everyone's everything. Deposits would require approvals and a contract call. Using separate vaults will be an easier, more secure experience for frequent traders who are more likely to be our users rather than casual, occasional traders.
|
||||
|
||||
|
||||
1
lib/@openzeppelin
Symbolic link
1
lib/@openzeppelin
Symbolic link
@@ -0,0 +1 @@
|
||||
openzeppelin-contracts
|
||||
1
lib/@uniswap/v3-core
Symbolic link
1
lib/@uniswap/v3-core
Symbolic link
@@ -0,0 +1 @@
|
||||
../v3-core/contracts/
|
||||
1
lib/@uniswap/v3-periphery
Symbolic link
1
lib/@uniswap/v3-periphery
Symbolic link
@@ -0,0 +1 @@
|
||||
../v3-periphery/contracts/
|
||||
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);
|
||||
}
|
||||
134
test/MockEnv.sol
Normal file
134
test/MockEnv.sol
Normal file
@@ -0,0 +1,134 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity =0.7.6;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "forge-std/console2.sol";
|
||||
import "../src/MockERC20.sol";
|
||||
import "../src/Util.sol";
|
||||
import "../src/Constants.sol";
|
||||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import "@uniswap/v3-core/contracts/libraries/TickMath.sol";
|
||||
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
|
||||
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
|
||||
import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol";
|
||||
|
||||
|
||||
contract MockEnv {
|
||||
|
||||
INonfungiblePositionManager public nfpm = Constants.uniswapV3NonfungiblePositionManager;
|
||||
ISwapRouter public swapper = Constants.uniswapV3SwapRouter;
|
||||
IUniswapV3Pool public pool;
|
||||
uint24 public fee;
|
||||
MockERC20 public COIN;
|
||||
MockERC20 public USD;
|
||||
address public token0; // either WETH or USDC depending on the order in the pool
|
||||
address public token1;
|
||||
bool public inverted;
|
||||
|
||||
|
||||
function init() public {
|
||||
COIN = new MockERC20('Mock Coin', 'MOCK', 18);
|
||||
USD = new MockERC20('Universally Supported Dollars', 'USD', 6);
|
||||
fee = 500;
|
||||
inverted = address(COIN) > address(USD);
|
||||
token0 = inverted ? address(USD) : address(COIN);
|
||||
token1 = inverted ? address(COIN) : address(USD);
|
||||
uint160 initialPrice = 1 * 2**96;
|
||||
// if this reverts here make sure Anvil is started and you are running forge with --rpc-url
|
||||
pool = IUniswapV3Pool(nfpm.createAndInitializePoolIfNecessary(token0, token1, fee, initialPrice));
|
||||
int24 ts = pool.tickSpacing();
|
||||
(, int24 lower, , , , ,) = pool.slot0();
|
||||
int24 upper = lower;
|
||||
for (int8 i = 0; i < 10; i++) {
|
||||
lower -= ts;
|
||||
upper += ts;
|
||||
stake(1 * 10 ** COIN.decimals(), 1000 * 10 ** USD.decimals(), lower, upper);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function stake(uint128 liquidity_, int24 lower, int24 upper) public
|
||||
returns (
|
||||
uint256 tokenId,
|
||||
uint128 liquidity,
|
||||
uint256 amount0,
|
||||
uint256 amount1
|
||||
)
|
||||
{
|
||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(lower);
|
||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(upper);
|
||||
(uint160 sqrtPriceX96, , , , , ,) = pool.slot0();
|
||||
(amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, liquidity_);
|
||||
return _stake(amount0, amount1, lower, upper);
|
||||
}
|
||||
|
||||
function stake(uint256 coinAmount, uint256 usdAmount, int24 lower, int24 upper) public
|
||||
returns (
|
||||
uint256 tokenId,
|
||||
uint128 liquidity,
|
||||
uint256 amount0,
|
||||
uint256 amount1
|
||||
)
|
||||
{
|
||||
return _stake(coinAmount, usdAmount, lower, upper);
|
||||
}
|
||||
|
||||
function _stake(uint256 coinAmount, uint256 usdAmount, int24 lower, int24 upper) private
|
||||
returns (
|
||||
uint256 tokenId,
|
||||
uint128 liquidity,
|
||||
uint256 amount0,
|
||||
uint256 amount1
|
||||
)
|
||||
{
|
||||
COIN.mint(address(this), coinAmount);
|
||||
COIN.approve(address(nfpm), coinAmount);
|
||||
USD.mint(address(this), usdAmount);
|
||||
USD.approve(address(nfpm), usdAmount);
|
||||
// struct MintParams {
|
||||
// address token0;
|
||||
// address token1;
|
||||
// uint24 fee;
|
||||
// int24 tickLower;
|
||||
// int24 tickUpper;
|
||||
// uint256 amount0Desired;
|
||||
// uint256 amount1Desired;
|
||||
// uint256 amount0Min;
|
||||
// uint256 amount1Min;
|
||||
// address recipient;
|
||||
// uint256 deadline;
|
||||
// }
|
||||
int24 ts = pool.tickSpacing();
|
||||
lower = Util.roundTick(lower, ts);
|
||||
upper = Util.roundTick(upper, ts);
|
||||
(uint256 a0, uint256 a1) = inverted ? (usdAmount, coinAmount) : (coinAmount, usdAmount);
|
||||
INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams(
|
||||
token0, token1, fee, lower, upper, a0, a1, 0, 0, msg.sender, block.timestamp
|
||||
);
|
||||
return nfpm.mint(params);
|
||||
}
|
||||
|
||||
|
||||
function swap(MockERC20 inToken, MockERC20 outToken, uint256 amountIn) public returns (uint256 amountOut) {
|
||||
uint160 limit = address(inToken) == pool.token0() ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1;
|
||||
return swap(inToken, outToken, amountIn, limit);
|
||||
}
|
||||
|
||||
function swap(MockERC20 inToken, MockERC20 outToken, uint256 amountIn, uint160 sqrtPriceLimitX96) public returns (uint256 amountOut) {
|
||||
inToken.approve(address(swapper), amountIn);
|
||||
// struct ExactInputSingleParams {
|
||||
// address tokenIn;
|
||||
// address tokenOut;
|
||||
// uint24 fee;
|
||||
// address recipient;
|
||||
// uint256 deadline;
|
||||
// uint256 amountIn;
|
||||
// uint256 amountOutMinimum;
|
||||
// uint160 sqrtPriceLimitX96;
|
||||
// }
|
||||
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams(
|
||||
address(inToken), address(outToken), fee, msg.sender, block.timestamp, amountIn, 0, sqrtPriceLimitX96
|
||||
);
|
||||
return swapper.exactInputSingle(params);
|
||||
}
|
||||
}
|
||||
@@ -4,137 +4,17 @@ pragma abicoder v2;
|
||||
|
||||
import "forge-std/console2.sol";
|
||||
import "forge-std/Test.sol";
|
||||
import "../src/MockERC20.sol";
|
||||
import "../src/Util.sol";
|
||||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import "@uniswap/v3-core/contracts/libraries/TickMath.sol";
|
||||
import "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
|
||||
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
|
||||
import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol";
|
||||
import "./MockEnv.sol";
|
||||
|
||||
|
||||
contract TestSinglePool is Test {
|
||||
|
||||
INonfungiblePositionManager public nfpm = INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
|
||||
ISwapRouter public swapper = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
|
||||
IUniswapV3Pool public pool;
|
||||
uint24 public fee;
|
||||
MockERC20 public COIN;
|
||||
MockERC20 public USD;
|
||||
address public token0; // either WETH or USDC depending on the order in the pool
|
||||
address public token1;
|
||||
bool public inverted;
|
||||
|
||||
contract TestSinglePool is MockEnv, Test {
|
||||
function setUp() public {
|
||||
COIN = new MockERC20('Mock Coin', 'MOCK', 18);
|
||||
USD = new MockERC20('Universally Supported Dollars', 'USD', 6);
|
||||
fee = 500;
|
||||
inverted = address(COIN) > address(USD);
|
||||
token0 = inverted ? address(USD) : address(COIN);
|
||||
token1 = inverted ? address(COIN) : address(USD);
|
||||
uint160 initialPrice = 1 * 2**96;
|
||||
// if this reverts here make sure Anvil is started and you are running forge with --rpc-url
|
||||
pool = IUniswapV3Pool(nfpm.createAndInitializePoolIfNecessary(token0, token1, fee, initialPrice));
|
||||
int24 ts = pool.tickSpacing();
|
||||
(, int24 lower, , , , ,) = pool.slot0();
|
||||
int24 upper = lower;
|
||||
for (int8 i = 0; i < 10; i++) {
|
||||
lower -= ts;
|
||||
upper += ts;
|
||||
stake(1 * 10 ** COIN.decimals(), 1000 * 10 ** USD.decimals(), lower, upper);
|
||||
}
|
||||
init();
|
||||
}
|
||||
|
||||
|
||||
function stake(uint128 liquidity_, int24 lower, int24 upper) public
|
||||
returns (
|
||||
uint256 tokenId,
|
||||
uint128 liquidity,
|
||||
uint256 amount0,
|
||||
uint256 amount1
|
||||
)
|
||||
{
|
||||
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(lower);
|
||||
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(upper);
|
||||
(uint160 sqrtPriceX96, , , , , ,) = pool.slot0();
|
||||
(amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, liquidity_);
|
||||
return _stake(amount0, amount1, lower, upper);
|
||||
}
|
||||
|
||||
function testSwap() public {
|
||||
function testSwap() public {
|
||||
COIN.mint(address(this), 1 * 10**18);
|
||||
uint256 usd = swap(COIN, USD, 1 * 10**18);
|
||||
console2.log(usd);
|
||||
}
|
||||
|
||||
function stake(uint256 coinAmount, uint256 usdAmount, int24 lower, int24 upper) public
|
||||
returns (
|
||||
uint256 tokenId,
|
||||
uint128 liquidity,
|
||||
uint256 amount0,
|
||||
uint256 amount1
|
||||
)
|
||||
{
|
||||
return _stake(coinAmount, usdAmount, lower, upper);
|
||||
}
|
||||
|
||||
function _stake(uint256 coinAmount, uint256 usdAmount, int24 lower, int24 upper) private
|
||||
returns (
|
||||
uint256 tokenId,
|
||||
uint128 liquidity,
|
||||
uint256 amount0,
|
||||
uint256 amount1
|
||||
)
|
||||
{
|
||||
COIN.mint(address(this), coinAmount);
|
||||
COIN.approve(address(nfpm), coinAmount);
|
||||
USD.mint(address(this), usdAmount);
|
||||
USD.approve(address(nfpm), usdAmount);
|
||||
// struct MintParams {
|
||||
// address token0;
|
||||
// address token1;
|
||||
// uint24 fee;
|
||||
// int24 tickLower;
|
||||
// int24 tickUpper;
|
||||
// uint256 amount0Desired;
|
||||
// uint256 amount1Desired;
|
||||
// uint256 amount0Min;
|
||||
// uint256 amount1Min;
|
||||
// address recipient;
|
||||
// uint256 deadline;
|
||||
// }
|
||||
int24 ts = pool.tickSpacing();
|
||||
lower = Util.roundTick(lower, ts);
|
||||
upper = Util.roundTick(upper, ts);
|
||||
(uint256 a0, uint256 a1) = inverted ? (usdAmount, coinAmount) : (coinAmount, usdAmount);
|
||||
INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams(
|
||||
token0, token1, fee, lower, upper, a0, a1, 0, 0, msg.sender, block.timestamp
|
||||
);
|
||||
return nfpm.mint(params);
|
||||
}
|
||||
|
||||
|
||||
function swap(MockERC20 inToken, MockERC20 outToken, uint256 amountIn) public returns (uint256 amountOut) {
|
||||
uint160 limit = address(inToken) == pool.token0() ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1;
|
||||
return swap(inToken, outToken, amountIn, limit);
|
||||
}
|
||||
|
||||
function swap(MockERC20 inToken, MockERC20 outToken, uint256 amountIn, uint160 sqrtPriceLimitX96) public returns (uint256 amountOut) {
|
||||
inToken.approve(address(swapper), amountIn);
|
||||
// struct ExactInputSingleParams {
|
||||
// address tokenIn;
|
||||
// address tokenOut;
|
||||
// uint24 fee;
|
||||
// address recipient;
|
||||
// uint256 deadline;
|
||||
// uint256 amountIn;
|
||||
// uint256 amountOutMinimum;
|
||||
// uint160 sqrtPriceLimitX96;
|
||||
// }
|
||||
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams(
|
||||
address(inToken), address(outToken), fee, msg.sender, block.timestamp, amountIn, 0, sqrtPriceLimitX96
|
||||
);
|
||||
return swapper.exactInputSingle(params);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
test/TestVault.sol
Normal file
25
test/TestVault.sol
Normal file
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity =0.7.6;
|
||||
|
||||
import "forge-std/console2.sol";
|
||||
import "../src/Factory.sol";
|
||||
import "../src/VaultAddress.sol";
|
||||
pragma abicoder v2;
|
||||
|
||||
contract TestVault {
|
||||
|
||||
Factory public factory;
|
||||
Vault public vault;
|
||||
|
||||
function setUp() public {
|
||||
factory = new Factory();
|
||||
vault = Vault(factory.deployVault(address(this)));
|
||||
}
|
||||
|
||||
function testDeterministicAddress() public {
|
||||
console2.log(address(vault));
|
||||
address d = VaultAddress.computeAddress(address(factory), address(this));
|
||||
console2.log(d);
|
||||
assert(address(vault) == d);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user