dexorder
This commit is contained in:
200
src/core/FeeManager.sol
Normal file
200
src/core/FeeManager.sol
Normal file
@@ -0,0 +1,200 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import {IFeeManager} from "../interface/IFeeManager.sol";
|
||||
|
||||
|
||||
//
|
||||
// The FeeManager contract is the authoritative source and mediator for what fees Dexorder charges for orders.
|
||||
//
|
||||
// It implements three fees:
|
||||
// * a per-order fee, payable upon order placement in native token
|
||||
// * a per-execution fee, meant to cover future gas costs, payable upon order placement in native token
|
||||
// * a fill fee which is a fraction of the amount received from a swap
|
||||
//
|
||||
// FeeManager enforces maximum limits on these fees as well as a delay before any new fees take effect. These delays
|
||||
// are called notice periods and they afford clients of Dexorder time to review new fee schedules before they take
|
||||
// effect. The fee schedule for any given order is locked-in at order creation time, and all future fills for that
|
||||
// order will be charged the fill fee that was in effect when the order was created.
|
||||
//
|
||||
// There are two notice periods: a short notice period of 1 hour for changing the fee schedule within set limits, plus
|
||||
// a longer 30-day notice period for changing the fee limits themselves. This allows fees to be adjusted quickly as
|
||||
// the market price of native token changes, while preventing a malicious fee manager to suddenly charge exhorbitant
|
||||
// fees without customers noticing it. The up-front fees in native coin must be sent along with the order placement
|
||||
// transaction, which means any wallet user will clearly see the fee amounts in their wallet software. The fill fee
|
||||
// has less up-front transparency, but it is also hard-limited to never be more than 1.27%, by virtue of being
|
||||
// represented as a uint8 value divided by 200.
|
||||
//
|
||||
// The fee administrator at Dexorder may propose changes to the fees at any time, but the proposed fees do not take
|
||||
// effect until a sufficient "notice period" has elapsed. There are two notice periods: a short notice period of
|
||||
// 1 hour to make changes to the fee schedule itself, but a longer 30-day notice period to change the maximum fee
|
||||
// limits allowed.
|
||||
|
||||
// Any orders which were created with a promised fill fee will remember that fee and apply it to all fills
|
||||
// for that order, even if Dexorder changes the fee schedule while the order is open and not yet complete.
|
||||
|
||||
|
||||
contract FeeManager is IFeeManager {
|
||||
|
||||
//
|
||||
// FEE CHANGE LIMITS
|
||||
//
|
||||
|
||||
// This many seconds must elapse before any change to the limits on fees takes effect.
|
||||
// uint32 constant public LIMIT_CHANGE_NOTICE_DURATION = 30 * 24 * 60 * 60; // 30 days
|
||||
uint32 immutable public LIMIT_CHANGE_NOTICE_DURATION; // todo remove debug timing of 5 minutes
|
||||
|
||||
// This many seconds must elapse before new fees (within limits) take effect.
|
||||
// uint32 constant public FEE_CHANGE_NOTICE_DURATION = 1 * 60 * 60; // 1 hour
|
||||
uint32 immutable public FEE_CHANGE_NOTICE_DURATION;
|
||||
|
||||
// The per-order fee should not need to change too dramatically.
|
||||
uint8 immutable public MAX_INCREASE_ORDER_FEE_PCT;
|
||||
|
||||
// tranche fees cover gas costs, which can spike dramatically, so we allow up to a doubling each day
|
||||
uint8 immutable public MAX_INCREASE_TRANCHE_FEE_PCT; // 100%
|
||||
|
||||
|
||||
FeeSchedule private _fees; // use fees()
|
||||
FeeSchedule private _limits; // use limits()
|
||||
|
||||
FeeSchedule private _proposedFees; // proposed change to the fee schedule
|
||||
function proposedFees() external view returns (FeeSchedule memory) { return _proposedFees; }
|
||||
uint32 public override proposedFeeActivationTime; // time at which the proposed fees will become active
|
||||
|
||||
FeeSchedule private _proposedLimits;
|
||||
function proposedLimits() external view returns (FeeSchedule memory) { return _proposedLimits; }
|
||||
uint32 public override proposedLimitActivationTime;
|
||||
|
||||
|
||||
address public immutable override admin;
|
||||
|
||||
address payable public override orderFeeAccount;
|
||||
address payable public override gasFeeAccount;
|
||||
address payable public override fillFeeAccount;
|
||||
|
||||
struct ConstructorArgs {
|
||||
uint32 LIMIT_CHANGE_NOTICE_DURATION;
|
||||
uint32 FEE_CHANGE_NOTICE_DURATION;
|
||||
uint8 MAX_INCREASE_ORDER_FEE_PCT;
|
||||
uint8 MAX_INCREASE_TRANCHE_FEE_PCT;
|
||||
|
||||
FeeSchedule fees;
|
||||
FeeSchedule limits;
|
||||
|
||||
address admin;
|
||||
address payable orderFeeAccount;
|
||||
address payable gasFeeAccount;
|
||||
address payable fillFeeAccount;
|
||||
}
|
||||
|
||||
constructor (ConstructorArgs memory args) {
|
||||
LIMIT_CHANGE_NOTICE_DURATION = args.LIMIT_CHANGE_NOTICE_DURATION;
|
||||
FEE_CHANGE_NOTICE_DURATION = args.FEE_CHANGE_NOTICE_DURATION;
|
||||
MAX_INCREASE_ORDER_FEE_PCT = args.MAX_INCREASE_ORDER_FEE_PCT;
|
||||
MAX_INCREASE_TRANCHE_FEE_PCT = args.MAX_INCREASE_TRANCHE_FEE_PCT;
|
||||
|
||||
_fees = args.fees;
|
||||
_limits = args.limits;
|
||||
|
||||
admin = args.admin;
|
||||
orderFeeAccount = args.orderFeeAccount;
|
||||
gasFeeAccount = args.gasFeeAccount;
|
||||
fillFeeAccount = args.fillFeeAccount;
|
||||
|
||||
emit FeesChanged(args.fees);
|
||||
emit FeeLimitsChanged(args.limits);
|
||||
}
|
||||
|
||||
|
||||
function fees() public view override returns (FeeSchedule memory) {
|
||||
return proposedFeeActivationTime != 0 && proposedFeeActivationTime <= block.timestamp ? _proposedFees : _fees;
|
||||
}
|
||||
|
||||
|
||||
function limits() public view override returns (FeeSchedule memory) {
|
||||
return proposedLimitActivationTime != 0 && proposedLimitActivationTime <= block.timestamp ? _proposedLimits : _limits;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Admin
|
||||
//
|
||||
|
||||
modifier onlyAdmin() {
|
||||
require(msg.sender == admin, "not admin");
|
||||
_;
|
||||
}
|
||||
|
||||
|
||||
function _push() internal {
|
||||
// if existing proposals have become active, set them as the main schedules.
|
||||
if (proposedLimitActivationTime != 0 && proposedLimitActivationTime <= block.timestamp) {
|
||||
_limits = _proposedLimits;
|
||||
proposedLimitActivationTime = 0;
|
||||
}
|
||||
if (proposedFeeActivationTime != 0 && proposedFeeActivationTime <= block.timestamp) {
|
||||
FeeSchedule memory prop = _proposedFees;
|
||||
uint256 orderFee = uint256(prop.orderFee) << prop.orderExp;
|
||||
uint256 orderFeeLimit = uint256(_limits.orderFee) << _limits.orderExp;
|
||||
uint256 gasFee = uint256(prop.gasFee) << prop.gasExp;
|
||||
uint256 gasFeeLimit = uint256(_limits.gasFee) << _limits.gasExp;
|
||||
|
||||
if(orderFee>orderFeeLimit){
|
||||
prop.orderFee = _limits.orderFee;
|
||||
prop.orderExp = _limits.orderExp;
|
||||
}
|
||||
if(gasFee>gasFeeLimit){
|
||||
prop.gasFee = _limits.gasFee;
|
||||
prop.gasExp = _limits.gasExp;
|
||||
}
|
||||
|
||||
_fees = prop;
|
||||
proposedFeeActivationTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setFees(FeeSchedule calldata sched) public override onlyAdmin {
|
||||
_push();
|
||||
|
||||
// check limits
|
||||
FeeSchedule memory limit = limits(); //REV technically can use _limits here, since you do _push()
|
||||
|
||||
uint256 orderFee = uint256(sched.orderFee) << sched.orderExp;
|
||||
uint256 orderFeeLimit = uint256(limit.orderFee) << limit.orderExp;
|
||||
require( orderFee <= orderFeeLimit, 'FL' );
|
||||
|
||||
uint256 gasFee = uint256(sched.gasFee) << sched.gasExp;
|
||||
uint256 gasFeeLimit = uint256(limit.gasFee) << limit.gasExp;
|
||||
require( gasFee <= gasFeeLimit, 'FL' );
|
||||
|
||||
require( sched.fillFeeHalfBps <= limit.fillFeeHalfBps, 'FL' );
|
||||
|
||||
_proposedFees = sched;
|
||||
proposedFeeActivationTime = uint32(block.timestamp + FEE_CHANGE_NOTICE_DURATION);
|
||||
emit FeesProposed(sched, proposedFeeActivationTime);
|
||||
}
|
||||
|
||||
|
||||
function setLimits(FeeSchedule calldata sched) public override onlyAdmin {
|
||||
_push();
|
||||
// Fee Limits may be changed with a much longer notice period.
|
||||
_proposedLimits = sched;
|
||||
proposedLimitActivationTime = uint32(block.timestamp + LIMIT_CHANGE_NOTICE_DURATION);
|
||||
emit FeeLimitsProposed(sched, proposedLimitActivationTime);
|
||||
}
|
||||
|
||||
|
||||
function setFeeAccounts(
|
||||
address payable fillFeeAccount_,
|
||||
address payable orderFeeAccount_,
|
||||
address payable gasFeeAccount_
|
||||
) public override onlyAdmin {
|
||||
fillFeeAccount = fillFeeAccount_;
|
||||
orderFeeAccount = orderFeeAccount_;
|
||||
gasFeeAccount = gasFeeAccount_;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
77
src/core/IEEE754.sol
Normal file
77
src/core/IEEE754.sol
Normal file
@@ -0,0 +1,77 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
type float is uint32; // https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types
|
||||
|
||||
float constant FLOAT_0 = float.wrap(0);
|
||||
|
||||
library IEEE754 {
|
||||
|
||||
// reduce (signed)) int to p bits
|
||||
function bits2int (uint256 b, uint256 p) internal pure returns (int256 r) {
|
||||
require(p >= 1 && p <= 256, "p invalid");
|
||||
uint256 s = 256 - p;
|
||||
r = int256( b << s ) >> s;
|
||||
}
|
||||
|
||||
int32 internal constant EBIAS = 127;
|
||||
int32 internal constant ENAN = 0xff; // Also includes infinity
|
||||
int32 internal constant ESUBNORM = 0;
|
||||
|
||||
uint32 internal constant SIGN_MASK = 0x8000_0000;
|
||||
uint32 internal constant EXP_MASK = 0x7f80_0000;
|
||||
uint32 internal constant MANT_MASK = 0x007f_ffff;
|
||||
uint32 internal constant ZERO_MASK = EXP_MASK | MANT_MASK;
|
||||
int32 internal constant MSB = 0x0080_0000;
|
||||
|
||||
|
||||
function toFixed(float floatValue, uint8 fixedBits) internal pure returns (int256 fixedPoint) {unchecked{
|
||||
|
||||
uint32 floatingPoint = float.unwrap(floatValue); // compile-time typecast
|
||||
|
||||
// Zero case
|
||||
|
||||
if (floatingPoint & ZERO_MASK == 0) return 0;
|
||||
|
||||
// Extract exponent field
|
||||
|
||||
int32 exp = int32(floatingPoint & EXP_MASK) >> 23;
|
||||
require (exp != ENAN, "NaN");
|
||||
|
||||
// Extract mantissa
|
||||
|
||||
int256 mant = int32(floatingPoint & MANT_MASK);
|
||||
if (exp == ESUBNORM) mant <<= 1;
|
||||
else mant |= MSB; // Add implied MSB to non-subnormal
|
||||
if (floatingPoint & SIGN_MASK != 0) mant = -mant; // Negate if sign bit set
|
||||
|
||||
// Compute shift amount
|
||||
|
||||
exp = exp - EBIAS; // Remove exponent bias
|
||||
int256 rshft = 23 - int32(uint32(fixedBits)) - exp; // Zero exp and integer fixedPoint throws away all but MSB
|
||||
|
||||
// Shift to arrive at fixed point alignment
|
||||
|
||||
if (rshft < 0) // Most likely case?
|
||||
fixedPoint = mant << uint256(-rshft);
|
||||
else if (rshft > 0)
|
||||
fixedPoint = mant >> uint256(rshft);
|
||||
else
|
||||
fixedPoint = mant;
|
||||
|
||||
return fixedPoint;
|
||||
}}
|
||||
|
||||
function isPositive(float f) internal pure returns (bool) {
|
||||
return float.unwrap(f) & SIGN_MASK == 0;
|
||||
}
|
||||
|
||||
function isNegative(float f) internal pure returns (bool) {
|
||||
return float.unwrap(f) & SIGN_MASK != 0;
|
||||
}
|
||||
|
||||
function isZero(float f) internal pure returns (bool) {
|
||||
return float.unwrap(f) == 0;
|
||||
}
|
||||
|
||||
}
|
||||
61
src/core/LineLib.sol
Normal file
61
src/core/LineLib.sol
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import {IEEE754, float} from "./IEEE754.sol";
|
||||
|
||||
|
||||
struct Line {
|
||||
float intercept; // if marketOrder==true, this is the (positive) max slippage amount
|
||||
float slope;
|
||||
}
|
||||
|
||||
|
||||
library LineLib {
|
||||
using IEEE754 for float;
|
||||
|
||||
function isEnabled(Line storage l) internal view returns (bool) {
|
||||
return !l.intercept.isZero() || !l.slope.isZero();
|
||||
}
|
||||
|
||||
// use this version for regular lines
|
||||
function priceNow(Line storage l) internal view
|
||||
returns (uint256 price) {
|
||||
int256 b = l.intercept.toFixed(96);
|
||||
if(l.slope.isZero())
|
||||
return uint256(b);
|
||||
int256 m = l.slope.toFixed(96);
|
||||
int256 x = int256(block.timestamp);
|
||||
price = computeLine(m,b,x);
|
||||
}
|
||||
|
||||
// use this version for "ratio" lines that are displaced relative to a start time and price
|
||||
function ratioPrice(Line storage l, uint32 startTime, uint256 startPrice) internal view
|
||||
returns (uint256 price) {
|
||||
int256 ratio = l.intercept.toFixed(96);
|
||||
// first we compute the natural intercept using the slope from the start time and price
|
||||
// y = mx + b
|
||||
// b = y - mx
|
||||
int256 y = int256(startPrice);
|
||||
int256 m = l.slope.toFixed(96);
|
||||
int256 x = int32(startTime);
|
||||
int256 mx = m * x; // x is not in X96 format so no denominator adjustment is necessary after m*x
|
||||
int256 sb = y - mx; // starting intercept
|
||||
// calculate the ratio
|
||||
int256 d = y * ratio / 2**96; // d is the intercept delta
|
||||
int256 b = sb + d; // apply the ratio delta to the starting intercept
|
||||
price = computeLine(m,b,x);
|
||||
}
|
||||
|
||||
function computeLine(int256 m, int256 b, int256 x) internal pure
|
||||
returns (uint256 y) {
|
||||
// steep lines may overflow any bitwidth quickly, but this would be merely a numerical error not a semantic one.
|
||||
// we handle overflows here explicitly, bounding the result to the range [0,MAXINT]
|
||||
unchecked {
|
||||
int256 z = m * x + b;
|
||||
if ((z - b) / m == x) // check the reverse calculation
|
||||
y = z <= 0 ? 0 : uint256(z); // no overflow, but bounded to zero. negative prices are not supported.
|
||||
else // overflow. bounded to either zero or maxval depending on the slope.
|
||||
y = m > 0 ? type(uint256).max : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
404
src/core/OrderLib.sol
Normal file
404
src/core/OrderLib.sol
Normal file
@@ -0,0 +1,404 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import "@forge-std/console2.sol";
|
||||
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import {Util} from "./Util.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {IEEE754, float} from "./IEEE754.sol";
|
||||
import {IFeeManager} from "../interface/IFeeManager.sol";
|
||||
import {IRouter} from "../interface/IRouter.sol";
|
||||
import "./OrderSpec.sol";
|
||||
import "./LineLib.sol";
|
||||
|
||||
|
||||
library OrderLib {
|
||||
using IEEE754 for float;
|
||||
using LineLib for Line;
|
||||
|
||||
function _placementFee(SwapOrder memory order, IFeeManager.FeeSchedule memory sched, SwapOrder memory conditionalOrder) internal pure
|
||||
returns (uint256 orderFee, uint256 executionFee) {
|
||||
// Conditional orders are charged for execution but not placement.
|
||||
if (order.amount==0)
|
||||
return (0,0);
|
||||
orderFee = _orderFee(sched);
|
||||
executionFee = _executionFee(order, sched, conditionalOrder); // special execution fee
|
||||
}
|
||||
|
||||
|
||||
function _placementFee(SwapOrder memory order, IFeeManager.FeeSchedule memory sched) internal pure
|
||||
returns (uint256 orderFee, uint256 executionFee) {
|
||||
// Place conditional orders using a zero amount to avoid placement fees on that conditional. Fees will be
|
||||
// charged instead to any order which references the conditional order.
|
||||
if (order.amount==0)
|
||||
return (0,0);
|
||||
// console2.log('computing fee');
|
||||
// console2.log(sched.orderFee);
|
||||
// console2.log(sched.orderExp);
|
||||
// console2.log(sched.gasFee);
|
||||
// console2.log(sched.gasExp);
|
||||
// console2.log(sched.fillFeeHalfBps);
|
||||
orderFee = _orderFee(sched);
|
||||
// console2.log(orderFee);
|
||||
executionFee = _executionFee(order, sched);
|
||||
// console2.log('total fee');
|
||||
// console2.log(orderFee+executionFee);
|
||||
}
|
||||
|
||||
|
||||
function _orderFee(IFeeManager.FeeSchedule memory sched) internal pure
|
||||
returns (uint256 orderFee) {
|
||||
orderFee = uint256(sched.orderFee) << sched.orderExp;
|
||||
}
|
||||
|
||||
function _executionFee(SwapOrder memory order, IFeeManager.FeeSchedule memory sched) internal pure
|
||||
returns (uint256 executionFee) {
|
||||
executionFee = _numExecutions(order) * (uint256(sched.gasFee) << sched.gasExp);
|
||||
}
|
||||
|
||||
function _executionFee(SwapOrder memory order, IFeeManager.FeeSchedule memory sched, SwapOrder memory conditionalOrder) internal pure
|
||||
returns (uint256 executionFee) {
|
||||
(uint256 orderFee, uint256 gasFee) = _placementFee(conditionalOrder, sched);
|
||||
uint256 placementFee = orderFee + gasFee;
|
||||
executionFee = _numExecutions(order) * placementFee;
|
||||
}
|
||||
|
||||
function _numExecutions(SwapOrder memory order) internal pure
|
||||
returns (uint256 numExecutions) {
|
||||
numExecutions = 0;
|
||||
for( uint i=0; i<order.tranches.length; i++ ) {
|
||||
uint16 rate = order.tranches[i].rateLimitFraction;
|
||||
uint256 exes;
|
||||
if (rate == 0)
|
||||
exes = 1;
|
||||
else {
|
||||
exes = MAX_FRACTION / rate;
|
||||
// ceil
|
||||
if( exes * rate < MAX_FRACTION)
|
||||
exes += 1;
|
||||
}
|
||||
// console2.log(exes);
|
||||
numExecutions += exes;
|
||||
}
|
||||
}
|
||||
|
||||
function _placeOrder(OrdersInfo storage self, SwapOrder memory order, uint8 fillFeeHalfBps, IRouter router) internal {
|
||||
SwapOrder[] memory memOrders = new SwapOrder[](1);
|
||||
memOrders[0] = order;
|
||||
return _placeOrders(self,memOrders,fillFeeHalfBps,OcoMode.NO_OCO, router);
|
||||
}
|
||||
|
||||
function _placeOrders(OrdersInfo storage self, SwapOrder[] memory memOrders, uint8 fillFeeHalfBps, OcoMode ocoMode, IRouter router) internal {
|
||||
// console2.log('_placeOrders');
|
||||
require(memOrders.length < type(uint8).max, 'TMO'); // TMO = Too Many Orders
|
||||
require(self.orders.length < type(uint64).max - memOrders.length, 'TMO');
|
||||
uint64 startIndex = uint64(self.orders.length);
|
||||
uint64 ocoGroup;
|
||||
if( ocoMode == OcoMode.NO_OCO )
|
||||
ocoGroup = NO_OCO_INDEX;
|
||||
else if ( ocoMode == OcoMode.CANCEL_ON_PARTIAL_FILL || ocoMode == OcoMode.CANCEL_ON_COMPLETION ){
|
||||
ocoGroup = uint64(self.ocoGroups.length);
|
||||
self.ocoGroups.push(OcoGroup(ocoMode, startIndex, uint8(memOrders.length)));
|
||||
}
|
||||
else
|
||||
revert('OCOM');
|
||||
// console2.log('copying orders');
|
||||
// solc can't automatically generate the code to copy from memory to storage :( so we explicitly code it here
|
||||
for( uint8 o = 0; o < memOrders.length; o++ ) {
|
||||
SwapOrder memory order = memOrders[o];
|
||||
require(order.route.exchange == Exchange.UniswapV3, 'UR'); // UR = Unknown Route
|
||||
|
||||
//NOTE: Conditional orders must not form a loop, so we disallow a conditional order that places another conditional order
|
||||
//There are many reasons this could be dangerous, one of which is that a conditional order loop could create a runaway
|
||||
//scenario where much more quantity is swapped than intended (e.g., a market order for limited quantity looping on itself,
|
||||
//resulting in unlimited quantity).
|
||||
//
|
||||
//Also note that for CONDITIONAL_ORDER_IN_CURRENT_GROUP, the referenced order must be PRIOR to the instant order, otherwise
|
||||
//the transaction will revert because `coIndex` below will be out of bounds.
|
||||
|
||||
if (order.conditionalOrder != NO_CONDITIONAL_ORDER) {
|
||||
uint64 coIndex = _conditionalOrderIndex(startIndex, order.conditionalOrder);
|
||||
order.conditionalOrder = coIndex; // replace any relative indexing with an absolute index
|
||||
|
||||
//NOTE: `order` is in `memory` so replacing `order.conditionalOrder` cannot affect any existing `condi` referenced below
|
||||
//If you change the order of operations or memory/storage in this code, be very careful about the require() check below.
|
||||
|
||||
require(
|
||||
coIndex < self.orders.length, // conditional order
|
||||
'COI' // COI = conditional order index violation
|
||||
);
|
||||
SwapOrderStatus storage condi = self.orders[coIndex]; //coIndex must therefore be a PRIOR placed order.
|
||||
require(
|
||||
// this order's output token must match the conditional order's input token
|
||||
order.tokenOut == condi.order.tokenIn
|
||||
// amountIsInput must be true
|
||||
&& condi.order.amountIsInput
|
||||
// cannot have any amount of its own
|
||||
&& condi.order.amount == 0
|
||||
// cannot chain a conditional order into another conditional order (prevent loops)
|
||||
&& condi.order.conditionalOrder == NO_CONDITIONAL_ORDER,
|
||||
'COS' // COS = conditional order suitability
|
||||
);
|
||||
}
|
||||
_createOrder(self, order, fillFeeHalfBps, ocoGroup, router, NO_CONDITIONAL_ORDER);
|
||||
}
|
||||
// console2.log('orders placed');
|
||||
}
|
||||
|
||||
uint64 constant internal CONDITIONAL_ORDER_IN_CURRENT_GROUP_MASK = CONDITIONAL_ORDER_IN_CURRENT_GROUP - 1;
|
||||
|
||||
function _conditionalOrderIndex(uint64 startIndex, uint64 coIndex) internal pure
|
||||
returns (uint64 index){
|
||||
// If the high bit (CONDITIONAL_ORDER_IN_CURRENT_GROUP) is set, then the index is relative to the
|
||||
// start of the order placement batch
|
||||
index = coIndex & CONDITIONAL_ORDER_IN_CURRENT_GROUP == 0 ? coIndex :
|
||||
startIndex + CONDITIONAL_ORDER_IN_CURRENT_GROUP_MASK & coIndex;
|
||||
}
|
||||
|
||||
function _prepTrancheStatus(Tranche memory tranche, TrancheStatus storage trancheStatus, uint32 startTime) internal {
|
||||
trancheStatus.startTime = (tranche.startTimeIsRelative ? startTime + tranche.startTime: tranche.startTime);
|
||||
trancheStatus.endTime = (tranche.endTimeIsRelative ? startTime + tranche.endTime: tranche.endTime);
|
||||
trancheStatus.activationTime = trancheStatus.startTime;
|
||||
}
|
||||
|
||||
function _createOrder(OrdersInfo storage self, SwapOrder memory order, uint8 fillFeeHalfBps, uint64 ocoGroup, IRouter router, uint64 origIndex ) internal
|
||||
returns (uint64 orderIndex)
|
||||
{
|
||||
// console2.log('exchange ok');
|
||||
// todo more order validation
|
||||
uint32 startTime = uint32(block.timestamp);
|
||||
orderIndex = uint64(self.orders.length);
|
||||
self.orders.push();
|
||||
// console2.log('pushed');
|
||||
SwapOrderStatus storage status = self.orders[orderIndex];
|
||||
status.order = order;
|
||||
status.fillFeeHalfBps = fillFeeHalfBps;
|
||||
status.startTime = startTime;
|
||||
status.ocoGroup = ocoGroup;
|
||||
status.originalOrder = origIndex;
|
||||
// console2.log('setting tranches');
|
||||
bool needStartPrice = false;
|
||||
for( uint t=0; t<order.tranches.length; t++ ) {
|
||||
Tranche memory tranche = order.tranches[t];
|
||||
|
||||
// todo implement barriers
|
||||
if( tranche.minIsBarrier || tranche.maxIsBarrier )
|
||||
revert('NI'); // Not Implemented
|
||||
|
||||
status.trancheStatus.push();
|
||||
_prepTrancheStatus(tranche,status.trancheStatus[t],startTime);
|
||||
if (tranche.minIsRatio || tranche.maxIsRatio)
|
||||
needStartPrice = true;
|
||||
// require(!tranche.marketOrder || !tranche.minIntercept.isNegative(), 'NSL'); // negative slippage
|
||||
}
|
||||
// console2.log('fee/oco');
|
||||
if (needStartPrice)
|
||||
status.startPrice = router.protectedPrice(order.route.exchange, order.tokenIn, order.tokenOut, order.route.fee);
|
||||
}
|
||||
|
||||
|
||||
struct ExecuteVars {
|
||||
uint256 price;
|
||||
// limit is passed to routes for slippage control. It is derived from the slippage variable if marketOrder is
|
||||
// true, otherwise from the minLine if it is set
|
||||
uint256 limit;
|
||||
uint256 amountIn;
|
||||
uint256 fillFee;
|
||||
uint256 trancheAmount;
|
||||
uint256 limitedAmount;
|
||||
uint256 amount;
|
||||
uint256 remaining;
|
||||
}
|
||||
|
||||
|
||||
function execute(
|
||||
OrdersInfo storage self, address owner, uint64 orderIndex, uint8 trancheIndex,
|
||||
PriceProof memory, IRouter router, IFeeManager feeManager ) internal
|
||||
returns(uint256 amountOut) {
|
||||
// Reference the tranche and validate open/available
|
||||
SwapOrderStatus storage status = self.orders[orderIndex];
|
||||
if (_isCanceled(self, orderIndex))
|
||||
revert('NO'); // Not Open
|
||||
Tranche storage tranche = status.order.tranches[trancheIndex];
|
||||
TrancheStatus storage tStatus = status.trancheStatus[trancheIndex];
|
||||
|
||||
ExecuteVars memory v;
|
||||
|
||||
//
|
||||
// Enforce constraints
|
||||
//
|
||||
|
||||
// time constraints
|
||||
require(block.timestamp < tStatus.endTime, 'TL'); // Time Late
|
||||
require(block.timestamp >= tStatus.startTime, 'TE'); // Time Early
|
||||
require(block.timestamp >= tStatus.activationTime, 'RL'); // Rate Limited
|
||||
|
||||
// market order slippage control: we overload minLine.intercept to store slippage value
|
||||
if( tranche.marketOrder && !tranche.minLine.intercept.isZero() ) {
|
||||
// console2.log('slippage');
|
||||
uint256 protectedPrice = router.protectedPrice(status.order.route.exchange, status.order.tokenIn,
|
||||
status.order.tokenOut, status.order.route.fee);
|
||||
// minIntercept is interpreted as the slippage ratio
|
||||
uint256 slippage = uint256(tranche.minLine.intercept.toFixed(96));
|
||||
v.limit = protectedPrice * 2**96 / (2**96+slippage);
|
||||
// console2.log(protectedPrice);
|
||||
// console2.log(slippage);
|
||||
// console2.log(v.limit);
|
||||
}
|
||||
|
||||
// line constraints
|
||||
else {
|
||||
v.price = 0;
|
||||
// check min line
|
||||
if( tranche.minLine.isEnabled() ) {
|
||||
v.price = router.rawPrice(status.order.route.exchange, status.order.tokenIn,
|
||||
status.order.tokenOut, status.order.route.fee);
|
||||
// console2.log('price');
|
||||
// console2.log(v.price);
|
||||
v.limit = tranche.minIsRatio ?
|
||||
tranche.minLine.ratioPrice(status.startTime, status.startPrice) :
|
||||
tranche.minLine.priceNow();
|
||||
// console2.log('min line limit', v.limit);
|
||||
// console2.log('price', v.price);
|
||||
require( v.price > v.limit, 'LL' );
|
||||
}
|
||||
// check max line
|
||||
if( tranche.maxLine.isEnabled()) {
|
||||
// price may have been already initialized by the min line
|
||||
if( v.price == 0 ) { // don't look it up a second time if we already have it.
|
||||
v.price = router.rawPrice(status.order.route.exchange, status.order.tokenIn,
|
||||
status.order.tokenOut, status.order.route.fee);
|
||||
// console2.log('price');
|
||||
// console2.log(v.price);
|
||||
}
|
||||
uint256 maxPrice = tranche.maxIsRatio ?
|
||||
tranche.maxLine.ratioPrice(status.startTime, status.startPrice) :
|
||||
tranche.maxLine.priceNow();
|
||||
// console2.log('max line limit');
|
||||
// console2.log(maxPrice);
|
||||
require( v.price <= maxPrice, 'LU' );
|
||||
}
|
||||
}
|
||||
|
||||
// compute size
|
||||
v.trancheAmount = status.order.amount * tranche.fraction / MAX_FRACTION; // the most this tranche could do
|
||||
v.amount = v.trancheAmount - tStatus.filled; // minus tranche fills
|
||||
if (tranche.rateLimitFraction != 0) {
|
||||
// rate limit sizing
|
||||
v.limitedAmount = v.trancheAmount * tranche.rateLimitFraction / MAX_FRACTION;
|
||||
if (v.amount > v.limitedAmount)
|
||||
v.amount = v.limitedAmount;
|
||||
}
|
||||
// order amount remaining
|
||||
v.remaining = status.order.amount - status.filled;
|
||||
if (v.amount > v.remaining) // not more than the order's overall remaining amount
|
||||
v.amount = v.remaining;
|
||||
require( v.amount >= status.order.minFillAmount, 'TF' );
|
||||
address recipient = status.order.outputDirectlyToOwner ? owner : address(this);
|
||||
IERC20 outToken = IERC20(status.order.tokenOut);
|
||||
// this variable is only needed for calculating the amount to forward to a conditional order, so we set it to 0 otherwise
|
||||
uint256 startingTokenOutBalance = status.order.conditionalOrder == NO_CONDITIONAL_ORDER ? 0 : outToken.balanceOf(address(this));
|
||||
|
||||
//
|
||||
// Order has been approved. Send to router for swap execution.
|
||||
//
|
||||
|
||||
// console2.log('router request:');
|
||||
// console2.log(status.order.tokenIn);
|
||||
// console2.log(status.order.tokenOut);
|
||||
// console2.log(recipient);
|
||||
// console2.log(v.amount);
|
||||
// console2.log(status.order.minFillAmount);
|
||||
// console2.log(status.order.amountIsInput);
|
||||
// console2.log(v.limit);
|
||||
// console2.log(status.order.route.fee);
|
||||
IRouter.SwapParams memory swapParams = IRouter.SwapParams(
|
||||
status.order.tokenIn, status.order.tokenOut, recipient,
|
||||
v.amount, status.order.minFillAmount, status.order.amountIsInput,
|
||||
v.limit, status.order.route.fee);
|
||||
// DELEGATECALL
|
||||
(bool success, bytes memory result) = address(router).delegatecall(
|
||||
abi.encodeWithSelector(IRouter.swap.selector, status.order.route.exchange, swapParams)
|
||||
);
|
||||
if (!success) {
|
||||
if (result.length > 0) { // if there was a reason given, forward it
|
||||
assembly ("memory-safe") {
|
||||
let size := mload(result)
|
||||
revert(add(32, result), size)
|
||||
}
|
||||
}
|
||||
else
|
||||
revert();
|
||||
}
|
||||
// delegatecall succeeded
|
||||
(v.amountIn, amountOut) = abi.decode(result, (uint256, uint256));
|
||||
|
||||
// Update filled amounts
|
||||
v.amount = status.order.amountIsInput ? v.amountIn : amountOut;
|
||||
status.filled += v.amount;
|
||||
tStatus.filled += v.amount;
|
||||
|
||||
// Update rate limit timing
|
||||
if (v.limitedAmount != 0) {
|
||||
// Rate limited. Compute the timestamp of the earliest next execution
|
||||
tStatus.activationTime = uint32(block.timestamp + v.amount * tranche.rateLimitPeriod / v.limitedAmount );
|
||||
}
|
||||
|
||||
// Take fill fee
|
||||
v.fillFee = amountOut * status.fillFeeHalfBps / 20_000;
|
||||
outToken.transfer(feeManager.fillFeeAccount(), v.fillFee);
|
||||
|
||||
emit DexorderSwapFilled(orderIndex, trancheIndex, v.amountIn, amountOut, v.fillFee,
|
||||
tStatus.activationTime);
|
||||
|
||||
// Conditional order placement
|
||||
// Fees for conditional orders are taken up-front by the VaultImpl and are not charged here.
|
||||
if (status.order.conditionalOrder != NO_CONDITIONAL_ORDER) {
|
||||
// the conditional order index will have been converted to an absolute index during placement
|
||||
SwapOrder memory condi = self.orders[status.order.conditionalOrder].order;
|
||||
// the amount forwarded will be different than amountOut due to our fee and possible token transfer taxes
|
||||
condi.amount = outToken.balanceOf(address(this)) - startingTokenOutBalance;
|
||||
// fillFee is preserved
|
||||
uint64 condiOrderIndex = _createOrder(self, condi, status.fillFeeHalfBps, NO_OCO_INDEX, router, status.order.conditionalOrder);
|
||||
emit DexorderSwapPlaced(condiOrderIndex, 1, 0, 0); // zero fees
|
||||
}
|
||||
|
||||
// Check order completion and OCO canceling
|
||||
uint256 remaining = status.order.amount - status.filled;
|
||||
if( remaining < status.order.minFillAmount ) {
|
||||
// we already get fill events so completion may be inferred without an extra Completion event
|
||||
if( status.ocoGroup != NO_OCO_INDEX)
|
||||
_cancelOco(self, status.ocoGroup);
|
||||
}
|
||||
else if( status.ocoGroup != NO_OCO_INDEX && self.ocoGroups[status.ocoGroup].mode == OcoMode.CANCEL_ON_PARTIAL_FILL )
|
||||
_cancelOco(self, status.ocoGroup);
|
||||
|
||||
// console2.log('orderlib execute completed');
|
||||
}
|
||||
|
||||
|
||||
// the price fixed-point standard is 96 decimal bits
|
||||
|
||||
function _cancelOco(OrdersInfo storage self, uint64 ocoIndex) internal {
|
||||
OcoGroup storage group = self.ocoGroups[ocoIndex];
|
||||
uint64 endIndex = group.startIndex + group.num;
|
||||
for( uint64 i=group.startIndex; i<endIndex; i++ )
|
||||
_cancelOrder(self, i);
|
||||
}
|
||||
|
||||
function _cancelOrder(OrdersInfo storage self, uint64 orderIndex) internal {
|
||||
self.orders[orderIndex].canceled = true;
|
||||
emit DexorderSwapCanceled(orderIndex);
|
||||
}
|
||||
|
||||
function _cancelAll(OrdersInfo storage self) internal {
|
||||
// All open orders will be considered cancelled.
|
||||
self.cancelAllIndex = uint64(self.orders.length);
|
||||
emit DexorderCancelAll(self.cancelAllIndex);
|
||||
}
|
||||
|
||||
function _isCanceled(OrdersInfo storage self, uint64 orderIndex) internal view returns(bool) {
|
||||
return orderIndex < self.cancelAllIndex || self.orders[orderIndex].canceled;
|
||||
}
|
||||
|
||||
}
|
||||
120
src/core/OrderSpec.sol
Normal file
120
src/core/OrderSpec.sol
Normal file
@@ -0,0 +1,120 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import {float} from "./IEEE754.sol";
|
||||
import {Line} from "./LineLib.sol";
|
||||
|
||||
uint64 constant NO_CONDITIONAL_ORDER = type(uint64).max;
|
||||
uint64 constant CONDITIONAL_ORDER_IN_CURRENT_GROUP = 1 << 63; // high bit flag
|
||||
uint64 constant NO_OCO_INDEX = type(uint64).max;
|
||||
uint16 constant MAX_FRACTION = type(uint16).max;
|
||||
uint32 constant DISTANT_PAST = 0;
|
||||
uint32 constant DISTANT_FUTURE = type(uint32).max;
|
||||
|
||||
|
||||
struct OrdersInfo {
|
||||
uint64 cancelAllIndex;
|
||||
SwapOrderStatus[] orders;
|
||||
OcoGroup[] ocoGroups;
|
||||
}
|
||||
|
||||
event DexorderSwapPlaced (uint64 indexed startOrderIndex, uint8 numOrders, uint256 orderFee, uint256 gasFee);
|
||||
|
||||
event DexorderSwapFilled (
|
||||
uint64 indexed orderIndex, uint8 indexed trancheIndex,
|
||||
uint256 amountIn, uint256 amountOut, uint256 fillFee,
|
||||
uint32 nextExecutionTime
|
||||
);
|
||||
|
||||
event DexorderSwapCanceled (uint64 orderIndex);
|
||||
event DexorderCancelAll (uint64 cancelAllIndex);
|
||||
|
||||
enum Exchange {
|
||||
UniswapV2, // 0
|
||||
UniswapV3 // 1
|
||||
}
|
||||
|
||||
// todo does embedding Route into SwapOrder take a full word?
|
||||
struct Route {
|
||||
Exchange exchange; // only ever UniswapV3 currently
|
||||
uint24 fee; // as of now, used as the "maxFee" parameter when placing swaps onto UniswapV3
|
||||
}
|
||||
|
||||
// Primary data structure for order specification. These fields are immutable after order placement.
|
||||
struct SwapOrder {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
Route route;
|
||||
uint256 amount; // the maximum quantity to fill
|
||||
uint256 minFillAmount; // if a tranche has less than this amount available to fill, it is considered completed
|
||||
bool amountIsInput; // whether amount is an in or out amount
|
||||
bool outputDirectlyToOwner; // whether the swap proceeds should go to the vault, or directly to the vault owner
|
||||
uint64 conditionalOrder; // use NO_CONDITIONAL_ORDER for normal orders. If the high bit is set, the order number is relative to the currently placed group of orders. e.g. `CONDITIONAL_ORDER_IN_CURRENT_GROUP & 2` refers to the third item in the order group currently being placed.
|
||||
Tranche[] tranches; // see Tranche below
|
||||
}
|
||||
|
||||
// "Status" includes dynamic information about the trade in addition to its static SwapOrder specification.
|
||||
struct SwapOrderStatus {
|
||||
SwapOrder order;
|
||||
// the fill fee is remembered from the active fee schedule at order creation time.
|
||||
// 1/20_000 "half bps" means the maximum representable value is 1.275%
|
||||
uint8 fillFeeHalfBps;
|
||||
bool canceled; // if true, the order is considered canceled, irrespective of its index relative to cancelAllIndex
|
||||
uint32 startTime; // the earliest time that an order can execute (as compared with block.timestamp)
|
||||
uint64 ocoGroup; // the "one cancels the other" group index in ocoGroups
|
||||
uint64 originalOrder; // Index of the original order in the orders array
|
||||
uint256 startPrice; // the price at which an order starts (e.g., the starting limit price)
|
||||
uint256 filled; // total amount filled so far
|
||||
TrancheStatus[] trancheStatus; // the status of each individual Tranche
|
||||
}
|
||||
|
||||
struct Tranche {
|
||||
uint16 fraction; // the fraction of the order's total amount that this tranche will, at most, fill
|
||||
//note: relative times become concrete when an order is placed for execution, this means that a
|
||||
//conditional order will calculate a concrete time once its condition becomes true
|
||||
bool startTimeIsRelative;
|
||||
bool endTimeIsRelative;
|
||||
bool minIsBarrier; // not yet supported
|
||||
bool maxIsBarrier; // not yet supported
|
||||
bool marketOrder; // if true, both min and max lines are ignored, and minIntercept is treated as a maximum slippage value (use positive numbers)
|
||||
bool minIsRatio; // todo price isRatio: recalculate intercept
|
||||
bool maxIsRatio;
|
||||
bool _reserved7;
|
||||
uint16 rateLimitFraction; // max fraction of this tranche's amount per rate-limited execution
|
||||
uint24 rateLimitPeriod; // seconds between rate limit resets
|
||||
|
||||
uint32 startTime; // use DISTANT_PAST to effectively disable
|
||||
uint32 endTime; // use DISTANT_FUTURE to effectively disable
|
||||
|
||||
// If intercept and slope are both 0, the line is disabled
|
||||
// Prices are always in terms of outputToken as the quote currency: the output amount per input amount. This is
|
||||
// equivalent to saying all orders are viewed as sells relative to the price.
|
||||
// The minLine is equivalent to a traditional limit order constraint, except this limit line can be diagonal.
|
||||
Line minLine;
|
||||
// The maxLine will be relatively unused, since it represents a boundry on TOO GOOD of a price.
|
||||
Line maxLine;
|
||||
}
|
||||
|
||||
struct TrancheStatus {
|
||||
uint256 filled; // sum(trancheFilled) == filled
|
||||
uint32 activationTime; // related to rate limit: the earliest time at which each tranche can execute. If 0, indicates TrancheStatus not concrete
|
||||
uint32 startTime;
|
||||
uint32 endTime;
|
||||
}
|
||||
|
||||
struct PriceProof {
|
||||
// todo
|
||||
uint proof;
|
||||
}
|
||||
|
||||
enum OcoMode {
|
||||
NO_OCO,
|
||||
CANCEL_ON_PARTIAL_FILL,
|
||||
CANCEL_ON_COMPLETION
|
||||
}
|
||||
|
||||
struct OcoGroup {
|
||||
OcoMode mode;
|
||||
uint64 startIndex; // starting orderIndex of the group
|
||||
uint8 num; // number of orders in the group
|
||||
}
|
||||
61
src/core/Router.sol
Normal file
61
src/core/Router.sol
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import "./UniswapV3.sol";
|
||||
import "./OrderSpec.sol";
|
||||
import "./UniswapSwapper.sol";
|
||||
import {IRouter} from "../interface/IRouter.sol";
|
||||
|
||||
contract Router is IRouter, UniswapV3Swapper {
|
||||
|
||||
constructor (
|
||||
IUniswapV3Factory uniswapV3Factory, ISwapRouter uniswapV3SwapRouter, uint32 uniswapV3OracleSeconds
|
||||
)
|
||||
UniswapV3Swapper(uniswapV3Factory, uniswapV3SwapRouter, uniswapV3OracleSeconds)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
function rawPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee) external view
|
||||
returns (uint256) {
|
||||
if (exchange == Exchange.UniswapV3)
|
||||
return _univ3_rawPrice(tokenIn, tokenOut, maxFee);
|
||||
revert('UR');
|
||||
}
|
||||
|
||||
function protectedPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee) external view
|
||||
returns (uint256) {
|
||||
if (exchange == Exchange.UniswapV3)
|
||||
return _univ3_protectedPrice(tokenIn, tokenOut, maxFee);
|
||||
revert('UR');
|
||||
}
|
||||
|
||||
function swap( Exchange exchange, SwapParams memory params ) external
|
||||
returns (uint256 amountIn, uint256 amountOut) {
|
||||
if (exchange == Exchange.UniswapV3)
|
||||
return _univ3_swap(params);
|
||||
revert('UR');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
contract ArbitrumRouter is Router {
|
||||
constructor()
|
||||
Router(
|
||||
UniswapV3Arbitrum.factory,
|
||||
UniswapV3Arbitrum.swapRouter,
|
||||
10 // Slippage TWAP window
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
contract ArbitrumSepoliaRouter is Router {
|
||||
constructor()
|
||||
Router(
|
||||
UniswapV3ArbitrumSepolia.factory,
|
||||
UniswapV3ArbitrumSepolia.swapRouter,
|
||||
10 // Slippage TWAP window
|
||||
) {}
|
||||
}
|
||||
|
||||
208
src/core/UniswapSwapper.sol
Normal file
208
src/core/UniswapSwapper.sol
Normal file
@@ -0,0 +1,208 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import "@forge-std/console2.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {Util} from "./Util.sol";
|
||||
import {UniswapV3} from "../core/UniswapV3.sol";
|
||||
import {IUniswapV3Pool} from "../../lib_uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import {IUniswapV3Factory} from "../../lib_uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
||||
import {TickMath} from "../../lib_uniswap/v3-core/contracts/libraries/TickMath.sol";
|
||||
import {ISwapRouter} from "../../lib_uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
|
||||
import "../../lib_uniswap/v3-periphery/contracts/libraries/TransferHelper.sol";
|
||||
import {PoolAddress} from "../../lib_uniswap/v3-periphery/contracts/libraries/PoolAddress.sol";
|
||||
import {IRouter} from "../interface/IRouter.sol";
|
||||
|
||||
|
||||
contract UniswapV3Swapper {
|
||||
// The top of this contract implements the ISwapper interface in terms of the UniswapV3 specific methods
|
||||
// at the bottom
|
||||
|
||||
ISwapRouter private immutable swapRouter;
|
||||
IUniswapV3Factory private immutable factory;
|
||||
uint32 private immutable oracleSeconds;
|
||||
|
||||
constructor( IUniswapV3Factory factory_, ISwapRouter swapRouter_, uint32 oracleSeconds_ ) {
|
||||
factory = factory_;
|
||||
swapRouter = swapRouter_;
|
||||
oracleSeconds = oracleSeconds_;
|
||||
}
|
||||
|
||||
function _univ3_rawPrice(address tokenIn, address tokenOut, uint24 maxFee) internal view
|
||||
returns (uint256 price) {
|
||||
(IUniswapV3Pool pool, bool inverted) = UniswapV3.getPool(factory, tokenIn, tokenOut, maxFee);
|
||||
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
|
||||
return Util.sqrtToPrice(sqrtPriceX96, inverted);
|
||||
}
|
||||
|
||||
// Returns the stabilized (oracle) price
|
||||
function _univ3_protectedPrice(address tokenIn, address tokenOut, uint24 maxFee) internal view
|
||||
returns (uint256)
|
||||
{
|
||||
// console2.log('oracle');
|
||||
// console2.log(oracleSeconds);
|
||||
if (oracleSeconds!=0){
|
||||
(IUniswapV3Pool pool, bool inverted) = UniswapV3.getPool(factory, tokenIn, tokenOut, maxFee);
|
||||
uint32[] memory secondsAgos = new uint32[](2);
|
||||
secondsAgos[0] = oracleSeconds;
|
||||
secondsAgos[1] = 0;
|
||||
try pool.observe(secondsAgos) returns (int56[] memory cumulative, uint160[] memory) {
|
||||
int56 delta = cumulative[1] - cumulative[0];
|
||||
int32 secsI = int32(oracleSeconds);
|
||||
int24 mean = int24(delta / secsI);
|
||||
if (delta < 0 && (delta % secsI != 0))
|
||||
mean--;
|
||||
// use Uniswap's tick-to-sqrt-price because it's verified
|
||||
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(mean);
|
||||
return Util.sqrtToPrice(sqrtPriceX96, inverted);
|
||||
}
|
||||
catch Error( string memory /*reason*/ ) {
|
||||
//fall through to return the rawPrice
|
||||
// console2.log('oracle broken');
|
||||
}
|
||||
}
|
||||
return _univ3_rawPrice(tokenIn, tokenOut, maxFee);
|
||||
}
|
||||
|
||||
function _univ3_swap(IRouter.SwapParams memory params) internal
|
||||
returns (uint256 amountIn, uint256 amountOut) {
|
||||
if( params.limitPriceX96 != 0 ) {
|
||||
bool inverted = params.tokenIn > params.tokenOut;
|
||||
if (inverted) {
|
||||
// console2.log('inverting params.limitPriceX96');
|
||||
// console2.log(params.limitPriceX96);
|
||||
params.limitPriceX96 = 2**96 * 2**96 / params.limitPriceX96;
|
||||
}
|
||||
// console2.log('params.limitPriceX96');
|
||||
// console2.log(params.limitPriceX96);
|
||||
}
|
||||
if (params.amountIsInput)
|
||||
(amountIn, amountOut) = _univ3_swapExactInput(params);
|
||||
else
|
||||
(amountIn, amountOut) = _univ3_swapExactOutput(params);
|
||||
}
|
||||
|
||||
|
||||
function _univ3_swapExactInput(IRouter.SwapParams memory params) internal
|
||||
returns (uint256 amountIn, uint256 amountOut)
|
||||
{
|
||||
// struct ExactInputSingleParams {
|
||||
// address tokenIn;
|
||||
// address tokenOut;
|
||||
// uint24 fee;
|
||||
// address recipient;
|
||||
// uint256 deadline;
|
||||
// uint256 amountIn;
|
||||
// uint256 amountOutMinimum;
|
||||
// uint160 sqrtPriceLimitX96;
|
||||
// }
|
||||
// console2.log('swapExactInput');
|
||||
// console2.log(address(this));
|
||||
// console2.log(params.tokenIn);
|
||||
// console2.log(params.tokenOut);
|
||||
// console2.log(uint(params.maxFee));
|
||||
// console2.log(address(params.recipient));
|
||||
// console2.log(params.amount);
|
||||
// console2.log(params.amountIsInput);
|
||||
// console2.log(uint(params.limitPriceX96));
|
||||
// console2.log(address(swapRouter));
|
||||
|
||||
amountIn = params.amount;
|
||||
uint256 balance = IERC20(params.tokenIn).balanceOf(address(this));
|
||||
// console2.log('amountIn balance');
|
||||
// console2.log(balance);
|
||||
if( balance == 0 || balance < params.minAmount ) // minAmount is units of input token
|
||||
revert('IIA');
|
||||
if( balance < amountIn )
|
||||
amountIn = balance;
|
||||
|
||||
TransferHelper.safeApprove(params.tokenIn, address(swapRouter), amountIn);
|
||||
// if (params.sqrtPriceLimitX96 == 0)
|
||||
// params.sqrtPriceLimitX96 = params.tokenIn < params.tokenOut ? TickMath.MIN_SQRT_RATIO+1 : TickMath.MAX_SQRT_RATIO-1;
|
||||
|
||||
uint160 sqrtPriceLimitX96 = uint160(Util.sqrt(uint256(params.limitPriceX96)<<96));
|
||||
// console2.log('sqrt price limit x96');
|
||||
// console2.log(uint(sqrtPriceLimitX96));
|
||||
|
||||
// console2.log('swapping...');
|
||||
amountOut = swapRouter.exactInputSingle(ISwapRouter.ExactInputSingleParams({
|
||||
tokenIn: params.tokenIn, tokenOut: params.tokenOut, fee: params.maxFee, recipient: params.recipient,
|
||||
deadline: block.timestamp, amountIn: amountIn, amountOutMinimum: 1, sqrtPriceLimitX96: sqrtPriceLimitX96
|
||||
}));
|
||||
// console2.log('swapped');
|
||||
// console2.log(amountOut);
|
||||
TransferHelper.safeApprove(params.tokenIn, address(swapRouter), 0);
|
||||
// console2.log('revoked approval');
|
||||
}
|
||||
|
||||
function _univ3_swapExactOutput(IRouter.SwapParams memory params) internal
|
||||
returns (uint256 amountIn, uint256 amountOut)
|
||||
{
|
||||
// struct ExactOutputSingleParams {
|
||||
// address tokenIn;
|
||||
// address tokenOut;
|
||||
// uint24 fee;
|
||||
// address recipient;
|
||||
// uint256 deadline;
|
||||
// uint256 amountOut;
|
||||
// uint256 amountInMaximum;
|
||||
// uint160 sqrtPriceLimitX96;
|
||||
// }
|
||||
uint256 balance = IERC20(params.tokenIn).balanceOf(address(this));
|
||||
if( balance == 0 )
|
||||
revert('IIA');
|
||||
uint256 maxAmountIn = balance;
|
||||
|
||||
// console2.log('swapExactOutput');
|
||||
// console2.log(address(this));
|
||||
// console2.log(params.tokenIn);
|
||||
// console2.log(params.tokenOut);
|
||||
// console2.log(uint(params.maxFee));
|
||||
// console2.log(address(params.recipient));
|
||||
// console2.log(params.amount);
|
||||
// console2.log(uint(params.limitPriceX96));
|
||||
// console2.log(address(swapRouter));
|
||||
// console2.log('approve');
|
||||
// console2.log(maxAmountIn);
|
||||
|
||||
TransferHelper.safeApprove(params.tokenIn, address(swapRouter), maxAmountIn);
|
||||
|
||||
uint160 sqrtPriceLimitX96 = uint160(Util.sqrt(uint256(params.limitPriceX96)<<96));
|
||||
// console2.log('sqrt price limit x96');
|
||||
// console2.log(uint(sqrtPriceLimitX96));
|
||||
|
||||
// console2.log('swapping...');
|
||||
try swapRouter.exactOutputSingle(ISwapRouter.ExactOutputSingleParams({
|
||||
tokenIn: params.tokenIn, tokenOut: params.tokenOut, fee: params.maxFee, recipient: params.recipient,
|
||||
deadline: block.timestamp, amountOut: params.amount, amountInMaximum: maxAmountIn,
|
||||
sqrtPriceLimitX96: sqrtPriceLimitX96
|
||||
})) returns (uint256 amtIn) {
|
||||
amountIn = amtIn;
|
||||
amountOut = params.amount;
|
||||
}
|
||||
catch Error( string memory reason ) {
|
||||
// todo check reason before trying exactinput
|
||||
// if the input amount was insufficient, use exactInputSingle to spend whatever remains.
|
||||
try swapRouter.exactInputSingle(ISwapRouter.ExactInputSingleParams({
|
||||
tokenIn: params.tokenIn, tokenOut: params.tokenOut, fee: params.maxFee, recipient: params.recipient,
|
||||
deadline: block.timestamp, amountIn: maxAmountIn, amountOutMinimum: 1, sqrtPriceLimitX96: sqrtPriceLimitX96
|
||||
})) returns (uint256 amtOut) {
|
||||
amountIn = maxAmountIn;
|
||||
amountOut = amtOut;
|
||||
}
|
||||
catch Error( string memory ) {
|
||||
revert(reason); // revert on the original reason
|
||||
}
|
||||
}
|
||||
// Why should we short-circuit output amounts that are below the minAmount? We have already paid the gas to
|
||||
// get this far. Might as well accept any amount.
|
||||
// require( amountOut >= params.minAmount, 'IIA' );
|
||||
// console2.log('swapped');
|
||||
// console2.log(amountIn);
|
||||
// console2.log(amountOut);
|
||||
|
||||
// revoke approval
|
||||
TransferHelper.safeApprove(params.tokenIn, address(swapRouter), 0);
|
||||
}
|
||||
|
||||
}
|
||||
35
src/core/UniswapV3.sol
Normal file
35
src/core/UniswapV3.sol
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import "../../lib_uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
||||
import "../../lib_uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
|
||||
import "../../lib_uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol";
|
||||
import {IUniswapV3Pool} from "../../lib_uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import {IWETH9} from "../../lib_uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol";
|
||||
|
||||
|
||||
library UniswapV3 {
|
||||
function getPool( IUniswapV3Factory factory, address tokenA, address tokenB, uint24 fee) internal pure
|
||||
returns (IUniswapV3Pool pool, bool inverted) {
|
||||
PoolAddress.PoolKey memory key = PoolAddress.getPoolKey(tokenA, tokenB, fee);
|
||||
pool = IUniswapV3Pool(PoolAddress.computeAddress(address(factory), key));
|
||||
inverted = tokenA > tokenB;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Uniswap v3 on Arbitrum One
|
||||
library UniswapV3Arbitrum {
|
||||
IWETH9 public constant weth9 = IWETH9(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
|
||||
IUniswapV3Factory public constant factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
|
||||
ISwapRouter public constant swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
|
||||
INonfungiblePositionManager public constant nfpm = INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88);
|
||||
}
|
||||
|
||||
// TESTNET
|
||||
library UniswapV3ArbitrumSepolia {
|
||||
IUniswapV3Factory public constant factory = IUniswapV3Factory(0x248AB79Bbb9bC29bB72f7Cd42F17e054Fc40188e);
|
||||
ISwapRouter public constant swapRouter = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
|
||||
INonfungiblePositionManager public constant nfpm = INonfungiblePositionManager(0x6b2937Bde17889EDCf8fbD8dE31C3C2a70Bc4d65);
|
||||
}
|
||||
|
||||
40
src/core/Util.sol
Normal file
40
src/core/Util.sol
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import "../../lib_uniswap/v3-core/contracts/libraries/FullMath.sol";
|
||||
|
||||
library Util {
|
||||
// from https://github.com/ethereum/dapp-bin/pull/50/files
|
||||
// the same logic as UniswapV2's version of sqrt
|
||||
function sqrt(uint x) internal pure returns (uint y) {
|
||||
// todo overflow is not possible in this algorithm, correct? we may wrap it in unchecked {}
|
||||
if (x == 0) return 0;
|
||||
else if (x <= 3) return 1;
|
||||
uint z = (x + 1) / 2;
|
||||
y = x;
|
||||
while (z < y)
|
||||
{
|
||||
y = z;
|
||||
z = (x / z + z) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
function roundTick(int24 tick, int24 window) internal pure returns (int24) {
|
||||
// NOTE: we round half toward zero
|
||||
int24 mod = tick % window;
|
||||
if (tick < 0)
|
||||
return - mod <= window / 2 ? tick - mod : tick - (window + mod);
|
||||
else
|
||||
return mod > window / 2 ? tick + (window - mod) : tick - mod;
|
||||
}
|
||||
|
||||
function sqrtToPrice( uint160 sqrtPriceX96, bool inverted ) internal pure returns (uint256) {
|
||||
return inverted ?
|
||||
FullMath.mulDiv(2**96 * 2**96 / sqrtPriceX96, 2**96, sqrtPriceX96) :
|
||||
FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, 2**96);
|
||||
}
|
||||
|
||||
function invertX96( uint256 valueX96 ) internal pure returns (uint256) {
|
||||
return 2**96 * 2**96 / valueX96;
|
||||
}
|
||||
}
|
||||
116
src/core/Vault.sol
Normal file
116
src/core/Vault.sol
Normal file
@@ -0,0 +1,116 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import {IVaultFactory} from "../interface/IVaultFactory.sol";
|
||||
import {IVaultProxy,IVaultImpl} from "../interface/IVault.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {Proxy} from "@openzeppelin/contracts/proxy/Proxy.sol";
|
||||
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import "./OrderSpec.sol";
|
||||
|
||||
|
||||
// All state in Vault is declared in VaultStorage so it will be identical in VaultImpl
|
||||
|
||||
contract VaultStorage is ReentrancyGuard { // The re-entrancy lock is part of the state
|
||||
address internal _impl;
|
||||
address internal _owner;
|
||||
bool internal _killed; // each Vault may be independently killed by its owner at any time
|
||||
OrdersInfo internal _ordersInfo;
|
||||
}
|
||||
|
||||
// Vault is implemented in three parts:
|
||||
// 1. VaultStorage contains the data members, which cannot be upgraded. The number of data slots is fixed upon
|
||||
// construction of the Vault contract.
|
||||
// 2. Vault itself inherits from the state and adds several "top-level" methods for receiving and withdrawing funds and
|
||||
// for upgrading the VaultImpl delegate. These vault methods may not be changed or upgraded.
|
||||
// 3. VaultImpl is a deployed contract shared by Vaults. If a method call is not found on Vault directly, the call
|
||||
// is delegated to the contract address stored in the `_impl` variable. The VaultImpl contract is basically the
|
||||
// OrderLib, implementing the common order manipulation methods. Each Vault may be independently upgraded to point
|
||||
// to a new VaultImpl contract by the owner calling their vault's `upgrade()` method with the correct argument.
|
||||
|
||||
contract Vault is IVaultProxy, VaultStorage, Proxy {
|
||||
//REV putting all variable decls together so we can more easily see visibility and immutability errors
|
||||
IVaultFactory immutable private _factory;
|
||||
uint8 immutable private _num;
|
||||
|
||||
function implementation() external view override returns(address) {return _impl;}
|
||||
|
||||
function factory() external view override returns(address) {return address(_factory);}
|
||||
|
||||
function killed() external view override returns(bool) {return _killed;}
|
||||
|
||||
function owner() external view override returns(address) {return _owner;}
|
||||
|
||||
function num() external view override returns(uint8) {return _num;}
|
||||
|
||||
// for OpenZeppelin Proxy to call implementation contract via fallback
|
||||
function _implementation() internal view override returns (address) {
|
||||
// If the VaultFactory that created this vault has had its kill switch activated, do not trust the implementation.
|
||||
// local _killed var allows individual users to put their vaults into "killed" mode, where Dexorder functionality
|
||||
// is disabled, but funds can still be moved.
|
||||
require(!_killed && !_factory.killed(), 'K');
|
||||
return _impl;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
_factory = IVaultFactory(msg.sender);
|
||||
(_owner, _num, _impl) = _factory.parameters();
|
||||
emit VaultCreated( _owner, _num );
|
||||
emit VaultImplChanged(_impl);
|
||||
}
|
||||
|
||||
// "Killing" a Vault prevents this proxy from forwarding any calls to the VaultImpl delegate contract. This means
|
||||
// all executions are stopped. Orders cannot be placed or canceled.
|
||||
function kill() external override onlyOwner {
|
||||
emit Killed();
|
||||
_killed = true;
|
||||
}
|
||||
|
||||
function upgrade(address newImpl) external override onlyOwner {
|
||||
// we force the upgrader to explicitly pass in the implementation contract address, then we
|
||||
// ensure that it matches the factory's current version.
|
||||
require( newImpl == _factory.implementation(), 'UV' );
|
||||
address oldImpl = _impl;
|
||||
if(oldImpl==newImpl){
|
||||
return;
|
||||
}
|
||||
_impl = newImpl;
|
||||
IVaultImpl(address(this)).vaultImplDidChange(oldImpl);
|
||||
emit VaultImplChanged(newImpl);
|
||||
}
|
||||
|
||||
receive() external payable override {
|
||||
emit Deposit(msg.sender, msg.value);
|
||||
}
|
||||
|
||||
function withdraw(uint256 amount) external override {
|
||||
_withdrawNative(payable(_owner), amount);
|
||||
}
|
||||
|
||||
function withdrawTo(address payable recipient, uint256 amount) external override {
|
||||
_withdrawNative(recipient, amount);
|
||||
}
|
||||
|
||||
function _withdrawNative(address payable recipient, uint256 amount) internal onlyOwner {
|
||||
recipient.transfer(amount);
|
||||
emit Withdrawal(recipient, msg.value);
|
||||
}
|
||||
|
||||
function withdraw(IERC20 token, uint256 amount) external override {
|
||||
_withdraw(token, _owner, amount);
|
||||
}
|
||||
|
||||
function withdrawTo(IERC20 token, address recipient, uint256 amount) external override {
|
||||
_withdraw(token, recipient, amount);
|
||||
}
|
||||
|
||||
function _withdraw(IERC20 token, address recipient, uint256 amount) internal onlyOwner nonReentrant {
|
||||
token.transfer(recipient, amount);
|
||||
}
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == _owner, "not owner");
|
||||
_;
|
||||
}
|
||||
|
||||
}
|
||||
133
src/core/VaultFactory.sol
Normal file
133
src/core/VaultFactory.sol
Normal file
@@ -0,0 +1,133 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import "./Vault.sol";
|
||||
import "../interface/IVault.sol";
|
||||
import "../interface/IVaultFactory.sol";
|
||||
import "../more/Dexorder.sol";
|
||||
|
||||
contract VaultFactory is IVaultFactory {
|
||||
|
||||
// The upgrader account may propose upgrades to the VaultImpl delegate contract used by the Vaults created by this
|
||||
// VaultFactory. These upgrade proposals take effect only after a long delay, during which time the community can
|
||||
// openly review the upgrade for security before it takes effect. Furthermore, each Vault owner must individually
|
||||
// approve the upgrade on their Vault for it to take effect.
|
||||
address public immutable override upgrader;
|
||||
|
||||
// "Killing" is an extreme countermeasure against either unforeen, unsolvable bugs or a compromising of the upgrader
|
||||
// account. Killing causes Vaults to stop forwarding method calls to their VaultImpl proxies, leaving vaults
|
||||
// in a deposit/withdraw-only mode, with all executions and order-related operations halted. Killing is
|
||||
// irreversible, and a hacker cannot stop the original upgrader account from shutting everything down in such an
|
||||
// emergency.
|
||||
bool public killed;
|
||||
event Killed();
|
||||
|
||||
// This is the common implementation contract, which is the delegate of the vault proxies. Each created vault
|
||||
// keeps its own delegate pointer, and the owner of each vault must explicity opt-in to upgrades (changes) of
|
||||
// their vault's implementation. The _vaultImpl pointed to here is the initial implementation for new vaults and
|
||||
// the new implementation for any vaults that authorize an upgrade().
|
||||
// The _vaultImpl cannot be changed immediately. The admin of the VaultFactory proposes a new implementation
|
||||
// which is stored in proposedImpl for at least UPGRADE_NOTICE_DURATION seconds before it becomes active as the
|
||||
// new implementation. This gives users and the community time to review any proposed changes before they can be
|
||||
// adopted. Any malicous entity that hacks the Dexorder admin account would also suffer this 30-day delay before
|
||||
// their exploit implementation would be used by anyone. Upgrade proposals are easily, publicly visible by looking
|
||||
// for the VaultImplProposed event on-chain.
|
||||
|
||||
// This is the implementation delegate for newly created vaults. Use implementation() to get, then cast to IVaultImpl type.
|
||||
address private _vaultImpl;
|
||||
|
||||
// Upgrades
|
||||
uint32 public immutable upgradeNoticeDuration;
|
||||
|
||||
|
||||
// 0 if no upgrade is pending, otherwise the timestamp when proposedImpl becomes the default for new vaults
|
||||
uint32 public override proposedImplActivationTimestamp;
|
||||
address public override proposedImpl; // the contract address of a proposed upgrade to the default vault implementation.
|
||||
|
||||
|
||||
constructor ( address upgrader_, address vaultImpl_, uint32 upgradeNoticeDuration_ ) {
|
||||
upgrader = upgrader_;
|
||||
_vaultImpl = vaultImpl_;
|
||||
upgradeNoticeDuration = upgradeNoticeDuration_;
|
||||
// these are all defaults
|
||||
// killed = false;
|
||||
// proposedImpl = address(0);
|
||||
// proposedImplActivationTimestamp = 0;
|
||||
}
|
||||
|
||||
struct Parameters {
|
||||
address owner;
|
||||
uint8 num;
|
||||
address vaultImpl;
|
||||
}
|
||||
|
||||
Parameters public override parameters;
|
||||
|
||||
function deployVault() public override returns (IVault vault) {
|
||||
return _deployVault(msg.sender, 0);
|
||||
}
|
||||
|
||||
function deployVault(uint8 num) public override returns (IVault vault) {
|
||||
return _deployVault(msg.sender, num);
|
||||
}
|
||||
|
||||
function deployVault(address owner) public override returns (IVault vault) {
|
||||
return _deployVault(owner, 0);
|
||||
}
|
||||
|
||||
function deployVault(address owner, uint8 num) public override returns (IVault vault) {
|
||||
return _deployVault(owner, num);
|
||||
}
|
||||
|
||||
function _deployVault(address owner, uint8 num) internal returns (IVault vault) {
|
||||
// We still allow Vault creation even if the factory has been killed. These vaults will simply be in withdraw
|
||||
// only mode. If someone accidentally sends money to their designated vault address but no contract has
|
||||
// been created there yet, being able to still deploy a vault will let the owner recover those funds.
|
||||
|
||||
parameters = Parameters(owner, num, _impl());
|
||||
// Vault addresses are salted with the owner address and vault number
|
||||
vault = IVault(payable(new Vault{salt: keccak256(abi.encodePacked(owner,num))}()));
|
||||
delete parameters;
|
||||
}
|
||||
|
||||
|
||||
function _impl() internal returns (address) {
|
||||
if (proposedImplActivationTimestamp != 0 && proposedImplActivationTimestamp <= block.timestamp) {
|
||||
// time to start using the latest upgrade
|
||||
_vaultImpl = proposedImpl;
|
||||
proposedImplActivationTimestamp = 0;
|
||||
emit IVaultProxy.VaultImplChanged(_vaultImpl);
|
||||
}
|
||||
return _vaultImpl;
|
||||
}
|
||||
|
||||
|
||||
function implementation() external view override returns (address) {
|
||||
return proposedImplActivationTimestamp != 0 && proposedImplActivationTimestamp <= block.timestamp ?
|
||||
proposedImpl : _vaultImpl;
|
||||
}
|
||||
|
||||
|
||||
modifier onlyUpgrader() {
|
||||
require(msg.sender == upgrader, "not upgrader");
|
||||
_;
|
||||
}
|
||||
|
||||
function upgradeImplementation( address newImpl ) external onlyUpgrader {
|
||||
proposedImpl = newImpl;
|
||||
proposedImplActivationTimestamp = uint32(block.timestamp + upgradeNoticeDuration);
|
||||
emit IVaultProxy.VaultImplProposed( newImpl, proposedImplActivationTimestamp);
|
||||
}
|
||||
|
||||
// If the upgrader ever calls kill(), VaultProxys will stop forwarding calls to the implementation contract. The withdrawal()
|
||||
// methods directly implemented on VaultProxy will continue to work.
|
||||
// This is intended as a last-ditch safety measure in case the upgrader account was leaked, and a malicious
|
||||
// VaultImpl has been proposed and cannot be stopped. In such a case, the entire factory
|
||||
// and all of its Vaults executions will be shut down to protect funds. Vault deposit/withdrawal will continue to
|
||||
// work normally.
|
||||
function kill() external onlyUpgrader {
|
||||
killed = true;
|
||||
emit Killed();
|
||||
}
|
||||
|
||||
}
|
||||
157
src/core/VaultImpl.sol
Normal file
157
src/core/VaultImpl.sol
Normal file
@@ -0,0 +1,157 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import {console2} from "@forge-std/console2.sol";
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import {IFeeManager} from "../interface/IFeeManager.sol";
|
||||
import {IVaultProxy, IVaultImpl} from "../interface/IVault.sol";
|
||||
import {VaultStorage} from "./Vault.sol";
|
||||
import {Dexorder} from "../more/Dexorder.sol";
|
||||
import "./OrderSpec.sol";
|
||||
import {OrderLib} from "./OrderLib.sol";
|
||||
import {IUniswapV3Factory} from "../../lib_uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
||||
import {IRouter} from "../interface/IRouter.sol";
|
||||
import {IWETH9} from "../../lib_uniswap/v3-periphery/contracts/interfaces/external/IWETH9.sol";
|
||||
import {UniswapV3Arbitrum} from "./UniswapV3.sol";
|
||||
|
||||
|
||||
// There is only one VaultImpl contract deployed, which is shared by all Vaults. When the vault proxy calls into
|
||||
// the implementation contract, the original Vault's state is used. So here the VaultImpl inherits the vault state but in
|
||||
// usage, this state will be the state of the calling Vault, not of the deployed VaultImpl contract instance.
|
||||
|
||||
contract VaultImpl is IVaultImpl, VaultStorage {
|
||||
|
||||
uint256 constant public version = 1;
|
||||
|
||||
IFeeManager public immutable feeManager;
|
||||
IRouter private immutable router;
|
||||
IWETH9 private immutable weth9;
|
||||
|
||||
constructor( IRouter router_, IFeeManager feeManager_, address weth9_ ) {
|
||||
router = router_;
|
||||
feeManager = feeManager_;
|
||||
weth9 = IWETH9(weth9_);
|
||||
}
|
||||
|
||||
function numSwapOrders() external view returns (uint64 num) {
|
||||
return uint64(_ordersInfo.orders.length);
|
||||
}
|
||||
|
||||
function placementFee(SwapOrder memory order, IFeeManager.FeeSchedule memory sched) public view
|
||||
returns (uint256 orderFee, uint256 gasFee) {
|
||||
return _placementFee(order,sched);
|
||||
}
|
||||
|
||||
function _placementFee(SwapOrder memory order, IFeeManager.FeeSchedule memory sched) internal view
|
||||
returns (uint256 orderFee, uint256 gasFee) {
|
||||
if (order.conditionalOrder == NO_CONDITIONAL_ORDER)
|
||||
return OrderLib._placementFee(order, sched);
|
||||
uint64 condiIndex = OrderLib._conditionalOrderIndex(uint64(_ordersInfo.orders.length), order.conditionalOrder);
|
||||
SwapOrder memory condi = _ordersInfo.orders[condiIndex].order;
|
||||
return OrderLib._placementFee(order, sched, condi);
|
||||
|
||||
}
|
||||
|
||||
function placementFee(SwapOrder[] memory orders, IFeeManager.FeeSchedule memory sched) public view
|
||||
returns (uint256 orderFee, uint256 gasFee) {
|
||||
orderFee = 0;
|
||||
gasFee = 0;
|
||||
for( uint i=0; i<orders.length; i++ ) {
|
||||
(uint256 ofee, uint256 efee) = _placementFee(orders[i], sched);
|
||||
orderFee += ofee;
|
||||
gasFee += efee;
|
||||
}
|
||||
}
|
||||
|
||||
function placeDexorder(SwapOrder memory order) external payable onlyOwner nonReentrant {
|
||||
// console2.log('placing order');
|
||||
IFeeManager.FeeSchedule memory sched = feeManager.fees();
|
||||
(uint256 orderFee, uint256 gasFee) = OrderLib._placementFee(order, sched);
|
||||
// We force the value to be sent with the message so that the user can see the fee immediately in their wallet
|
||||
// software before they confirm the transaction. If the user overpays, the extra amount is refunded.
|
||||
_takeFee(payable(msg.sender), orderFee, gasFee);
|
||||
uint64 startIndex = uint64(_ordersInfo.orders.length);
|
||||
OrderLib._placeOrder(_ordersInfo, order, sched.fillFeeHalfBps, router);
|
||||
emit DexorderSwapPlaced(startIndex, 1, orderFee, gasFee);
|
||||
// console2.log('order placed');
|
||||
}
|
||||
|
||||
function placeDexorders(SwapOrder[] memory orders, OcoMode ocoMode) external payable onlyOwner nonReentrant {
|
||||
// console2.log('placing orders');
|
||||
IFeeManager.FeeSchedule memory sched = feeManager.fees();
|
||||
(uint256 orderFee, uint256 gasFee) = placementFee(orders, sched);
|
||||
_takeFee(payable(msg.sender), orderFee, gasFee);
|
||||
uint64 startIndex = uint64(_ordersInfo.orders.length);
|
||||
OrderLib._placeOrders(_ordersInfo, orders, sched.fillFeeHalfBps, ocoMode, router);
|
||||
emit DexorderSwapPlaced(startIndex, uint8(orders.length), orderFee, gasFee);
|
||||
// console2.log('orders placed');
|
||||
}
|
||||
|
||||
function _takeFee( address payable sender, uint256 orderFee, uint256 gasFee ) internal {
|
||||
require( msg.value >= orderFee + gasFee, 'FEE');
|
||||
if (orderFee > 0)
|
||||
feeManager.orderFeeAccount().transfer(orderFee);
|
||||
if (gasFee > 0)
|
||||
feeManager.gasFeeAccount().transfer(gasFee);
|
||||
uint256 totalFee = orderFee + gasFee;
|
||||
if (totalFee < msg.value) {
|
||||
uint256 refund = msg.value - totalFee;
|
||||
// console2.log('refunding fee');
|
||||
// console2.log(refund);
|
||||
sender.transfer(refund);
|
||||
}
|
||||
}
|
||||
|
||||
function swapOrderStatus(uint64 orderIndex) external view returns (SwapOrderStatus memory status) {
|
||||
return _ordersInfo.orders[orderIndex];
|
||||
}
|
||||
|
||||
function execute(uint64 orderIndex, uint8 tranche_index, PriceProof memory proof) external nonReentrant
|
||||
{
|
||||
address payable t = payable(address(this));
|
||||
IVaultProxy proxy = IVaultProxy(t);
|
||||
address owner = proxy.owner();
|
||||
OrderLib.execute(_ordersInfo, owner, orderIndex, tranche_index, proof, router, feeManager);
|
||||
}
|
||||
|
||||
function cancelDexorder(uint64 orderIndex) external onlyOwner nonReentrant {
|
||||
OrderLib._cancelOrder(_ordersInfo,orderIndex);
|
||||
}
|
||||
|
||||
function cancelAllDexorders() external onlyOwner nonReentrant {
|
||||
OrderLib._cancelAll(_ordersInfo);
|
||||
}
|
||||
|
||||
function orderCanceled(uint64 orderIndex) external view returns (bool) {
|
||||
require( orderIndex < _ordersInfo.orders.length, 'OI' );
|
||||
return OrderLib._isCanceled(_ordersInfo, orderIndex);
|
||||
}
|
||||
|
||||
function vaultImplDidChange(address oldAddress) external {
|
||||
}
|
||||
|
||||
function wrapper() external view returns (address) { return address(weth9); }
|
||||
|
||||
function wrap(uint256 amount) external onlyOwner nonReentrant {
|
||||
require(address(weth9)!=address(0),'WU');
|
||||
weth9.deposit{value:amount}();
|
||||
}
|
||||
|
||||
function unwrap(uint256 amount) external onlyOwner nonReentrant {
|
||||
require(address(weth9)!=address(0),'WU');
|
||||
weth9.withdraw(amount);
|
||||
}
|
||||
|
||||
modifier onlyOwner() {
|
||||
require(msg.sender == _owner, "not owner");
|
||||
_;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
contract ArbitrumVaultImpl is VaultImpl {
|
||||
constructor(IRouter router_, IFeeManager feeManager_)
|
||||
VaultImpl(router_, feeManager_, address(UniswapV3Arbitrum.weth9))
|
||||
{}
|
||||
}
|
||||
64
src/interface/IFeeManager.sol
Normal file
64
src/interface/IFeeManager.sol
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
interface IFeeManager {
|
||||
|
||||
struct FeeSchedule {
|
||||
uint8 orderFee;
|
||||
uint8 orderExp;
|
||||
uint8 gasFee;
|
||||
uint8 gasExp;
|
||||
uint8 fillFeeHalfBps;
|
||||
}
|
||||
|
||||
// Emitted when a new VaultImpl is proposed by the upgrader account
|
||||
event FeesProposed(FeeSchedule indexed fees, uint32 indexed activationTime);
|
||||
|
||||
// Emitted when a new VaultImpl contract has fulfilled its waiting period and become the new default implementation
|
||||
event FeesChanged(FeeSchedule indexed fees);
|
||||
|
||||
// Emitted when a new fee limits are proposed by the upgrader account
|
||||
event FeeLimitsProposed(FeeSchedule indexed limits, uint32 indexed activationTime);
|
||||
|
||||
// Emitted when a new fee limit schedule has fulfilled its waiting period and become the new fee limits
|
||||
event FeeLimitsChanged(FeeSchedule indexed limits);
|
||||
|
||||
// Currently active fee schedule. Orders follow the FeeSchedule in effect at placement time.
|
||||
function fees() external view returns (FeeSchedule memory);
|
||||
|
||||
// The fee schedule cannot exceed these maximum values unless the upgrader account proposes a new set of limits
|
||||
// first, which is subject to the extended waiting period imposed by proposedLimitActivationTime()
|
||||
function limits() external view returns (FeeSchedule memory);
|
||||
|
||||
function proposedFees() external view returns (FeeSchedule memory);
|
||||
function proposedFeeActivationTime() external view returns (uint32);
|
||||
function proposedLimits() external view returns (FeeSchedule memory);
|
||||
function proposedLimitActivationTime() external view returns (uint32);
|
||||
|
||||
|
||||
//
|
||||
// Accounts
|
||||
//
|
||||
|
||||
// The admin account may change the fees, limits, and fee account addresses.
|
||||
function admin() external view returns (address);
|
||||
|
||||
// The three fee types are each sent to a separate address for accounting.
|
||||
function orderFeeAccount() external view returns (address payable);
|
||||
function gasFeeAccount() external view returns (address payable);
|
||||
function fillFeeAccount() external view returns (address payable);
|
||||
|
||||
// The admin may change the fees within the limits after only a short delay
|
||||
function setFees(FeeSchedule calldata sched) external;
|
||||
|
||||
// The admin may change the fee limits themselves after a long delay
|
||||
function setLimits(FeeSchedule calldata sched) external;
|
||||
|
||||
// The admin may adjust the destination of fees at any time
|
||||
function setFeeAccounts(
|
||||
address payable fillFeeAccount,
|
||||
address payable orderFeeAccount,
|
||||
address payable gasFeeAccount
|
||||
) external;
|
||||
|
||||
}
|
||||
30
src/interface/IRouter.sol
Normal file
30
src/interface/IRouter.sol
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import "../core/OrderSpec.sol";
|
||||
|
||||
interface IRouter {
|
||||
|
||||
// Returns the current price of the pool for comparison with limit lines.
|
||||
function rawPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee) external view
|
||||
returns (uint256);
|
||||
|
||||
// Returns the oracle price, with protections against fast moving price changes (typically used in comparisons to slippage price)
|
||||
function protectedPrice(Exchange exchange, address tokenIn, address tokenOut, uint24 maxFee) external view
|
||||
returns (uint256);
|
||||
|
||||
struct SwapParams {
|
||||
address tokenIn;
|
||||
address tokenOut;
|
||||
address recipient;
|
||||
uint256 amount;
|
||||
uint256 minAmount;
|
||||
bool amountIsInput;
|
||||
uint256 limitPriceX96;
|
||||
uint24 maxFee;
|
||||
}
|
||||
|
||||
function swap( Exchange exchange, SwapParams memory params ) external
|
||||
returns (uint256 amountIn, uint256 amountOut);
|
||||
|
||||
}
|
||||
75
src/interface/IVault.sol
Normal file
75
src/interface/IVault.sol
Normal file
@@ -0,0 +1,75 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "../core/OrderSpec.sol";
|
||||
import {IFeeManager} from "./IFeeManager.sol";
|
||||
|
||||
|
||||
interface IVaultImpl {
|
||||
|
||||
function version() external view returns (uint256);
|
||||
|
||||
function feeManager() external view returns (IFeeManager);
|
||||
function placementFee(SwapOrder memory order, IFeeManager.FeeSchedule memory sched) external view returns (uint256 orderFee, uint256 gasFee);
|
||||
function placementFee(SwapOrder[] memory orders, IFeeManager.FeeSchedule memory sched) external view returns (uint256 orderFee, uint256 gasFee);
|
||||
|
||||
function placeDexorder(SwapOrder memory order) external payable;
|
||||
function placeDexorders(SwapOrder[] memory orders, OcoMode ocoMode) external payable;
|
||||
|
||||
function numSwapOrders() external view returns (uint64 num);
|
||||
function swapOrderStatus(uint64 orderIndex) external view returns (SwapOrderStatus memory status);
|
||||
|
||||
function execute(uint64 orderIndex, uint8 tranche_index, PriceProof memory proof) external;
|
||||
|
||||
function cancelDexorder(uint64 orderIndex) external;
|
||||
function cancelAllDexorders() external;
|
||||
function orderCanceled(uint64 orderIndex) external view returns (bool);
|
||||
|
||||
function vaultImplDidChange(address oldImpl) external; //REV: Changed this name to avoid similarity with the event name
|
||||
|
||||
function wrapper() external view returns (address);
|
||||
function wrap(uint256 amount) external;
|
||||
function unwrap(uint256 amount) external;
|
||||
|
||||
}
|
||||
|
||||
|
||||
interface IVaultProxy {
|
||||
|
||||
event VaultCreated(address indexed owner, uint8 num);
|
||||
event Killed();
|
||||
|
||||
// Deposit and Withdrawal are for native coin transfers. ERC20 tokens emit Transfer events on their own.
|
||||
event Deposit(address indexed sender, uint256 amount);
|
||||
event Withdrawal(address indexed receiver, uint256 amount);
|
||||
|
||||
// Implementation upgrade events
|
||||
event VaultImplProposed(address impl, uint32 activationTime);
|
||||
event VaultImplChanged(address impl);
|
||||
|
||||
function factory() external view returns (address);
|
||||
|
||||
function kill() external;
|
||||
function killed() external view returns (bool);
|
||||
|
||||
function owner() external view returns (address);
|
||||
|
||||
function num() external view returns (uint8);
|
||||
|
||||
function implementation() external view returns (address);
|
||||
|
||||
function upgrade(address impl) external;
|
||||
|
||||
receive() external payable;
|
||||
|
||||
function withdraw(uint256 amount) external;
|
||||
|
||||
function withdrawTo(address payable recipient, uint256 amount) external;
|
||||
|
||||
function withdraw(IERC20 token, uint256 amount) external;
|
||||
|
||||
function withdrawTo(IERC20 token, address recipient, uint256 amount) external;
|
||||
}
|
||||
|
||||
interface IVault is IVaultProxy, IVaultImpl {}
|
||||
25
src/interface/IVaultFactory.sol
Normal file
25
src/interface/IVaultFactory.sol
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import {IVault} from "./IVault.sol";
|
||||
|
||||
|
||||
interface IVaultFactory {
|
||||
function killed() external view returns (bool);
|
||||
|
||||
// Only vault number 0 is currently supported by the backend.
|
||||
function deployVault() external returns (IVault vault);
|
||||
function deployVault(uint8 num) external returns (IVault vault);
|
||||
function deployVault(address owner) external returns (IVault vault);
|
||||
function deployVault(address owner, uint8 num) external returns (IVault vault);
|
||||
|
||||
function implementation() external view returns (address); // current implementation of the vault methods
|
||||
function upgrader() external view returns (address);
|
||||
function proposedImplActivationTimestamp() external view returns (uint32);
|
||||
function proposedImpl() external view returns (address);
|
||||
function upgradeImplementation( address newImpl ) external; // used by the admin to propose an upgrade
|
||||
|
||||
// used by the vault constructor to get arguments
|
||||
function parameters() external view returns (address owner, uint8 num, address vaultImpl);
|
||||
|
||||
}
|
||||
72
src/more/Dexorder.sol
Normal file
72
src/more/Dexorder.sol
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
import "@forge-std/console2.sol";
|
||||
import "../core/OrderSpec.sol";
|
||||
import {IVault} from "../interface/IVault.sol";
|
||||
|
||||
// represents the Dexorder organization
|
||||
|
||||
contract Dexorder {
|
||||
|
||||
address public immutable admin;
|
||||
|
||||
modifier onlyAdmin() {
|
||||
require(msg.sender == admin, "not admin");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor () {
|
||||
admin = address(0);
|
||||
}
|
||||
|
||||
receive() external payable {} // for testing purposes
|
||||
|
||||
|
||||
// Execution batching
|
||||
|
||||
event DexorderExecutions(bytes16 indexed id, string[] errors);
|
||||
|
||||
struct ExecutionRequest {
|
||||
address payable vault;
|
||||
uint64 orderIndex;
|
||||
uint8 trancheIndex;
|
||||
PriceProof proof;
|
||||
}
|
||||
|
||||
|
||||
function execute( bytes16 id, ExecutionRequest memory req ) public returns (string memory error) {
|
||||
// console2.log('Dexorder execute() single');
|
||||
error = _execute(req);
|
||||
string[] memory errors = new string[](1);
|
||||
errors[0] = error;
|
||||
emit DexorderExecutions(id, errors);
|
||||
// console2.log('Dexorder execute() single completed');
|
||||
}
|
||||
|
||||
|
||||
function execute( bytes16 id, ExecutionRequest[] memory reqs ) public returns (string[] memory errors) {
|
||||
// console2.log('Dexorder execute() multi');
|
||||
// console2.log(reqs.length);
|
||||
errors = new string[](reqs.length);
|
||||
for( uint8 i=0; i<reqs.length; i++ )
|
||||
errors[i] = _execute(reqs[i]);
|
||||
emit DexorderExecutions(id, errors);
|
||||
}
|
||||
|
||||
|
||||
function _execute( ExecutionRequest memory req ) private returns (string memory error) {
|
||||
// console2.log('Dexorder _execute()');
|
||||
// single tranche execution
|
||||
try IVault(req.vault).execute(req.orderIndex, req.trancheIndex, req.proof) {
|
||||
error = '';
|
||||
// console2.log('execution successful');
|
||||
}
|
||||
catch Error(string memory reason) {
|
||||
if( bytes(reason).length == 0 )
|
||||
reason = 'UNK';
|
||||
// console2.log('execute error code');
|
||||
// console2.log(reason);
|
||||
error = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/more/FeeManagerLib.sol
Normal file
153
src/more/FeeManagerLib.sol
Normal file
@@ -0,0 +1,153 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import {IFeeManager,FeeManager} from "../core/FeeManager.sol";
|
||||
|
||||
library FeeManagerLib {
|
||||
|
||||
function defaultFeeManager() internal returns (FeeManager) {
|
||||
address payable a = payable(msg.sender);
|
||||
return defaultFeeManager(a);
|
||||
}
|
||||
|
||||
|
||||
function defaultFeeManager(address payable owner) internal returns (FeeManager) {
|
||||
return FeeManagerLib.defaultFeeManager(owner, owner, owner, owner);
|
||||
}
|
||||
|
||||
|
||||
function defaultFeeManager(
|
||||
address admin,
|
||||
address payable orderFeeAccount,
|
||||
address payable gasFeeAccount,
|
||||
address payable fillFeeAccount
|
||||
) internal
|
||||
returns (FeeManager) {
|
||||
uint32 limitChangeNoticeDuration = 7 * 24 * 60 * 60; // 7 days
|
||||
uint32 feeChangeNoticeDuration = 1 * 60 * 60; // 1 hour
|
||||
uint8 maxIncreaseOrderFeePct = 10; // 10% per hour (within the limits)
|
||||
uint8 maxIncreaseTrancheFeePct = 100; // 100% per hour (within the limits) gas prices can change quickly
|
||||
|
||||
uint8 orderFee = 0;
|
||||
uint8 orderExp = 0;
|
||||
// about 2.5¢ at $4000 ETH
|
||||
uint8 gasFee = 181;
|
||||
uint8 gasExp = 35;
|
||||
uint8 fillFeeHalfBps = 30; // 15 bps fill fee
|
||||
|
||||
IFeeManager.FeeSchedule memory fees = IFeeManager.FeeSchedule(
|
||||
orderFee, orderExp,
|
||||
gasFee, gasExp,
|
||||
fillFeeHalfBps
|
||||
);
|
||||
IFeeManager.FeeSchedule memory limits = IFeeManager.FeeSchedule(
|
||||
orderFee, orderExp,
|
||||
gasFee, gasExp,
|
||||
fillFeeHalfBps
|
||||
);
|
||||
FeeManager.ConstructorArgs memory args = FeeManager.ConstructorArgs(
|
||||
limitChangeNoticeDuration, feeChangeNoticeDuration, maxIncreaseOrderFeePct, maxIncreaseTrancheFeePct,
|
||||
fees, limits, admin, orderFeeAccount, gasFeeAccount, fillFeeAccount
|
||||
);
|
||||
return new FeeManager(args);
|
||||
}
|
||||
|
||||
|
||||
function freeFeeManager() internal returns (FeeManager) {
|
||||
address payable a = payable(msg.sender);
|
||||
return freeFeeManager(a);
|
||||
}
|
||||
|
||||
|
||||
function freeFeeManager(address payable owner) internal returns (FeeManager) {
|
||||
return FeeManagerLib.freeFeeManager(owner, owner, owner, owner);
|
||||
}
|
||||
|
||||
|
||||
function freeFeeManager(
|
||||
address admin,
|
||||
address payable orderFeeAccount,
|
||||
address payable gasFeeAccount,
|
||||
address payable fillFeeAccount
|
||||
) internal
|
||||
returns (FeeManager) {
|
||||
uint32 limitChangeNoticeDuration = 5 * 60 * 60; // LIMIT_CHANGE_NOTICE_DURATION 5 minutes
|
||||
uint32 feeChangeNoticeDuration = 2 * 60 * 60; // FEE_CHANGE_NOTICE_DURATION 2 minutes
|
||||
uint8 maxIncreaseOrderFeePct = 10; // 10% per hour (within the limits)
|
||||
uint8 maxIncreaseTrancheFeePct = 100; // 100% per hour (within the limits) gas prices can change quickly
|
||||
|
||||
uint8 orderFee = 0;
|
||||
uint8 orderExp = 0;
|
||||
uint8 gasFee = 0;
|
||||
uint8 gasExp = 0;
|
||||
uint8 fillFeeHalfBps = 0;
|
||||
|
||||
IFeeManager.FeeSchedule memory fees = IFeeManager.FeeSchedule(
|
||||
orderFee, orderExp,
|
||||
gasFee, gasExp,
|
||||
fillFeeHalfBps
|
||||
);
|
||||
IFeeManager.FeeSchedule memory limits = IFeeManager.FeeSchedule(
|
||||
orderFee, orderExp,
|
||||
gasFee, gasExp,
|
||||
fillFeeHalfBps
|
||||
);
|
||||
FeeManager.ConstructorArgs memory args = FeeManager.ConstructorArgs(
|
||||
limitChangeNoticeDuration, feeChangeNoticeDuration, maxIncreaseOrderFeePct, maxIncreaseTrancheFeePct,
|
||||
fees, limits, admin, orderFeeAccount, gasFeeAccount, fillFeeAccount
|
||||
);
|
||||
return new FeeManager(args);
|
||||
}
|
||||
|
||||
|
||||
function debugFeeManager() internal returns (FeeManager) {
|
||||
return debugFeeManager(payable(msg.sender));
|
||||
}
|
||||
|
||||
|
||||
function debugFeeManager(address payable owner) internal returns (FeeManager) {
|
||||
return FeeManagerLib.debugFeeManager(owner, owner, owner, owner);
|
||||
}
|
||||
|
||||
|
||||
function debugFeeManager(
|
||||
address admin,
|
||||
address payable orderFeeAccount,
|
||||
address payable gasFeeAccount,
|
||||
address payable fillFeeAccount
|
||||
) internal
|
||||
returns (FeeManager) {
|
||||
uint32 limitChangeNoticeDuration = 5 * 60 * 60; // LIMIT_CHANGE_NOTICE_DURATION 5 minutes
|
||||
uint32 feeChangeNoticeDuration = 2 * 60 * 60; // FEE_CHANGE_NOTICE_DURATION 2 minutes
|
||||
uint8 maxIncreaseOrderFeePct = 10; // 10% per hour (within the limits)
|
||||
uint8 maxIncreaseTrancheFeePct = 100; // 100% per hour (within the limits) gas prices can change quickly
|
||||
|
||||
// todo limits
|
||||
// about $1 at $4000 ETH
|
||||
uint8 orderFee = 227;
|
||||
uint8 orderExp = 40;
|
||||
// about 5¢ at $4000 ETH
|
||||
uint8 gasFee = 181;
|
||||
uint8 gasExp = 36;
|
||||
uint8 fillFeeHalfBps = 30; // 15 bps fill fee
|
||||
|
||||
IFeeManager.FeeSchedule memory fees = IFeeManager.FeeSchedule(
|
||||
orderFee, orderExp,
|
||||
gasFee, gasExp,
|
||||
fillFeeHalfBps
|
||||
);
|
||||
IFeeManager.FeeSchedule memory limits = IFeeManager.FeeSchedule(
|
||||
orderFee, orderExp,
|
||||
gasFee, gasExp,
|
||||
fillFeeHalfBps
|
||||
);
|
||||
FeeManager.ConstructorArgs memory args = FeeManager.ConstructorArgs(
|
||||
limitChangeNoticeDuration, feeChangeNoticeDuration, maxIncreaseOrderFeePct, maxIncreaseTrancheFeePct,
|
||||
fees, limits, admin, orderFeeAccount, gasFeeAccount, fillFeeAccount
|
||||
);
|
||||
return new FeeManager(args);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
101
src/more/MockERC20.sol
Normal file
101
src/more/MockERC20.sol
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@forge-std/console2.sol";
|
||||
import {IERC20Metadata} from "../../lib_uniswap/v3-periphery/contracts/interfaces/IERC20Metadata.sol";
|
||||
|
||||
|
||||
contract MockERC20 is IERC20Metadata {
|
||||
|
||||
// This token allows anyone to mint as much as they desire
|
||||
|
||||
string public name;
|
||||
string public symbol;
|
||||
uint8 public decimals;
|
||||
uint256 public totalSupply;
|
||||
|
||||
mapping(address=>uint256) private _balances;
|
||||
mapping(address=>mapping(address=>uint256)) private _allowances;
|
||||
|
||||
constructor(string memory name_, string memory symbol_, uint8 decimals_)
|
||||
{
|
||||
// console2.log('MockERC20 constructor');
|
||||
name = name_;
|
||||
symbol = symbol_;
|
||||
decimals = decimals_;
|
||||
totalSupply = 0;
|
||||
}
|
||||
|
||||
function mint(address account, uint256 amount) public {
|
||||
// console2.log('MockERC20 mint');
|
||||
_balances[account] += amount;
|
||||
emit Transfer(address(this),account,amount);
|
||||
}
|
||||
|
||||
function burn(uint256 amount) public {
|
||||
// console2.log('MockERC20 burn');
|
||||
require(_balances[msg.sender] >= amount);
|
||||
_balances[msg.sender] -= amount;
|
||||
emit Transfer(msg.sender,address(this),amount);
|
||||
}
|
||||
|
||||
function balanceOf(address account) public view returns (uint256) {
|
||||
// console2.log('MockERC20 balance');
|
||||
return _balances[account];
|
||||
}
|
||||
|
||||
function transfer(address to, uint256 value) public returns (bool) {
|
||||
// console2.log('transfer');
|
||||
// console2.log(msg.sender);
|
||||
// console2.log(to);
|
||||
// console2.log(value);
|
||||
return _transferFrom(msg.sender, to, value);
|
||||
}
|
||||
|
||||
function allowance(address owner, address spender) public view returns (uint256) {
|
||||
// console2.log('MockERC20 allowance');
|
||||
return _allowances[owner][spender];
|
||||
}
|
||||
|
||||
function approve(address spender, uint256 value) public returns (bool) {
|
||||
// console2.log('approve');
|
||||
// console2.log(msg.sender);
|
||||
// console2.log(spender);
|
||||
// console2.log(value);
|
||||
_allowances[msg.sender][spender] = value;
|
||||
emit Approval(msg.sender, spender, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
function transferFrom(address from, address to, uint256 value) public returns (bool) {
|
||||
// console2.log('transferFrom');
|
||||
// console2.log(msg.sender);
|
||||
// console2.log(from);
|
||||
// console2.log(to);
|
||||
// console2.log(value);
|
||||
if( msg.sender != from ) {
|
||||
// console2.log('allowance');
|
||||
// console2.log(_allowances[from][msg.sender]);
|
||||
require(value <= _allowances[from][msg.sender], 'Insufficient allowance');
|
||||
if( _allowances[from][msg.sender] != type(uint256).max )
|
||||
_allowances[from][msg.sender] -= value;
|
||||
}
|
||||
return _transferFrom(from, to, value);
|
||||
}
|
||||
|
||||
function _transferFrom(address from, address to, uint256 value) private returns (bool) {
|
||||
// console2.log('_transferFrom');
|
||||
// console2.log(from);
|
||||
// console2.log(to);
|
||||
// console2.log(value);
|
||||
// console2.log(_balances[from]);
|
||||
require(_balances[from] >= value, 'Insufficient balance');
|
||||
_balances[from] -= value;
|
||||
_balances[to] += value;
|
||||
emit Transfer(from,to,value);
|
||||
// console2.log('raw transfer completed');
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
109
src/more/QueryHelper.sol
Normal file
109
src/more/QueryHelper.sol
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import "@forge-std/console2.sol";
|
||||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "../core/OrderSpec.sol";
|
||||
import {IVault} from "../interface/IVault.sol";
|
||||
import "@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol";
|
||||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
||||
import "../core/UniswapV3.sol";
|
||||
import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
||||
|
||||
|
||||
contract QueryHelper {
|
||||
uint8 constant public version = 1;
|
||||
uint8 constant public UNKNOWN_DECIMALS = type(uint8).max;
|
||||
|
||||
IUniswapV3Factory public immutable factory;
|
||||
|
||||
constructor( IUniswapV3Factory factory_ ) {
|
||||
factory = factory_;
|
||||
}
|
||||
|
||||
function getBalances( address vault, address[] memory tokens ) public view
|
||||
returns (
|
||||
uint256[] memory balances,
|
||||
uint256[] memory decimals
|
||||
) {
|
||||
require(tokens.length < type(uint16).max);
|
||||
balances = new uint256[](tokens.length);
|
||||
decimals = new uint256[](tokens.length);
|
||||
for( uint16 i=0; i < tokens.length; i++ ) {
|
||||
try IERC20(tokens[i]).balanceOf(vault) returns (uint256 balance) {
|
||||
balances[i] = balance;
|
||||
}
|
||||
catch {
|
||||
balances[i] = 0;
|
||||
}
|
||||
try ERC20(tokens[i]).decimals() returns (uint8 dec) {
|
||||
decimals[i] = dec;
|
||||
}
|
||||
catch {
|
||||
decimals[i] = UNKNOWN_DECIMALS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RoutesResult {
|
||||
Exchange exchange;
|
||||
uint24 fee;
|
||||
address pool;
|
||||
}
|
||||
|
||||
function getRoutes( address tokenA, address tokenB ) public view
|
||||
returns(RoutesResult[] memory routes) {
|
||||
// todo discover all supported pools
|
||||
// console2.log('getRoutes');
|
||||
// console2.log(tokenA);
|
||||
// console2.log(tokenB);
|
||||
// here we find the highest liquidity pool for v2 and for v3
|
||||
uint24[4] memory fees = [uint24(100),500,3000,10000];
|
||||
uint24 uniswapV2Fee = 0;
|
||||
// uint128 uniswapV2Liquidity = 0;
|
||||
// address uniswapV2Pool = address(0);
|
||||
uint24 uniswapV3Fee = 0;
|
||||
uint256 uniswapV3Liquidity = 0;
|
||||
address uniswapV3Pool = address(0);
|
||||
IERC20 ercA = IERC20(tokenA);
|
||||
for( uint8 f=0; f<4; f++ ) {
|
||||
// console2.log('getPool...');
|
||||
uint24 fee = fees[f];
|
||||
IUniswapV3Pool pool = IUniswapV3Pool(PoolAddress.computeAddress(
|
||||
address(UniswapV3Arbitrum.factory), PoolAddress.PoolKey(tokenA, tokenB, fee)));
|
||||
if( address(pool) == address(0) ) {
|
||||
// console2.log('no pool');
|
||||
continue;
|
||||
}
|
||||
// console2.log('got pool');
|
||||
// console2.log(address(pool));
|
||||
// NOTE: pool.liquidity() is only the current tick's liquidity, so we look at the pool's balance
|
||||
// of one of the tokens as a measure of liquidity
|
||||
uint256 liquidity = ercA.balanceOf(address(pool));
|
||||
// console2.log(liquidity);
|
||||
if( liquidity > uniswapV3Liquidity ) {
|
||||
uniswapV3Fee = fee;
|
||||
uniswapV3Liquidity = liquidity;
|
||||
uniswapV3Pool = address(pool);
|
||||
}
|
||||
}
|
||||
uint8 routesCount = uniswapV3Fee > 0 ? 1 : 0 + uniswapV2Fee > 0 ? 1 : 0;
|
||||
// console2.log(uniswapV3Pool);
|
||||
// console2.log(uint(routesCount));
|
||||
routes = new QueryHelper.RoutesResult[](routesCount);
|
||||
uint8 i = 0;
|
||||
// todo v2
|
||||
if( uniswapV3Fee > 0 )
|
||||
routes[i++] = QueryHelper.RoutesResult(Exchange.UniswapV3, uniswapV3Fee, uniswapV3Pool);
|
||||
}
|
||||
|
||||
function poolStatus(IUniswapV3Pool pool) public view
|
||||
returns (
|
||||
int24 tick,
|
||||
uint128 liquidity
|
||||
) {
|
||||
(, tick,,,,,) = pool.slot0();
|
||||
liquidity = pool.liquidity();
|
||||
}
|
||||
}
|
||||
34
src/more/VaultAddress.sol
Normal file
34
src/more/VaultAddress.sol
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
pragma solidity 0.8.26;
|
||||
|
||||
import "@forge-std/console2.sol";
|
||||
|
||||
|
||||
library VaultAddress {
|
||||
// keccak-256 hash of the Vault's bytecode (not the deployed bytecode but the initialization bytecode)
|
||||
bytes32 public constant VAULT_INIT_CODE_HASH = 0xada4cd6b4cbd10f5eb48a27bde34a08fc1427562cff58ab798def9b1883d3f8a;
|
||||
|
||||
// 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) {
|
||||
return computeAddress(factory, owner, 0);
|
||||
}
|
||||
|
||||
function computeAddress(address factory, address owner, uint8 num) internal pure returns (address vault) {
|
||||
bytes32 salt = keccak256(abi.encodePacked(owner,num));
|
||||
vault = address(uint160(
|
||||
uint256(
|
||||
keccak256(
|
||||
abi.encodePacked(
|
||||
hex'ff',
|
||||
factory,
|
||||
salt,
|
||||
VAULT_INIT_CODE_HASH
|
||||
)
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user