This commit is contained in:
dexorder
2024-10-17 02:42:28 -04:00
commit 25def69c66
878 changed files with 112489 additions and 0 deletions

200
src/core/FeeManager.sol Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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))
{}
}

View 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
View 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
View 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 {}

View 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
View 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
View 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
View 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
View 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
View 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
)
)
)
));
}
}