Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: discrete swap fee accounting #8

Merged
merged 10 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 31 additions & 17 deletions docs/1-technical-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ pub trait ISolverHooks<TContractState> {
// # Returns
// * `amount_in` - amount in
// * `amount_out` - amount out
fn quote(self: @TContractState, market_id: felt252, swap_params: SwapParams,) -> (u256, u256);
// * `fees` - fees
fn quote(self: @TContractState, market_id: felt252, swap_params: SwapParams,) -> SwapAmounts;

// Get the initial token supply to mint when first depositing to a market.
//
Expand All @@ -63,11 +64,11 @@ pub trait ISolverHooks<TContractState> {

### `ReplicatingSolver`

The Replicating Solver is a simple Solver that replicates an oracle price feed, plus a spread, to generate bid / ask quotes for a given token pair.
The Replicating Solver is a simple Solver that replicates an oracle price feed, plus a dynamic spread, to generate bid / ask quotes for a given token pair.

It allows setting a number of configurable parameters for each solver market, including:

1. `min_spread`: a fixed spread added to the oracle price to generate the bid and ask quote
1. `fee_rate`: swap fee rate applied to swaps (base 10000)
2. `range` : the range of the virtual liquidity position (denominated in number of limits or ticks) used to construct the swap quote, based on Uniswap liquidity formulae
3. `max_delta` : inventory delta, or the single-sided spread applied to an imbalanced portfolio, with the aim of incentivising swappers to rebalance the solver market back to a 50/50 ratio
4. `max_skew` : the maximum allowable skew of base / quote reserves in the solver market, beyond which the solver will not allow swaps unless they improve skew
Expand Down Expand Up @@ -132,10 +133,12 @@ Liquidity providers can deposit to a solver market by calling `deposit()` (or `d
// # Returns
// * `base_deposit` - base asset deposited
// * `quote_deposit` - quote asset deposited
// * `base_fees` - base asset fees
// * `quote_fees` - quote asset fees
// * `shares` - pool shares minted in the form of liquidity
fn deposit_initial(
ref self: TContractState, market_id: felt252, base_amount: u256, quote_amount: u256
) -> (u256, u256, u256);
) -> AmountsWithShares;

// Deposit liquidity to market.
//
Expand All @@ -147,10 +150,12 @@ fn deposit_initial(
// # Returns
// * `base_deposit` - base asset deposited
// * `quote_deposit` - quote asset deposited
// * `base_fees` - base asset fees
// * `quote_fees` - quote asset fees
// * `shares` - pool shares minted
fn deposit(
ref self: TContractState, market_id: felt252, base_amount: u256, quote_amount: u256
) -> (u256, u256, u256);
) -> AmountsWithShares;
```

Withdrawals can be made either by calling `withdraw_public()` to withdraw from a public vault, or `withdraw_private()` to withdraw an arbitrary amounts from a private vault (available to the vault owner only).
Expand All @@ -166,9 +171,11 @@ Withdrawals can be made either by calling `withdraw_public()` to withdraw from a
// # Returns
// * `base_amount` - base asset withdrawn
// * `quote_amount` - quote asset withdrawn
// * `base_fees` - base asset fees
// * `quote_fees` - quote asset fees
fn withdraw_public(
ref self: TContractState, market_id: felt252, shares: u256
) -> (u256, u256);
) -> Amounts;

// Withdraw exact token amounts from market.
// Called for private vaults. For public vaults, use `withdraw_public`.
Expand All @@ -181,9 +188,11 @@ fn withdraw_public(
// # Returns
// * `base_amount` - base asset withdrawn
// * `quote_amount` - quote asset withdrawn
// * `base_fees` - base asset fees
// * `quote_fees` - quote asset fees
fn withdraw_private(
ref self: TContractState, market_id: felt252, base_amount: u256, quote_amount: u256
) -> (u256, u256);
) -> Amounts;
```

### Swapping and quoting
Expand All @@ -202,18 +211,20 @@ The `quote()` function is part of the `SolverHooks` interface and should be impl
// # Returns
// * `amount_in` - amount in
// * `amount_out` - amount out
fn quote(self: @TContractState, market_id: felt252, swap_params: SwapParams,) -> (u256, u256);
// * `fees` - fees
fn quote(self: @TContractState, market_id: felt252, swap_params: SwapParams,) -> SwapAmounts;

// Swap through a market.
//
// # Arguments
// * `market_id` - market id
// * `swap_params` - swap parameters
//
// # Returns
// * `amount_in` - amount in
// * `amount_out` - amount out
fn swap(ref self: TContractState, market_id: felt252, swap_params: SwapParams,) -> (u256, u256);
//
// # Arguments
// * `market_id` - market id
// * `swap_params` - swap parameters
//
// # Returns
// * `amount_in` - amount in
// * `amount_out` - amount out
// * `fees` - fees
fn swap(ref self: TContractState, market_id: felt252, swap_params: SwapParams,) -> SwapAmounts;

// Information about a swap.
//
Expand All @@ -240,6 +251,9 @@ Solvers are deployed with a contract `owner` that has permission to set and coll
// # Arguments
// * `receiver` - address to receive fees
// * `token` - token to collect fees for
//
// # Returns
// * `amount` - amount of fees collected
fn collect_withdraw_fees(
ref self: TContractState, receiver: ContractAddress, token: ContractAddress
) -> u256;
Expand Down
2 changes: 1 addition & 1 deletion docs/2-solver-implementations.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ It is designed as a singleton contract supporting multiple solver markets, each
The solver market configs are as follows:

1. Owner: address that controls market configurations, pausing, and ownership transfers
2. Min spread: spread applied to the oracle price to calculate the bid and ask prices
2. Swap fee: swap fee applied to amounts swapped into the market
3. Range: the range of the virtual liquidity position, which affects the execution slippage of the swap
4. Max delta: the delta (or offset) applied to bid and ask prices to correct for inventory skew
5. Max skew: the maximum portfolio skew of the market, above which swaps will be rejected
Expand Down
24 changes: 22 additions & 2 deletions docs/3-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub struct Swap {
pub exact_input: bool,
pub amount_in: u256,
pub amount_out: u256,
pub fees: u256,
}
```

Expand All @@ -52,6 +53,7 @@ pub struct Swap {
- `exact_input` is a boolean indicating if the swap amount was specified as input or output
- `amount_in` is the amount of the input token swapped in
- `amount_out` is the amount of the output token swapped out
- `fees` is the amount of fees (denominated in the input token) paid for the swap

### `Deposit` / `Withdraw`

Expand All @@ -65,6 +67,20 @@ pub struct Deposit {
pub market_id: felt252,
pub base_amount: u256,
pub quote_amount: u256,
pub base_fees: u256,
pub quote_fees: u256,
pub shares: u256,
}

pub struct Withdraw {
#[key]
pub caller: ContractAddress,
#[key]
pub market_id: felt252,
pub base_amount: u256,
pub quote_amount: u256,
pub base_fees: u256,
pub quote_fees: u256,
pub shares: u256,
}
```
Expand All @@ -73,8 +89,12 @@ pub struct Deposit {
- `market_id` is the unique id of the market (see `CreateMarket` above)
- `base_amount` is the amount of base tokens deposited
- `quote_amount` is the amount of quote tokens deposited
- `base_fees` is the amount fees collected in base tokens
- `quote_fees` is the amount of fees collected in quote tokens
- `shares` is the amount of LP shares minted or burned

Any deposit or withdraw action triggers a fee withdrawal, which is why these events emits collected fees.

### `Pause` / `Unpause`

These events are emitted when a market is paused and unpaused. Paused markets should not accrue rewards as they will reject incoming swaps. These events can be indexed to track the paused state of solver markets.
Expand Down Expand Up @@ -109,7 +129,7 @@ This event is emitted when a solver market's parameters are updated by its owner
pub(crate) struct SetMarketParams {
#[key]
pub market_id: felt252,
pub min_spread: u32,
pub fee_rate: u16,
pub range: u32,
pub max_delta: u32,
pub max_skew: u16,
Expand All @@ -121,7 +141,7 @@ pub(crate) struct SetMarketParams {
```

- `market_id` is the unique id of the market (see `CreateMarket` above)
- `min_spread` is the spread, denominated in limits (1.00001 or 0.001% tick) added to the oracle price to arrive at the bid upper or ask lower price
- `fee_rate` is the swap fee rate deducted from swap amounts paid in (expressed in base 10000)
- `range` is the range, denominated in limits, of the virtual liquidity position that the swap is executed over (we apply the same calculations as Uniswap liquidity positions). The bid lower price is calculated by as `bid_upper - range`, and the ask upper price is calculated as `ask_lower + range`
- `max_delta` is a dynamic shift applied to the bid and ask prices in the event of a skew in the composition of the pool (e.g. if the pool is 90% ETH and 10% DAI, the price of ETH will be shifted by `skew * max_delta` to incentivise swappers to move the pool back to 50/50 ratio)
- `max_skew` is a hard cap applied to the skew of the pool, above which swaps are rejected
Expand Down
5 changes: 2 additions & 3 deletions models/src/libraries/SpreadMath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export const getVirtualPosition = (

export const getVirtualPositionRange = (
isBid: boolean,
minSpread: Decimal.Value,
delta: Decimal.Value,
range: Decimal.Value,
oraclePrice: Decimal.Value,
Expand All @@ -58,11 +57,11 @@ export const getVirtualPositionRange = (
let limit = priceToLimit(scaledOraclePrice, 1, !isBid);

if (isBid) {
const upperLimit = new Decimal(limit).sub(minSpread).add(delta);
const upperLimit = new Decimal(limit).add(delta);
const lowerLimit = upperLimit.sub(range);
return { lowerLimit, upperLimit };
} else {
const lowerLimit = new Decimal(limit).add(minSpread).add(delta);
const lowerLimit = new Decimal(limit).add(delta);
const upperLimit = lowerLimit.add(range);
return { lowerLimit, upperLimit };
}
Expand Down
91 changes: 62 additions & 29 deletions models/src/libraries/SwapLib.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
import Decimal from "decimal.js";
import { PRECISION, ROUNDING } from "../config";
import { liquidityToBase, liquidityToQuote } from "../math/liquidityMath";
import { grossToNet, netToFee } from "../math/feeMath";

export const getSwapAmounts = (
isBuy: boolean,
exactInput: boolean,
amount: Decimal.Value,
thresholdSqrtPrice: Decimal.Value | null,
thresholdAmount: Decimal.Value | null,
lowerSqrtPrice: Decimal.Value,
upperSqrtPrice: Decimal.Value,
liquidity: Decimal.Value,
baseDecimals: number,
quoteDecimals: number
): { amountIn: Decimal.Value; amountOut: Decimal.Value } => {
export const getSwapAmounts = ({
isBuy,
exactInput,
amount,
swapFeeRate,
thresholdSqrtPrice,
thresholdAmount,
lowerSqrtPrice,
upperSqrtPrice,
liquidity,
baseDecimals,
quoteDecimals,
}: {
isBuy: boolean;
exactInput: boolean;
amount: Decimal.Value;
swapFeeRate: Decimal.Value;
thresholdSqrtPrice: Decimal.Value | null;
thresholdAmount: Decimal.Value | null;
lowerSqrtPrice: Decimal.Value;
upperSqrtPrice: Decimal.Value;
liquidity: Decimal.Value;
baseDecimals: number;
quoteDecimals: number;
}): {
amountIn: Decimal.Value;
amountOut: Decimal.Value;
fees: Decimal.Value;
} => {
const scaledLowerSqrtPrice = new Decimal(lowerSqrtPrice).mul(
new Decimal(10).pow((baseDecimals - quoteDecimals) / 2)
);
Expand All @@ -30,51 +48,64 @@ export const getSwapAmounts = (
? Decimal.max(thresholdSqrtPrice, scaledLowerSqrtPrice)
: scaledLowerSqrtPrice;

const { amountIn, amountOut, nextSqrtPrice } = computeSwapAmount(
startSqrtPrice,
const { amountIn, amountOut, fees, nextSqrtPrice } = computeSwapAmount({
currSqrtPrice: startSqrtPrice,
targetSqrtPrice,
liquidity,
amount,
exactInput
);
amountRem: amount,
swapFeeRate,
exactInput,
});

const grossAmountIn = new Decimal(amountIn).add(fees);

if (thresholdAmount) {
if (exactInput) {
if (amountOut < thresholdAmount)
throw new Error("Threshold amount not met");
} else {
if (amountIn > thresholdAmount)
if (grossAmountIn > thresholdAmount)
throw new Error("Threshold amount exceeded");
}
}

return { amountIn, amountOut };
return { amountIn: grossAmountIn, amountOut, fees };
};

export const computeSwapAmount = (
currSqrtPrice: Decimal.Value,
targetSqrtPrice: Decimal.Value,
liquidity: Decimal.Value,
amountRem: Decimal.Value,
exactInput: boolean
) => {
export const computeSwapAmount = ({
currSqrtPrice,
targetSqrtPrice,
liquidity,
amountRem,
swapFeeRate,
exactInput,
}: {
currSqrtPrice: Decimal.Value;
targetSqrtPrice: Decimal.Value;
liquidity: Decimal.Value;
amountRem: Decimal.Value;
swapFeeRate: Decimal.Value;
exactInput: boolean;
}) => {
Decimal.set({ precision: PRECISION, rounding: ROUNDING });
const isBuy = new Decimal(targetSqrtPrice).gt(currSqrtPrice);
let amountIn: Decimal.Value = "0";
let amountOut: Decimal.Value = "0";
let nextSqrtPrice: Decimal.Value = "0";
let fees: Decimal.Value = "0";

if (exactInput) {
const amountRemainingLessFee = grossToNet(amountRem, swapFeeRate);
amountIn = isBuy
? liquidityToQuote(currSqrtPrice, targetSqrtPrice, liquidity)
: liquidityToBase(targetSqrtPrice, currSqrtPrice, liquidity);
if (new Decimal(amountRem).gte(amountIn)) {
if (new Decimal(amountRemainingLessFee).gte(amountIn)) {
nextSqrtPrice = targetSqrtPrice;
} else {
nextSqrtPrice = nextSqrtPriceAmountIn(
currSqrtPrice,
liquidity,
amountRem,
amountRemainingLessFee,
isBuy
);
}
Expand Down Expand Up @@ -118,7 +149,9 @@ export const computeSwapAmount = (

// In Uniswap, if target price is not reached, LP takes the remainder of the maximum input as fee.
// We don't do that here.
return { amountIn, amountOut, nextSqrtPrice };
fees = netToFee(amountIn, swapFeeRate);

return { amountIn, amountOut, fees, nextSqrtPrice };
};

export const nextSqrtPriceAmountIn = (
Expand Down
21 changes: 21 additions & 0 deletions models/src/math/feeMath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Decimal from "decimal.js";
import { PRECISION, ROUNDING } from "../config";

export const netToFee = (netAmount: Decimal.Value, feeRate: Decimal.Value) => {
Decimal.set({ precision: PRECISION, rounding: ROUNDING });
let netAmountBN = new Decimal(netAmount);
let one = new Decimal(1);
let fee = netAmountBN.mul(feeRate).div(one.sub(feeRate));
return fee.toFixed();
};

export const grossToNet = (
grossAmount: Decimal.Value,
feeRate: Decimal.Value
) => {
Decimal.set({ precision: PRECISION, rounding: ROUNDING });
let grossAmountDec = new Decimal(grossAmount);
let one = new Decimal(1);
let netAmount = grossAmountDec.mul(one.sub(feeRate));
return netAmount.toFixed();
};
Loading
Loading