From ae7cfc85f196abd66a1a381127ac147806d83b27 Mon Sep 17 00:00:00 2001 From: parketh Date: Wed, 28 Aug 2024 13:42:50 +0100 Subject: [PATCH 01/10] feat: deploy mainnet --- scripts/mainnet.sh | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 scripts/mainnet.sh diff --git a/scripts/mainnet.sh b/scripts/mainnet.sh new file mode 100644 index 0000000..dde8023 --- /dev/null +++ b/scripts/mainnet.sh @@ -0,0 +1,36 @@ +############################# +# Environment variables +############################# + +export STARKNET_RPC=https://free-rpc.nethermind.io/mainnet-juno +export DEPLOYER=0x1418f16f5981dd3a79ddccc7b466d93c22c47f3203808a387145bd7b70d6daf +export OWNER=0x043777a54d5e36179709060698118f1f6f5553ca1918d1004b07640dfc425000 +export STARKNET_KEYSTORE=~/.starkli-wallets/deployer/mainnet_deployer_keystore.json +export STARKNET_ACCOUNT=~/.starkli-wallets/deployer/mainnet_deployer_account.json +export ORACLE=0x2a85bd616f912537c50a49a4076db02c00b29b2cdc8a197ce92ed1837fa875b + +############################# +# Declare contract class +############################# + +# Vault token +starkli declare --rpc $STARKNET_RPC --account $STARKNET_ACCOUNT --keystore $STARKNET_KEYSTORE '/Users/parkyeung/dev/solver/target/dev/haiko_solver_replicating_VaultToken.contract_class.json' + +# Replicating Solver +starkli declare --rpc $STARKNET_RPC --account $STARKNET_ACCOUNT --keystore $STARKNET_KEYSTORE '/Users/parkyeung/dev/solver/target/dev/haiko_solver_replicating_ReplicatingSolver.contract_class.json' + +############################# +# Deploy contracts +############################# + +# Replicating Solver +starkli deploy --rpc $STARKNET_RPC $REPLICATING_SOLVER_CLASS $OWNER $ORACLE $VAULT_TOKEN_CLASS + +############################# +# Deployments +############################# + +# 28 August 2024 +export REPLICATING_SOLVER=0x07f2975ef3d288a031a842bdb50253d6255344356f9f4a02e54fbc147b007a13 +export REPLICATING_SOLVER_CLASS=0x0589858bd41fc0c922ff5c656f3373f7072fb8a69fcd02c8cdad0dce6666d4fb +export VAULT_TOKEN_CLASS=0x0375ac4a5e2fcf5323f3992541e050bd59c7aaa1b37ab6a1b271ebfbdf74a47b \ No newline at end of file From 0824f6cd9c2bcfdfe8508ac37547e95232d60dad Mon Sep 17 00:00:00 2001 From: parketh Date: Wed, 28 Aug 2024 15:22:16 +0100 Subject: [PATCH 02/10] fix: vault token class --- scripts/mainnet.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mainnet.sh b/scripts/mainnet.sh index dde8023..b6c5528 100644 --- a/scripts/mainnet.sh +++ b/scripts/mainnet.sh @@ -33,4 +33,4 @@ starkli deploy --rpc $STARKNET_RPC $REPLICATING_SOLVER_CLASS $OWNER $ORACLE $VAU # 28 August 2024 export REPLICATING_SOLVER=0x07f2975ef3d288a031a842bdb50253d6255344356f9f4a02e54fbc147b007a13 export REPLICATING_SOLVER_CLASS=0x0589858bd41fc0c922ff5c656f3373f7072fb8a69fcd02c8cdad0dce6666d4fb -export VAULT_TOKEN_CLASS=0x0375ac4a5e2fcf5323f3992541e050bd59c7aaa1b37ab6a1b271ebfbdf74a47b \ No newline at end of file +export VAULT_TOKEN_CLASS=0x04c73cd841298b8ce734d5c6bd5ab4fc4eb18ef7ad3be7d87d450f8ef98d703b \ No newline at end of file From 429e0b0894fe69cf5fb06896f8276fde68e185b6 Mon Sep 17 00:00:00 2001 From: parketh Date: Wed, 11 Sep 2024 23:37:38 +0100 Subject: [PATCH 03/10] feat: explicit swap fees --- docs/1-technical-architecture.md | 4 +- docs/2-solver-implementations.md | 2 +- docs/3-events.md | 4 +- models/src/libraries/SpreadMath.ts | 5 +- models/src/libraries/SwapLib.ts | 24 +- models/src/math/feeMath.ts | 21 ++ models/tests/libraries/SpreadMath.test.ts | 81 ++++- models/tests/libraries/SwapLib.test.ts | 25 +- models/tests/solver/DebugSwap.test.ts | 5 +- models/tests/solver/Swap.test.ts | 60 ++-- .../src/contracts/mocks/mock_solver.cairo | 89 +++-- packages/core/src/contracts/solver.cairo | 243 ++++++++------ packages/core/src/interfaces/ISolver.cairo | 62 ++-- .../core/src/libraries/store_packing.cairo | 14 +- packages/core/src/tests/helpers/utils.cairo | 3 + .../tests/libraries/test_store_packing.cairo | 4 + .../src/tests/solver/test_get_balances.cairo | 113 +++---- .../core/src/tests/solver/test_swap.cairo | 32 +- .../core/src/tests/solver/test_withdraw.cairo | 189 ++++++++--- packages/core/src/types.cairo | 16 +- .../src/contracts/replicating_solver.cairo | 31 +- .../src/libraries/spread_math.cairo | 17 +- .../src/libraries/store_packing.cairo | 6 +- .../replicating/src/libraries/swap_lib.cairo | 37 ++- .../src/tests/helpers/params.cairo | 2 +- .../tests/libraries/test_spread_math.cairo | 131 +++----- .../tests/libraries/test_store_packing.cairo | 4 +- .../src/tests/libraries/test_swap_lib.cairo | 103 +++--- .../libraries/test_swap_lib_invariants.cairo | 44 ++- .../src/tests/solver/debug_swap.cairo | 4 +- .../src/tests/solver/test_e2e.cairo | 97 +++--- .../src/tests/solver/test_market_params.cairo | 36 +- .../src/tests/solver/test_swap.cairo | 307 +++++++++++------- .../src/tests/solver/test_withdraw.cairo | 168 ++++++++-- packages/replicating/src/types.cairo | 6 +- .../src/contracts/reversion_solver.cairo | 14 +- .../reversion/src/libraries/spread_math.cairo | 87 ++--- .../src/libraries/store_packing.cairo | 6 +- .../reversion/src/libraries/swap_lib.cairo | 37 ++- packages/reversion/src/types.cairo | 6 +- scripts/src/configs.ts | 12 +- scripts/src/index.ts | 2 +- 42 files changed, 1360 insertions(+), 793 deletions(-) create mode 100644 models/src/math/feeMath.ts diff --git a/docs/1-technical-architecture.md b/docs/1-technical-architecture.md index 62a39cf..476da28 100644 --- a/docs/1-technical-architecture.md +++ b/docs/1-technical-architecture.md @@ -63,11 +63,11 @@ pub trait ISolverHooks { ### `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 diff --git a/docs/2-solver-implementations.md b/docs/2-solver-implementations.md index 75cb165..be238c1 100644 --- a/docs/2-solver-implementations.md +++ b/docs/2-solver-implementations.md @@ -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 diff --git a/docs/3-events.md b/docs/3-events.md index ff4fe3b..0f232b2 100644 --- a/docs/3-events.md +++ b/docs/3-events.md @@ -109,7 +109,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, @@ -121,7 +121,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 diff --git a/models/src/libraries/SpreadMath.ts b/models/src/libraries/SpreadMath.ts index ab4159b..a7b0cf2 100644 --- a/models/src/libraries/SpreadMath.ts +++ b/models/src/libraries/SpreadMath.ts @@ -43,7 +43,6 @@ export const getVirtualPosition = ( export const getVirtualPositionRange = ( isBid: boolean, - minSpread: Decimal.Value, delta: Decimal.Value, range: Decimal.Value, oraclePrice: Decimal.Value, @@ -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 }; } diff --git a/models/src/libraries/SwapLib.ts b/models/src/libraries/SwapLib.ts index d982c0a..7ff66ca 100644 --- a/models/src/libraries/SwapLib.ts +++ b/models/src/libraries/SwapLib.ts @@ -1,11 +1,13 @@ 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, + swapFeeRate: Decimal.Value, thresholdSqrtPrice: Decimal.Value | null, thresholdAmount: Decimal.Value | null, lowerSqrtPrice: Decimal.Value, @@ -13,7 +15,11 @@ export const getSwapAmounts = ( liquidity: Decimal.Value, baseDecimals: number, quoteDecimals: number -): { amountIn: Decimal.Value; amountOut: Decimal.Value } => { +): { + amountIn: Decimal.Value; + amountOut: Decimal.Value; + fees: Decimal.Value; +} => { const scaledLowerSqrtPrice = new Decimal(lowerSqrtPrice).mul( new Decimal(10).pow((baseDecimals - quoteDecimals) / 2) ); @@ -30,11 +36,12 @@ export const getSwapAmounts = ( ? Decimal.max(thresholdSqrtPrice, scaledLowerSqrtPrice) : scaledLowerSqrtPrice; - const { amountIn, amountOut, nextSqrtPrice } = computeSwapAmount( + const { amountIn, amountOut, fees, nextSqrtPrice } = computeSwapAmount( startSqrtPrice, targetSqrtPrice, liquidity, amount, + swapFeeRate, exactInput ); @@ -48,7 +55,7 @@ export const getSwapAmounts = ( } } - return { amountIn, amountOut }; + return { amountIn: new Decimal(amountIn).add(fees), amountOut, fees }; }; export const computeSwapAmount = ( @@ -56,6 +63,7 @@ export const computeSwapAmount = ( targetSqrtPrice: Decimal.Value, liquidity: Decimal.Value, amountRem: Decimal.Value, + swapFeeRate: Decimal.Value, exactInput: boolean ) => { Decimal.set({ precision: PRECISION, rounding: ROUNDING }); @@ -63,18 +71,20 @@ export const computeSwapAmount = ( 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 ); } @@ -118,7 +128,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 = ( diff --git a/models/src/math/feeMath.ts b/models/src/math/feeMath.ts new file mode 100644 index 0000000..e0bceb5 --- /dev/null +++ b/models/src/math/feeMath.ts @@ -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(); +}; diff --git a/models/tests/libraries/SpreadMath.test.ts b/models/tests/libraries/SpreadMath.test.ts index 09f44e8..a462406 100644 --- a/models/tests/libraries/SpreadMath.test.ts +++ b/models/tests/libraries/SpreadMath.test.ts @@ -53,19 +53,86 @@ const testGetVirtualPositionRangeCases = () => { const quoteDecimals = 18; const cases = [ - getVirtualPositionRange(true, 0, 0, 1, 1, baseDecimals, quoteDecimals), - getVirtualPositionRange(false, 0, 0, 1, 1, baseDecimals, quoteDecimals), + { + delta: 0, + range: 1, + oraclePrice: 1, + }, + { + delta: 0, + range: 1000, + oraclePrice: 1, + }, + { + delta: -100, + range: 1000, + oraclePrice: 1, + }, + { + delta: 100, + range: 1000, + oraclePrice: 1, + }, + { + delta: -5000, + range: 1000, + oraclePrice: 1, + }, + { + delta: 5000, + range: 1000, + oraclePrice: 1, + }, + { + delta: -7905625, + range: 1000, + oraclePrice: 1, + }, + { + delta: 7905625, + range: 1000, + oraclePrice: 1, + }, + { + delta: 0, + range: 1000, + oraclePrice: "0.0000000000000000000000000001", + }, + { + delta: 0, + range: 1000, + oraclePrice: + "21445968470833706281754813411422482.6295263805072231182393896500", + }, ]; for (let i = 0; i < cases.length; i++) { - const pos = cases[i]; + const c = cases[i]; console.log(`Case ${i + 1}`); + const bid = getVirtualPositionRange( + true, + c.delta, + c.range, + c.oraclePrice, + baseDecimals, + quoteDecimals + ); + const ask = getVirtualPositionRange( + false, + c.delta, + c.range, + c.oraclePrice, + baseDecimals, + quoteDecimals + ); console.log({ - lowerLimit: new Decimal(pos.lowerLimit).toFixed(0), - upperLimit: new Decimal(pos.upperLimit).toFixed(0), + bidLower: new Decimal(bid.lowerLimit).toFixed(0), + bidUpper: new Decimal(bid.upperLimit).toFixed(0), + askLower: new Decimal(ask.lowerLimit).toFixed(0), + askUpper: new Decimal(ask.upperLimit).toFixed(0), }); } }; -testGetVirtualPositionCases(); -// testGetVirtualPositionRangeCases(); +// testGetVirtualPositionCases(); +testGetVirtualPositionRangeCases(); diff --git a/models/tests/libraries/SwapLib.test.ts b/models/tests/libraries/SwapLib.test.ts index 99a4cd7..775ed98 100644 --- a/models/tests/libraries/SwapLib.test.ts +++ b/models/tests/libraries/SwapLib.test.ts @@ -1,22 +1,35 @@ import Decimal from "decimal.js"; import { getSwapAmounts } from "../../src/libraries/SwapLib"; +type SwapParams = { + 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; +}; const testGetSwapAmounts = () => { - const cases = [ + const cases: SwapParams[] = [ { isBuy: true, exactInput: true, amount: 1, + swapFeeRate: 0.005, thresholdSqrtPrice: null, thresholdAmount: null, - lowerSqrtPrice: 1, - upperSqrtPrice: 1.2 ** 0.5, + lowerSqrtPrice: 0.8 ** 0.5, + upperSqrtPrice: 1, liquidity: 10000, }, { isBuy: true, exactInput: true, amount: 1, + swapFeeRate: 0.005, thresholdSqrtPrice: null, thresholdAmount: null, lowerSqrtPrice: 1, @@ -27,6 +40,7 @@ const testGetSwapAmounts = () => { isBuy: false, exactInput: true, amount: 10, + swapFeeRate: 0.005, thresholdSqrtPrice: 0.95 ** 0.5, thresholdAmount: null, lowerSqrtPrice: 0.8 ** 0.5, @@ -37,6 +51,7 @@ const testGetSwapAmounts = () => { isBuy: true, exactInput: true, amount: 10, + swapFeeRate: 0.005, thresholdSqrtPrice: 1.05 ** 0.5, thresholdAmount: null, lowerSqrtPrice: 1, @@ -50,10 +65,11 @@ const testGetSwapAmounts = () => { for (let i = 0; i < cases.length; i++) { const params = cases[i]; - const { amountIn, amountOut } = getSwapAmounts( + const { amountIn, amountOut, fees } = getSwapAmounts( params.isBuy, params.exactInput, params.amount, + params.swapFeeRate, params.thresholdSqrtPrice, params.thresholdAmount, params.lowerSqrtPrice, @@ -66,6 +82,7 @@ const testGetSwapAmounts = () => { console.log({ amountIn: new Decimal(amountIn).mul(1e18).toFixed(0), amountOut: new Decimal(amountOut).mul(1e18).toFixed(0), + fees: new Decimal(fees).mul(1e18).toFixed(0), }); } }; diff --git a/models/tests/solver/DebugSwap.test.ts b/models/tests/solver/DebugSwap.test.ts index 138472c..355f81d 100644 --- a/models/tests/solver/DebugSwap.test.ts +++ b/models/tests/solver/DebugSwap.test.ts @@ -8,7 +8,7 @@ import { getSwapAmounts } from "../../src/libraries/SwapLib"; const isBuy = false; const exactInput = false; const amount = "10000000000"; -const minSpread = 25; +const swapFeeRate = 25; const range = 5000; const maxDelta = 500; const oraclePrice = "0.37067545"; @@ -18,10 +18,8 @@ const baseDecimals = 18; const quoteDecimals = 6; const delta = getDelta(maxDelta, baseReserves, quoteReserves, oraclePrice); -console.log({ delta }); const { lowerLimit, upperLimit } = getVirtualPositionRange( !isBuy, - minSpread, delta, range, oraclePrice, @@ -39,6 +37,7 @@ console.log({ lowerSqrtPrice, upperSqrtPrice, liquidity }); const { amountIn, amountOut } = getSwapAmounts( isBuy, exactInput, + swapFeeRate, amount, null, null, diff --git a/models/tests/solver/Swap.test.ts b/models/tests/solver/Swap.test.ts index dc8fc01..5fc2abd 100644 --- a/models/tests/solver/Swap.test.ts +++ b/models/tests/solver/Swap.test.ts @@ -11,7 +11,7 @@ type Case = { oraclePrice: Decimal.Value; baseReserves: Decimal.Value; quoteReserves: Decimal.Value; - minSpread: Decimal.Value; + feeRate: Decimal.Value; range: Decimal.Value; maxDelta: Decimal.Value; maxSkew: Decimal.Value; @@ -51,11 +51,11 @@ const getSwapCases = (): SwapCase[] => { const testSwapCases = () => { const cases: Case[] = [ { - description: "1) Full range liq, price 1, no spread", + description: "1) Full range liq, price 1, no fees", oraclePrice: 1, baseReserves: 1000, quoteReserves: 1000, - minSpread: 0, + feeRate: 0, range: 7906625, maxDelta: 0, maxSkew: 0, @@ -64,11 +64,11 @@ const testSwapCases = () => { thresholdAmount: null, }, { - description: "2) Full range liq, price 0.1, no spread", + description: "2) Full range liq, price 0.1, no fees", oraclePrice: 0.1, baseReserves: 100, quoteReserves: 1000, - minSpread: 0, + feeRate: 0, range: 7676365, maxDelta: 0, maxSkew: 0, @@ -77,11 +77,11 @@ const testSwapCases = () => { thresholdAmount: null, }, { - description: "3) Full range liq, price 10, no spread", + description: "3) Full range liq, price 10, no fees", oraclePrice: 10, baseReserves: 1000, quoteReserves: 100, - minSpread: 0, + feeRate: 0, range: 7676365, maxDelta: 0, maxSkew: 0, @@ -90,11 +90,11 @@ const testSwapCases = () => { thresholdAmount: null, }, { - description: "4) Concentrated liq, price 1, no spread", + description: "4) Concentrated liq, price 1, no fees", oraclePrice: 1, baseReserves: 1000, quoteReserves: 1000, - minSpread: 0, + feeRate: 0, range: 5000, maxDelta: 0, maxSkew: 0, @@ -103,11 +103,11 @@ const testSwapCases = () => { thresholdAmount: null, }, { - description: "5) Concentrated liq, price 1, 100 spread", + description: "5) Concentrated liq, price 1, 100 fees", oraclePrice: 1, baseReserves: 1000, quoteReserves: 1000, - minSpread: 100, + feeRate: 100, range: 5000, maxDelta: 0, maxSkew: 0, @@ -116,11 +116,11 @@ const testSwapCases = () => { thresholdAmount: null, }, { - description: "6) Concentrated liq, price 10, 50000 spread", + description: "6) Concentrated liq, price 10, 5000 fees", oraclePrice: 10, baseReserves: 1000, quoteReserves: 1000, - minSpread: 50000, + feeRate: 5000, range: 5000, maxDelta: 0, maxSkew: 0, @@ -129,11 +129,11 @@ const testSwapCases = () => { thresholdAmount: null, }, { - description: "7) Concentrated liq, price 1, 100 spread, 500 max delta", + description: "7) Concentrated liq, price 1, 100 fees, 500 max delta", oraclePrice: 1, baseReserves: 500, quoteReserves: 1000, - minSpread: 100, + feeRate: 100, range: 5000, maxDelta: 500, maxSkew: 0, @@ -142,12 +142,11 @@ const testSwapCases = () => { thresholdAmount: null, }, { - description: - "8) Concentrated liq, price 0.1, 100 spread, 20000 max delta", + description: "8) Concentrated liq, price 0.1, 100 fees, 20000 max delta", oraclePrice: 0.1, baseReserves: 500, quoteReserves: 1000, - minSpread: 100, + feeRate: 100, range: 5000, maxDelta: 20000, maxSkew: 0, @@ -160,7 +159,7 @@ const testSwapCases = () => { oraclePrice: 1, baseReserves: 100, quoteReserves: 100, - minSpread: 100, + feeRate: 100, range: 5000, maxDelta: 0, maxSkew: 0, @@ -173,7 +172,7 @@ const testSwapCases = () => { oraclePrice: 1000000000000000, baseReserves: 1000, quoteReserves: 1000, - minSpread: 100, + feeRate: 100, range: 5000, maxDelta: 0, maxSkew: 0, @@ -186,7 +185,7 @@ const testSwapCases = () => { oraclePrice: "0.00000001", baseReserves: 1000, quoteReserves: 1000, - minSpread: 100, + feeRate: 100, range: 5000, maxDelta: 0, maxSkew: 0, @@ -199,7 +198,7 @@ const testSwapCases = () => { oraclePrice: 1, baseReserves: 100, quoteReserves: 100, - minSpread: 100, + feeRate: 100, range: 50000, maxDelta: 0, maxSkew: 0, @@ -222,7 +221,7 @@ const testSwapCases = () => { oraclePrice: 1, baseReserves: 100, quoteReserves: 100, - minSpread: 100, + feeRate: 100, range: 50000, maxDelta: 0, maxSkew: 0, @@ -245,13 +244,13 @@ const testSwapCases = () => { oraclePrice: 1, baseReserves: 1000, quoteReserves: 1000, - minSpread: 100, + feeRate: 100, range: 5000, maxDelta: 0, maxSkew: 0, amount: 100, thresholdSqrtPrice: null, - thresholdAmount: "0.99650000000000000000", + thresholdAmount: "0.98750000000000000000", swapCasesOverride: [ { isBuy: true, @@ -268,13 +267,13 @@ const testSwapCases = () => { oraclePrice: 1, baseReserves: 1000, quoteReserves: 1000, - minSpread: 100, + feeRate: 100, range: 5000, maxDelta: 0, maxSkew: 0, amount: 100, thresholdSqrtPrice: null, - thresholdAmount: "1.00350000000000000000", + thresholdAmount: "101.5", swapCasesOverride: [ { isBuy: true, @@ -294,7 +293,7 @@ const testSwapCases = () => { oraclePrice, baseReserves, quoteReserves, - minSpread, + feeRate, range, maxDelta, amount, @@ -317,7 +316,6 @@ const testSwapCases = () => { ); const { lowerLimit, upperLimit } = getVirtualPositionRange( !isBuy, - minSpread, delta, range, oraclePrice, @@ -330,10 +328,11 @@ const testSwapCases = () => { upperLimit, isBuy ? baseReserves : quoteReserves ); - const { amountIn, amountOut } = getSwapAmounts( + const { amountIn, amountOut, fees } = getSwapAmounts( isBuy, exactInput, amount, + Number(feeRate) / 10000, thresholdSqrtPrice, thresholdAmount, lowerSqrtPrice, @@ -345,6 +344,7 @@ const testSwapCases = () => { console.log({ amountIn: new Decimal(amountIn).mul(1e18).toFixed(0), amountOut: new Decimal(amountOut).mul(1e18).toFixed(0), + fees: new Decimal(fees).mul(1e18).toFixed(0), }); j++; } diff --git a/packages/core/src/contracts/mocks/mock_solver.cairo b/packages/core/src/contracts/mocks/mock_solver.cairo index a39234b..2a273c7 100644 --- a/packages/core/src/contracts/mocks/mock_solver.cairo +++ b/packages/core/src/contracts/mocks/mock_solver.cairo @@ -12,6 +12,19 @@ pub trait IMockSolver { // * `market_id` - market id // * `params` - market params fn set_price(ref self: TContractState, market_id: felt252, price: u256); + + // Get fee rate for solver market. + // + // # Returns + // * `fee_rate` - fee rate + fn fee_rate(self: @TContractState, market_id: felt252) -> u16; + + // Set fee rate for solver market. + // + // # Params + // * `market_id` - market id + // * `fee_rate` - fee rate + fn set_fee_rate(ref self: TContractState, market_id: felt252, fee_rate: u16); } #[starknet::contract] @@ -21,6 +34,7 @@ pub mod MockSolver { use starknet::contract_address::contract_address_const; use starknet::get_block_timestamp; use starknet::class_hash::ClassHash; + use core::cmp::min; // Local imports. use super::IMockSolver; @@ -30,7 +44,7 @@ pub mod MockSolver { use haiko_solver_core::types::{PositionInfo, MarketState, MarketInfo, SwapParams}; // Haiko imports. - use haiko_lib::math::math; + use haiko_lib::math::{math, fee_math}; use haiko_lib::constants::ONE; // External imports. @@ -58,6 +72,8 @@ pub mod MockSolver { solver: SolverComponent::Storage, // Price (base 1e28, indexed by market id) price: LegacyMap::, + // Fee rate + fee_rate: LegacyMap::, } //////////////////////////////// @@ -93,11 +109,12 @@ pub mod MockSolver { // * `swap_params` - swap parameters // // # Returns - // * `amount_in` - amount in + // * `amount_in` - amount in including fees // * `amount_out` - amount out + // * `fees` - fees fn quote( self: @ContractState, market_id: felt252, swap_params: SwapParams, - ) -> (u256, u256) { + ) -> (u256, u256, u256) { // Run validity checks. let state: MarketState = self.solver.market_state.read(market_id); let market_info: MarketInfo = self.solver.market_info.read(market_id); @@ -115,32 +132,40 @@ pub mod MockSolver { let price = math::mul_div(unscaled_price, quote_scale, base_scale, false); // Calculate and return swap amounts. - let amount_calc = if swap_params.is_buy == swap_params.exact_input { - math::mul_div(swap_params.amount, ONE, price, false) - } else { - math::mul_div(swap_params.amount, price, ONE, false) - }; + let fee_rate = self.fee_rate.read(market_id); let (amount_in, amount_out) = if swap_params.exact_input { - (swap_params.amount, amount_calc) + let fees = fee_math::calc_fee(swap_params.amount, fee_rate); + let amount_in_excl_fees = swap_params.amount - fees; + let amount_out = if swap_params.is_buy { + math::mul_div(amount_in_excl_fees, ONE, price, false) + } else { + math::mul_div(amount_in_excl_fees, price, ONE, false) + }; + (swap_params.amount, amount_out) } else { - (amount_calc, swap_params.amount) + let amount_in_excl_fees = if swap_params.is_buy { + math::mul_div(swap_params.amount, price, ONE, false) + } else { + math::mul_div(swap_params.amount, ONE, price, false) + }; + let amount_in = fee_math::net_to_gross(amount_in_excl_fees, fee_rate); + (amount_in, swap_params.amount) }; - // Cap at available amount. - let (max_amount_in, max_amount_out) = if swap_params.is_buy { - let amount_in = math::mul_div(state.base_reserves, price, ONE, false); - (amount_in, state.base_reserves) + // Cap amount out by reserves. + let amount_out_capped = if swap_params.is_buy { + min(amount_out, state.base_reserves) } else { - let amount_out = math::mul_div(state.quote_reserves, ONE, price, false); - (state.quote_reserves, amount_out) + min(amount_out, state.quote_reserves) }; - - // Return capped amounts. - if amount_in > max_amount_in || amount_out > max_amount_out { - (max_amount_in, max_amount_out) + let amount_in_capped = if amount_out_capped < amount_out { + math::mul_div(amount_in, amount_out_capped, amount_out, true) } else { - (amount_in, amount_out) - } + amount_in + }; + let fees_capped = fee_math::calc_fee(amount_in_capped, fee_rate); + + (amount_in_capped, amount_out_capped, fees_capped) } // Initial token supply to mint when first depositing to a market. @@ -200,5 +225,25 @@ pub mod MockSolver { fn set_price(ref self: ContractState, market_id: felt252, price: u256) { self.price.write(market_id, price); } + + // Get fee rate + // + // # Arguments + // * `market_id` - market id + // + // # Returns + // * `fee_rate` - fee rate + fn fee_rate(self: @ContractState, market_id: felt252) -> u16 { + self.fee_rate.read(market_id) + } + + // Set fee rate + // + // # Arguments + // * `market_id` - market id + // * `fee_rate` - fee rate + fn set_fee_rate(ref self: ContractState, market_id: felt252, fee_rate: u16) { + self.fee_rate.write(market_id, fee_rate); + } } } diff --git a/packages/core/src/contracts/solver.cairo b/packages/core/src/contracts/solver.cairo index c2db48b..54206d1 100644 --- a/packages/core/src/contracts/solver.cairo +++ b/packages/core/src/contracts/solver.cairo @@ -100,6 +100,7 @@ pub mod SolverComponent { pub exact_input: bool, pub amount_in: u256, pub amount_out: u256, + pub fees: u256, } #[derive(Drop, starknet::Event)] @@ -121,6 +122,8 @@ pub mod SolverComponent { pub market_id: felt252, pub base_amount: u256, pub quote_amount: u256, + pub base_fees: u256, + pub quote_fees: u256, pub shares: u256, } @@ -271,86 +274,49 @@ pub mod SolverComponent { // # Returns // * `base_amount` - total base tokens owned // * `quote_amount` - total quote tokens owned - fn get_balances(self: @ComponentState, market_id: felt252) -> (u256, u256) { + // * `base_fees` - total base fees owned + // * `quote_fees` - total quote fees owned + fn get_balances( + self: @ComponentState, market_id: felt252 + ) -> (u256, u256, u256, u256) { let state: MarketState = self.market_state.read(market_id); - (state.base_reserves, state.quote_reserves) + (state.base_reserves, state.quote_reserves, state.base_fees, state.quote_fees) } - // Get token amounts held in reserve for a list of markets. + // Get user token balances held in solver market. // // # Arguments - // * `market_ids` - list of market ids - // - // # Returns - // * `balances` - list of base and quote token amounts - fn get_balances_array( - self: @ComponentState, market_ids: Span - ) -> Span<(u256, u256)> { - let mut balances: Array<(u256, u256)> = array![]; - let mut i = 0; - loop { - if i == market_ids.len() { - break; - } - let market_id = *market_ids.at(i); - let (base_amount, quote_amount) = self.get_balances(market_id); - balances.append((base_amount, quote_amount)); - i += 1; - }; - // Return balances. - balances.span() - } - - // Get token amounts and shares held in solver market for a list of users. - // - // # Arguments - // * `users` - list of user address - // * `market_ids` - list of market ids + // * `user` - user address + // * `market_id` - market id // // # Returns // * `base_amount` - base tokens owned by user // * `quote_amount` - quote tokens owned by user - // * `user_shares` - user shares - // * `total_shares` - total shares in market - fn get_user_balances_array( - self: @ComponentState, - users: Span, - market_ids: Span - ) -> Span<(u256, u256, u256, u256)> { - // Check users and market ids of equal length. - assert(users.len() == market_ids.len(), 'LengthMismatch'); - - let mut balances: Array<(u256, u256, u256, u256)> = array![]; - let mut i = 0; - loop { - if i == users.len() { - break; - } - let market_id = *market_ids.at(i); - let state = self.market_state.read(market_id); - // Handle non-existent vault token. - if state.vault_token == contract_address_const::<0x0>() { - balances.append((0, 0, 0, 0)); - i += 1; - continue; - } - // Handle divison by 0 case. - let vault_token = ERC20ABIDispatcher { contract_address: state.vault_token }; - let total_shares = vault_token.totalSupply(); - if total_shares == 0 { - balances.append((0, 0, 0, 0)); - i += 1; - continue; - } - // Calculate balances and shares - let user_shares = vault_token.balanceOf(*users.at(i)); - let (base_balance, quote_balance) = self.get_balances(market_id); - let base_amount = math::mul_div(base_balance, user_shares, total_shares, false); - let quote_amount = math::mul_div(quote_balance, user_shares, total_shares, false); - balances.append((base_amount, quote_amount, user_shares, total_shares)); - i += 1; - }; - balances.span() + // * `base_fees` - base fees owned by user + // * `quote_fees` - quote fees owned by user + fn get_user_balances( + self: @ComponentState, user: ContractAddress, market_id: felt252 + ) -> (u256, u256, u256, u256) { + let state: MarketState = self.market_state.read(market_id); + // Handle non-existent vault token. + if state.vault_token == contract_address_const::<0x0>() { + return (0, 0, 0, 0); + } + // Handle divison by 0 case. + let vault_token = ERC20ABIDispatcher { contract_address: state.vault_token }; + let total_shares = vault_token.totalSupply(); + if total_shares == 0 { + return (0, 0, 0, 0); + } + // Calculate balances and shares + let user_shares = vault_token.balanceOf(user); + let (base_balance, quote_balance, base_fees, quote_fees) = self.get_balances(market_id); + let user_base = math::mul_div(base_balance, user_shares, total_shares, false); + let user_quote = math::mul_div(quote_balance, user_shares, total_shares, false); + let user_base_fees = math::mul_div(base_fees, user_shares, total_shares, false); + let user_quote_fees = math::mul_div(quote_fees, user_shares, total_shares, false); + + (user_base, user_quote, user_base_fees, user_quote_fees) } // Create market for solver. @@ -424,11 +390,12 @@ pub mod SolverComponent { // * `swap_params` - swap parameters // // # Returns - // * `amount_in` - amount in + // * `amount_in` - amount in including fees // * `amount_out` - amount out + // * `fees` - fees fn swap( ref self: ComponentState, market_id: felt252, swap_params: SwapParams, - ) -> (u256, u256) { + ) -> (u256, u256, u256) { // Run validity checks. let state: MarketState = self.market_state.read(market_id); let market_info: MarketInfo = self.market_info.read(market_id); @@ -440,7 +407,7 @@ pub mod SolverComponent { // Get amounts. let solver_hooks = ISolverHooksDispatcher { contract_address: get_contract_address() }; - let (amount_in, amount_out) = solver_hooks.quote(market_id, swap_params); + let (amount_in, amount_out, fees) = solver_hooks.quote(market_id, swap_params); // Check amounts non-zero and satisfy threshold amounts. assert(amount_in != 0 && amount_out != 0, 'AmountZero'); @@ -472,11 +439,13 @@ pub mod SolverComponent { // Update reserves. let mut state: MarketState = self.market_state.read(market_id); if swap_params.is_buy { - state.quote_reserves += amount_in; + state.quote_reserves += amount_in - fees; state.base_reserves -= amount_out; + state.quote_fees += fees; } else { - state.base_reserves += amount_in; + state.base_reserves += amount_in - fees; state.quote_reserves -= amount_out; + state.base_fees += fees; } self.market_state.write(market_id, state); @@ -495,12 +464,13 @@ pub mod SolverComponent { is_buy: swap_params.is_buy, exact_input: swap_params.exact_input, amount_in, - amount_out + amount_out, + fees } ) ); - (amount_in, amount_out) + (amount_in, amount_out, fees) } // Deposit initial liquidity to market. @@ -775,11 +745,13 @@ pub mod SolverComponent { // * `shares` - pool shares to burn // // # Returns - // * `base_amount` - base asset withdrawn - // * `quote_amount` - quote asset withdrawn + // * `base_amount` - base asset withdrawn, including fees + // * `quote_amount` - quote asset withdrawn, including fees + // * `base_fees` - base fees withdrawn + // * `quote_fees` - quote fees withdrawn fn withdraw_public( ref self: ComponentState, market_id: felt252, shares: u256 - ) -> (u256, u256) { + ) -> (u256, u256, u256, u256) { // Fetch state. let market_info = self.market_info.read(market_id); let mut state: MarketState = self.market_state.read(market_id); @@ -798,14 +770,18 @@ pub mod SolverComponent { IVaultTokenDispatcher { contract_address: state.vault_token }.burn(caller, shares); // Calculate share of reserves to withdraw. Commit state changes. - let base_withdraw = math::mul_div(state.base_reserves, shares, total_supply, false); - let quote_withdraw = math::mul_div(state.quote_reserves, shares, total_supply, false); - state.base_reserves -= base_withdraw; - state.quote_reserves -= quote_withdraw; + let base_amount = math::mul_div(state.base_reserves, shares, total_supply, false); + let quote_amount = math::mul_div(state.quote_reserves, shares, total_supply, false); + let base_fees = math::mul_div(state.base_fees, shares, total_supply, false); + let quote_fees = math::mul_div(state.quote_fees, shares, total_supply, false); + state.base_reserves -= base_amount; + state.quote_reserves -= quote_amount; + state.base_fees -= base_fees; + state.quote_fees -= quote_fees; self.market_state.write(market_id, state); // Deduct applicable fees, emit events and return withdrawn amounts. - self._withdraw(market_id, base_withdraw, quote_withdraw, shares) + self._withdraw(market_id, base_amount, quote_amount, base_fees, quote_fees, shares) } // Withdraw exact token amounts from market. @@ -817,14 +793,16 @@ pub mod SolverComponent { // * `quote_amount` - quote amount requested // // # Returns - // * `base_amount` - base asset withdrawn - // * `quote_amount` - quote asset withdrawn + // * `base_amount` - base asset withdrawn, including fees + // * `quote_amount` - quote asset withdrawn, including fees + // * `base_fees` - base fees withdrawn + // * `quote_fees` - quote fees withdrawn fn withdraw_private( ref self: ComponentState, market_id: felt252, base_amount: u256, quote_amount: u256 - ) -> (u256, u256) { + ) -> (u256, u256, u256, u256) { // Fetch state. let market_info = self.market_info.read(market_id); let mut state: MarketState = self.market_state.read(market_id); @@ -835,16 +813,42 @@ pub mod SolverComponent { self.assert_market_owner(market_id); // Cap withdraw amount at available. Commit state changes. - let base_withdraw = min(base_amount, state.base_reserves); - let quote_withdraw = min(quote_amount, state.quote_reserves); + let base_withdraw = min(base_amount, state.base_reserves + state.base_fees); + let quote_withdraw = min(quote_amount, state.quote_reserves + state.quote_fees); + let base_fees_withdraw = if base_withdraw < state.base_reserves + state.base_fees { + math::mul_div( + state.base_fees, base_withdraw, state.base_reserves + state.base_fees, false + ) + } else { + state.base_fees + }; + let quote_fees_withdraw = if quote_withdraw < state.quote_reserves + state.quote_fees { + math::mul_div( + state.quote_fees, quote_withdraw, state.quote_reserves + state.quote_fees, false + ) + } else { + state.quote_fees + }; + let base_withdraw_excl_fees = base_withdraw - base_fees_withdraw; + let quote_withdraw_excl_fees = quote_withdraw - quote_fees_withdraw; // Commit state updates. - state.base_reserves -= base_withdraw; - state.quote_reserves -= quote_withdraw; + state.base_reserves -= base_withdraw_excl_fees; + state.quote_reserves -= quote_withdraw_excl_fees; + state.base_fees -= base_fees_withdraw; + state.quote_fees -= quote_fees_withdraw; self.market_state.write(market_id, state); // Deduct applicable fees, emit events and return withdrawn amounts. - self._withdraw(market_id, base_withdraw, quote_withdraw, 0) + self + ._withdraw( + market_id, + base_withdraw_excl_fees, + quote_withdraw_excl_fees, + base_fees_withdraw, + quote_fees_withdraw, + 0 + ) } // Collect withdrawal fees. @@ -1059,23 +1063,31 @@ pub mod SolverComponent { // // # Arguments // * `market_id` - market id - // * `base_amount` - amount of base assets to withdraw, gross of withdraw fees - // * `quote_amount` - amount of quote assets to withdraw, gross of withdraw fees + // * `base_amount` - amount of base assets to withdraw, excluding earned swap fees and withdraw fees + // * `quote_amount` - amount of quote assets to withdraw, excluding earned swap fees and withdraw fees + // * `base_fees` - amount of earned base fees, gross of withdraw fees + // * `quote_fees` - amount of earned quote fees, gross of withdraw fees // * `shares` - pool shares to burn for public vaults, or 0 for private vaults // // # Returns - // * `base_withdraw` - base assets withdrawn - // * `quote_withdraw` - quote assets withdrawn + // * `base_withdraw` - base assets withdrawn, including fees + // * `quote_withdraw` - quote assets withdrawn, including fees + // * `base_fees` - base fees withdrawn + // * `quote_fees` - quote fees withdrawn fn _withdraw( ref self: ComponentState, market_id: felt252, base_amount: u256, quote_amount: u256, + base_fees: u256, + quote_fees: u256, shares: u256 - ) -> (u256, u256) { + ) -> (u256, u256, u256, u256) { // Initialise values. - let mut base_withdraw = base_amount; - let mut quote_withdraw = quote_amount; + let mut base_withdraw = base_amount + base_fees; + let mut quote_withdraw = quote_amount + quote_fees; + let mut base_fees_withdraw = base_fees; + let mut quote_fees_withdraw = quote_fees; let mut base_withdraw_fees = 0; let mut quote_withdraw_fees = 0; @@ -1086,6 +1098,8 @@ pub mod SolverComponent { quote_withdraw_fees = fee_math::calc_fee(quote_withdraw, withdraw_fee_rate); base_withdraw -= base_withdraw_fees; quote_withdraw -= quote_withdraw_fees; + base_fees_withdraw -= fee_math::calc_fee(base_fees, withdraw_fee_rate); + quote_fees_withdraw -= fee_math::calc_fee(quote_fees, withdraw_fee_rate); } // Transfer tokens to caller. @@ -1103,19 +1117,34 @@ pub mod SolverComponent { // Commit state updates. if base_withdraw_fees != 0 { - let base_fees = self.withdraw_fees.read(market_info.base_token); - self.withdraw_fees.write(market_info.base_token, base_fees + base_withdraw_fees); + let base_withdraw_fees_balance = self.withdraw_fees.read(market_info.base_token); + self + .withdraw_fees + .write(market_info.base_token, base_withdraw_fees_balance + base_withdraw_fees); } if quote_withdraw_fees != 0 { - let quote_fees = self.withdraw_fees.read(market_info.quote_token); - self.withdraw_fees.write(market_info.quote_token, quote_fees + quote_withdraw_fees); + let quote_withdraw_fees_balance = self.withdraw_fees.read(market_info.quote_token); + self + .withdraw_fees + .write( + market_info.quote_token, quote_withdraw_fees_balance + quote_withdraw_fees + ); } // Emit events. + // Here we emit the gross amounts, without deducting withdraw fees. self .emit( Event::Withdraw( - Withdraw { market_id, caller, base_amount, quote_amount, shares, } + Withdraw { + market_id, + caller, + base_amount, + quote_amount, + base_fees, + quote_fees, + shares + } ) ); if base_withdraw_fees != 0 { @@ -1142,7 +1171,7 @@ pub mod SolverComponent { } // Return withdrawn amounts. - (base_withdraw, quote_withdraw) + (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) } } } diff --git a/packages/core/src/interfaces/ISolver.cairo b/packages/core/src/interfaces/ISolver.cairo index ffa1187..026d110 100644 --- a/packages/core/src/interfaces/ISolver.cairo +++ b/packages/core/src/interfaces/ISolver.cairo @@ -51,31 +51,25 @@ pub trait ISolver { // # Returns // * `base_amount` - total base tokens owned // * `quote_amount` - total quote tokens owned - fn get_balances(self: @TContractState, market_id: felt252) -> (u256, u256); + // * `base_fees` - total base fees owned + // * `quote_fees` - total quote fees owned + fn get_balances(self: @TContractState, market_id: felt252) -> (u256, u256, u256, u256); - // Get token amounts held in reserve for a list of markets. + // Get user token balances held in solver market. // // # Arguments - // * `market_ids` - list of market ids - // - // # Returns - // * `balances` - list of base and quote token amounts - fn get_balances_array(self: @TContractState, market_ids: Span) -> Span<(u256, u256)>; - - // Get token amounts and shares held in solver market for a list of users. - // - // # Arguments - // * `users` - list of user address - // * `market_ids` - list of market ids + // * `user` - user address + // * `market_id` - market id // // # Returns // * `base_amount` - base tokens owned by user // * `quote_amount` - quote tokens owned by user - // * `user_shares` - user shares - // * `total_shares` - total shares in market - fn get_user_balances_array( - self: @TContractState, users: Span, market_ids: Span - ) -> Span<(u256, u256, u256, u256)>; + // * `base_fees` - base fees owned by user + // * `quote_fees` - quote fees owned by user + fn get_user_balances( + self: @TContractState, user: ContractAddress, market_id: felt252 + ) -> (u256, u256, u256, u256); + // Create market for solver. // At the moment, only callable by contract owner to prevent unwanted claiming of markets. @@ -98,9 +92,12 @@ pub trait ISolver { // * `swap_params` - swap parameters // // # Returns - // * `amount_in` - amount in + // * `amount_in` - amount in including fees // * `amount_out` - amount out - fn swap(ref self: TContractState, market_id: felt252, swap_params: SwapParams,) -> (u256, u256); + // * `fees` - fees + fn swap( + ref self: TContractState, market_id: felt252, swap_params: SwapParams, + ) -> (u256, u256, u256); // Deposit initial liquidity to market. // Should be used whenever total deposits in a market are zero. This can happen both @@ -185,9 +182,13 @@ pub trait ISolver { // * `shares` - pool shares to burn // // # Returns - // * `base_amount` - base asset withdrawn - // * `quote_amount` - quote asset withdrawn - fn withdraw_public(ref self: TContractState, market_id: felt252, shares: u256) -> (u256, u256); + // * `base_amount` - base asset withdrawn, including fees + // * `quote_amount` - quote asset withdrawn, including fees + // * `base_fees` - base fees withdrawn + // * `quote_fees` - quote fees withdrawn + fn withdraw_public( + ref self: TContractState, market_id: felt252, shares: u256 + ) -> (u256, u256, u256, u256); // Withdraw exact token amounts from market. // Called for private vaults. For public vaults, use `withdraw_public`. @@ -198,11 +199,13 @@ pub trait ISolver { // * `quote_amount` - quote amount requested // // # Returns - // * `base_amount` - base asset withdrawn - // * `quote_amount` - quote asset withdrawn + // * `base_amount` - base asset withdrawn, including fees + // * `quote_amount` - quote asset withdrawn, including fees + // * `base_fees` - base fees withdrawn + // * `quote_fees` - quote fees withdrawn fn withdraw_private( ref self: TContractState, market_id: felt252, base_amount: u256, quote_amount: u256 - ) -> (u256, u256); + ) -> (u256, u256, u256, u256); // Collect withdrawal fees. // Only callable by contract owner. @@ -271,9 +274,12 @@ pub trait ISolverHooks { // * `swap_params` - swap parameters // // # Returns - // * `amount_in` - amount in + // * `amount_in` - amount in including fees // * `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, + ) -> (u256, u256, u256); // Get the initial token supply to mint when first depositing to a market. // diff --git a/packages/core/src/libraries/store_packing.cairo b/packages/core/src/libraries/store_packing.cairo index ad0e265..f4c2b49 100644 --- a/packages/core/src/libraries/store_packing.cairo +++ b/packages/core/src/libraries/store_packing.cairo @@ -12,17 +12,21 @@ pub(crate) impl MarketStateStorePacking of StorePacking PackedMarketState { let slab0: felt252 = value.base_reserves.try_into().unwrap(); let slab1: felt252 = value.quote_reserves.try_into().unwrap(); - let slab2: felt252 = value.vault_token.try_into().unwrap(); - let slab3: felt252 = bool_to_felt(value.is_paused); - PackedMarketState { slab0, slab1, slab2, slab3 } + let slab2: felt252 = value.base_fees.try_into().unwrap(); + let slab3: felt252 = value.quote_fees.try_into().unwrap(); + let slab4: felt252 = value.vault_token.try_into().unwrap(); + let slab5: felt252 = bool_to_felt(value.is_paused); + PackedMarketState { slab0, slab1, slab2, slab3, slab4, slab5 } } fn unpack(value: PackedMarketState) -> MarketState { MarketState { base_reserves: value.slab0.try_into().unwrap(), quote_reserves: value.slab1.try_into().unwrap(), - vault_token: value.slab2.try_into().unwrap(), - is_paused: felt_to_bool(value.slab3), + base_fees: value.slab2.try_into().unwrap(), + quote_fees: value.slab3.try_into().unwrap(), + vault_token: value.slab4.try_into().unwrap(), + is_paused: felt_to_bool(value.slab5), } } } diff --git a/packages/core/src/tests/helpers/utils.cairo b/packages/core/src/tests/helpers/utils.cairo index a55efc0..698cd99 100644 --- a/packages/core/src/tests/helpers/utils.cairo +++ b/packages/core/src/tests/helpers/utils.cairo @@ -168,6 +168,9 @@ fn _before( let mock_solver = IMockSolverDispatcher { contract_address: solver.contract_address }; mock_solver.set_price(market_id, ONE); + // Set fee rate. + mock_solver.set_fee_rate(market_id, 50); + // Fund owner with initial token balances and approve strategy and market manager as spenders. let base_amount = to_e18(10000000000000000000000); let quote_amount = to_e18(10000000000000000000000); diff --git a/packages/core/src/tests/libraries/test_store_packing.cairo b/packages/core/src/tests/libraries/test_store_packing.cairo index c9ef62c..6529916 100644 --- a/packages/core/src/tests/libraries/test_store_packing.cairo +++ b/packages/core/src/tests/libraries/test_store_packing.cairo @@ -33,6 +33,8 @@ fn test_store_packing_market_state() { let market_state = MarketState { base_reserves: 1389123122000000000000000000000, quote_reserves: 2401299999999999999999999999999, + base_fees: 381289131303000000, + quote_fees: 1000000000000000000, is_paused: false, vault_token: contract_address_const::<0x123>(), }; @@ -42,6 +44,8 @@ fn test_store_packing_market_state() { assert(unpacked.base_reserves == market_state.base_reserves, 'Market state: base reserves'); assert(unpacked.quote_reserves == market_state.quote_reserves, 'Market state: quote reserves'); + assert(unpacked.base_fees == market_state.base_fees, 'Market state: base fees'); + assert(unpacked.quote_fees == market_state.quote_fees, 'Market state: quote fees'); assert(unpacked.is_paused == market_state.is_paused, 'Market state: is paused'); assert(unpacked.vault_token == market_state.vault_token, 'Market state: vault token'); } diff --git a/packages/core/src/tests/solver/test_get_balances.cairo b/packages/core/src/tests/solver/test_get_balances.cairo index 2861e72..30040f7 100644 --- a/packages/core/src/tests/solver/test_get_balances.cairo +++ b/packages/core/src/tests/solver/test_get_balances.cairo @@ -7,10 +7,11 @@ use haiko_solver_core::{ ISolver::{ISolverDispatcher, ISolverDispatcherTrait}, IVaultToken::{IVaultTokenDispatcher, IVaultTokenDispatcherTrait}, }, - tests::helpers::utils::before, + tests::helpers::utils::before, types::SwapParams }; // Haiko imports. +use haiko_lib::math::math; use haiko_lib::helpers::params::{owner, alice, bob}; use haiko_lib::helpers::utils::{to_e18, approx_eq}; @@ -38,34 +39,28 @@ fn test_get_balances() { let quote_owner = to_e18(500); solver.deposit_initial(market_id, base_owner, quote_owner); - // Get balances. - let (base, quote) = solver.get_balances(market_id); - assert(base == base_owner, 'Base amount'); - assert(quote == quote_owner, 'Quote amount'); -} - -#[test] -fn test_get_balances_array() { - let (_base_token, _quote_token, _vault_token_class, solver, market_id, _vault_token_opt) = - before( - true - ); - - // Deposit initial. - start_prank(CheatTarget::One(solver.contract_address), owner()); - let base_owner = to_e18(100); - let quote_owner = to_e18(500); - solver.deposit_initial(market_id, base_owner, quote_owner); + // Swap. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let params = SwapParams { + is_buy: true, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Get balances. - let balances = solver.get_balances_array(array![market_id].span()); - let (base, quote) = *balances.at(0); - assert(base == base_owner, 'Base amount'); - assert(quote == quote_owner, 'Quote amount'); + let (base_amount, quote_amount, base_fees, quote_fees) = solver.get_balances(market_id); + assert(base_amount == base_owner - amount_out, 'Base amount'); + assert(quote_amount == quote_owner + amount_in - fees, 'Quote amount'); + assert(base_fees == 0, 'Base fees'); + assert(quote_fees == fees, 'Quote fees'); } #[test] -fn test_get_user_balances_array() { +fn test_get_user_balances() { let (_base_token, _quote_token, _vault_token_class, solver, market_id, _vault_token_opt) = before( true @@ -73,49 +68,45 @@ fn test_get_user_balances_array() { // Deposit initial. start_prank(CheatTarget::One(solver.contract_address), owner()); - let base_owner = to_e18(100); - let quote_owner = to_e18(500); - let (_, _, shares_owner) = solver.deposit_initial(market_id, base_owner, quote_owner); + let base_deposit_owner = to_e18(100); + let quote_deposit_owner = to_e18(500); + solver.deposit_initial(market_id, base_deposit_owner, quote_deposit_owner); // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let base_alice = to_e18(50); - let quote_alice = to_e18(250); - let (_, _, shares_alice) = solver.deposit(market_id, base_alice, quote_alice); + let base_deposit_alice = to_e18(50); + let quote_deposit_alice = to_e18(250); + solver.deposit(market_id, base_deposit_alice, quote_deposit_alice); + + // Swap. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let params = SwapParams { + is_buy: true, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Get balances. - let balances = solver - .get_user_balances_array( - array![owner(), alice()].span(), array![market_id, market_id].span(), - ); - let (base_owner_, quote_owner_, shares_owner_, shares_total_owner) = *balances.at(0); - let (base_alice_, quote_alice_, shares_alice_, shares_total_alice) = *balances.at(1); + let (base_owner, quote_owner, base_fees_owner, quote_fees_owner) = solver + .get_user_balances(owner(), market_id); + let (base_alice, quote_alice, base_fees_alice, quote_fees_alice) = solver + .get_user_balances(alice(), market_id); // Run checks. - assert(base_owner == base_owner_, 'Base owner'); - assert(quote_owner == quote_owner_, 'Quote owner'); - assert(shares_owner == shares_owner_, 'Shares owner'); - assert(shares_total_owner == shares_owner + shares_alice, 'Shares total owner'); - assert(approx_eq(base_alice, base_alice_, 10), 'Base alice'); - assert(approx_eq(quote_alice, quote_alice_, 10), 'Quote alice'); - assert(shares_alice == shares_alice_, 'Shares alice'); - assert(shares_total_alice == shares_owner + shares_alice, 'Shares total alice'); -} - -//////////////////////////////// -// TESTS - Fail cases -//////////////////////////////// - -#[test] -#[should_panic(expected: ('LengthMismatch',))] -fn test_get_user_balances_array_length_mismatch() { - let (_base_token, _quote_token, _vault_token_class, solver, market_id, _vault_token_opt) = - before( - true + assert(amount_in == params.amount, 'Amount in'); + assert(fees == math::mul_div(params.amount, 50, 10000, true), 'Fees'); + assert(approx_eq(base_owner, base_deposit_owner - amount_out * 2 / 3, 1), 'Base owner'); + assert( + approx_eq(quote_owner, quote_deposit_owner + (amount_in - fees) * 2 / 3, 1), 'Quote owner' ); - - solver - .get_user_balances_array( - array![owner(), alice(), bob()].span(), array![market_id, market_id].span(), - ); + assert(base_fees_owner == 0, 'Base fees owner'); + assert(approx_eq(quote_fees_owner, fees * 2 / 3, 1), 'Quote fees owner'); + assert(approx_eq(base_alice, base_deposit_alice - amount_out / 3, 1), 'Base alice'); + assert(approx_eq(quote_alice, quote_deposit_alice + (amount_in - fees) / 3, 1), 'Quote alice'); + assert(base_fees_alice == 0, 'Base fees alice'); + assert(approx_eq(quote_fees_alice, fees / 3, 1), 'Quote fees alice'); } diff --git a/packages/core/src/tests/solver/test_swap.cairo b/packages/core/src/tests/solver/test_swap.cairo index ccb2c84..604ce08 100644 --- a/packages/core/src/tests/solver/test_swap.cairo +++ b/packages/core/src/tests/solver/test_swap.cairo @@ -14,6 +14,7 @@ use haiko_solver_core::{ }; // Haiko imports. +use haiko_lib::math::math; use haiko_lib::helpers::params::{owner, alice}; use haiko_lib::helpers::utils::{to_e18, to_e28, approx_eq, approx_eq_pct}; use haiko_lib::helpers::actions::token::{fund, approve}; @@ -54,11 +55,12 @@ fn test_swap_buy_exact_in() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out) = solver.swap(market_id, params); + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Run checks. assert(amount_in == params.amount, 'Amount in'); - assert(amount_out == to_e18(1), 'Amount out'); + assert(amount_out == 995000000000000000, 'Amount out'); + assert(fees == math::mul_div(params.amount, 50, 10000, true), 'Fees'); } #[test] @@ -87,11 +89,12 @@ fn test_swap_sell_exact_in() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out) = solver.swap(market_id, params); + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Run checks. assert(amount_in == params.amount, 'Amount in'); - assert(amount_out == to_e18(5), 'Amount out'); + assert(amount_out == 4975000000000000000, 'Amount out'); + assert(fees == math::mul_div(params.amount, 50, 10000, true), 'Fees'); } #[test] @@ -108,7 +111,7 @@ fn test_swap_buy_exact_out() { // Deposit initial. start_prank(CheatTarget::One(solver.contract_address), owner()); - solver.deposit_initial(market_id, to_e18(5), to_e18(25)); + solver.deposit_initial(market_id, to_e18(25), to_e18(25)); // Swap buy. start_prank(CheatTarget::One(solver.contract_address), alice()); @@ -120,11 +123,12 @@ fn test_swap_buy_exact_out() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out) = solver.swap(market_id, params); + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Run checks. - assert(amount_in == to_e18(5), 'Amount in'); + assert(approx_eq(amount_in, math::mul_div(to_e18(5), 10000, 9950, true), 1), 'Amount in'); assert(amount_out == params.amount, 'Amount out'); + assert(approx_eq(fees, math::mul_div(to_e18(5), 50, 9950, true), 1), 'Fees'); } #[test] @@ -141,7 +145,7 @@ fn test_swap_sell_exact_out() { // Deposit initial. start_prank(CheatTarget::One(solver.contract_address), owner()); - solver.deposit_initial(market_id, to_e18(5), to_e18(25)); + solver.deposit_initial(market_id, to_e18(50), to_e18(250)); // Swap buy. start_prank(CheatTarget::One(solver.contract_address), alice()); @@ -153,11 +157,12 @@ fn test_swap_sell_exact_out() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out) = solver.swap(market_id, params); + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Run checks. - assert(amount_in == to_e18(1), 'Amount in'); + assert(approx_eq(amount_in, math::mul_div(to_e18(1), 10000, 9950, true), 1), 'Amount in'); assert(amount_out == params.amount, 'Amount out'); + assert(approx_eq(fees, math::mul_div(to_e18(1), 50, 9950, true), 1), 'Fees'); } //////////////////////////////// @@ -188,7 +193,7 @@ fn test_swap_should_emit_event() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out) = solver.swap(market_id, params); + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Check events emitted. spy @@ -202,6 +207,7 @@ fn test_swap_should_emit_event() { caller: alice(), amount_in, amount_out, + fees, is_buy: params.is_buy, exact_input: params.exact_input } @@ -394,7 +400,7 @@ fn test_swap_fails_if_swap_sell_with_zero_liquidity() { } #[test] -#[should_panic(expected: ('ThresholdAmount', 10000000000000000000, 0))] +#[should_panic(expected: ('ThresholdAmount', 9950000000000000000, 0))] fn test_swap_fails_if_swap_buy_below_threshold_amount() { let (_base_token, _quote_token, _vault_token_class, solver, market_id, _vault_token_opt) = before( @@ -419,7 +425,7 @@ fn test_swap_fails_if_swap_buy_below_threshold_amount() { } #[test] -#[should_panic(expected: ('ThresholdAmount', 10000000000000000000, 0))] +#[should_panic(expected: ('ThresholdAmount', 9950000000000000000, 0))] fn test_swap_fails_if_swap_sell_below_threshold_amount() { let (_base_token, _quote_token, _vault_token_class, solver, market_id, _vault_token_opt) = before( diff --git a/packages/core/src/tests/solver/test_withdraw.cairo b/packages/core/src/tests/solver/test_withdraw.cairo index 7e8b536..203cd1a 100644 --- a/packages/core/src/tests/solver/test_withdraw.cairo +++ b/packages/core/src/tests/solver/test_withdraw.cairo @@ -8,7 +8,8 @@ use haiko_solver_core::{ ISolver::{ISolverDispatcher, ISolverDispatcherTrait}, IVaultToken::{IVaultTokenDispatcher, IVaultTokenDispatcherTrait}, }, - types::MarketInfo, tests::helpers::{actions::deploy_mock_solver, utils::{before, snapshot},}, + types::{MarketInfo, SwapParams}, + tests::helpers::{actions::deploy_mock_solver, utils::{before, snapshot},}, }; // Haiko imports. @@ -43,30 +44,46 @@ fn test_withdraw_partial_shares_from_public_vault() { start_prank(CheatTarget::One(solver.contract_address), alice()); let (_, _, shares) = solver.deposit(market_id, base_amount, quote_amount); + // Swap. + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); + // Snapshot before. let vault_token = vault_token_opt.unwrap(); let bef = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw) = solver.withdraw_public(market_id, shares / 2); + let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver + .withdraw_public(market_id, shares / 2); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(base_withdraw, base_amount / 2, 10), 'Base deposit'); - assert(approx_eq(quote_withdraw, quote_amount / 2, 10), 'Quote deposit'); + assert(approx_eq(base_withdraw, (base_amount * 2 + amount_in) / 4, 10), 'Base deposit'); + assert(approx_eq(quote_withdraw, (quote_amount * 2 - amount_out) / 4, 10), 'Quote deposit'); + assert(approx_eq(base_fees, fees / 4, 1), 'Base fees'); + assert(quote_fees == 0, 'Quote fees'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal - shares / 2, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal - shares / 2, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - base_withdraw, + aft.market_state.base_reserves == bef.market_state.base_reserves + - (base_withdraw - base_fees), 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - quote_withdraw, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + - (quote_withdraw - quote_fees), 'Quote reserve' ); } @@ -81,15 +98,26 @@ fn test_withdraw_remaining_shares_from_public_vault() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let (_, _, shares_owner) = solver.deposit_initial(market_id, base_amount, quote_amount); // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (_, _, shares) = solver.deposit(market_id, base_amount, quote_amount); + let (_, _, shares_alice) = solver.deposit(market_id, base_amount, quote_amount); + + // Swap. + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Withdraw owner. start_prank(CheatTarget::One(solver.contract_address), owner()); - solver.withdraw_public(market_id, shares_init); + solver.withdraw_public(market_id, shares_owner); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -97,24 +125,29 @@ fn test_withdraw_remaining_shares_from_public_vault() { // Withdraw LP. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw) = solver.withdraw_public(market_id, shares); + let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver + .withdraw_public(market_id, shares_alice); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(base_withdraw, base_amount, 10), 'Base deposit'); - assert(approx_eq(quote_withdraw, quote_amount, 10), 'Quote deposit'); + assert(approx_eq(base_withdraw, (base_amount * 2 + amount_in) / 2, 1), 'Base deposit'); + assert(approx_eq(quote_withdraw, (quote_amount * 2 - amount_out) / 2, 1), 'Quote deposit'); + assert(base_fees == fees / 2, 'Base fees'); + assert(quote_fees == 0, 'Quote fees'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal - shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal - shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal - shares_alice, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal - shares_alice, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - base_withdraw, + aft.market_state.base_reserves == bef.market_state.base_reserves + - (base_withdraw - base_fees), 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - quote_withdraw, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + - (quote_withdraw - quote_fees), 'Quote reserve' ); } @@ -158,20 +191,34 @@ fn test_withdraw_private_base_only() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, _, fees) = solver.swap(market_id, params); + // Snapshot before. let vault_token = contract_address_const::<0x0>(); let bef = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver.withdraw_private(market_id, base_amount, 0); + let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver + .withdraw_private(market_id, base_amount + amount_in, 0); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount, 10), 'Base withdraw'); + assert(approx_eq(base_withdraw, base_amount + amount_in, 1), 'Base withdraw'); assert(quote_withdraw == 0, 'Quote withdraw'); + assert(approx_eq(base_fees, fees, 1), 'Base fees'); + assert(quote_fees == 0, 'Quote fees'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); @@ -191,20 +238,34 @@ fn test_withdraw_private_quote_only() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (_, amount_out, _) = solver.swap(market_id, params); + // Snapshot before. let vault_token = contract_address_const::<0x0>(); let bef = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver.withdraw_private(market_id, 0, quote_amount); + let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver + .withdraw_private(market_id, 0, quote_amount); // will be capped at available // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. assert(base_withdraw == 0, 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount, 10), 'Quote withdraw'); + assert(approx_eq(quote_withdraw, quote_amount - amount_out, 1), 'Quote withdraw'); + assert(base_fees == 0, 'Base fees'); + assert(quote_fees == 0, 'Quote fees'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); @@ -224,31 +285,48 @@ fn test_withdraw_private_partial_amounts() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); + // Snapshot before. let vault_token = contract_address_const::<0x0>(); let bef = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver - .withdraw_private(market_id, base_amount / 2, quote_amount / 2); + let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver + .withdraw_private( + market_id, (base_amount + amount_in) / 2, (quote_amount - amount_out) / 2 + ); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount / 2, 10), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount / 2, 10), 'Quote withdraw'); + assert(approx_eq(base_withdraw, (base_amount + amount_in) / 2, 1), 'Base withdraw'); + assert(approx_eq(quote_withdraw, (quote_amount - amount_out) / 2, 1), 'Quote withdraw'); + assert(approx_eq(base_fees, fees / 2, 1), 'Base fees'); + assert(quote_fees == 0, 'Quote fees'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - base_withdraw, + aft.market_state.base_reserves == bef.market_state.base_reserves + - (base_withdraw - base_fees), 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - quote_withdraw, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + - (quote_withdraw - quote_fees), 'Quote reserve' ); } @@ -266,31 +344,46 @@ fn test_withdraw_all_remaining_balances_from_private_vault() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); + // Snapshot before. let vault_token = contract_address_const::<0x0>(); let bef = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver - .withdraw_private(market_id, base_amount, quote_amount); + let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver + .withdraw_private(market_id, base_amount + amount_in, quote_amount - amount_out); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount, 10), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount, 10), 'Quote withdraw'); + assert(approx_eq(base_withdraw, base_amount + amount_in, 1), 'Base withdraw'); + assert(approx_eq(quote_withdraw, quote_amount - amount_out, 1), 'Quote withdraw'); + assert(base_fees == fees, 'Base fees'); + assert(quote_fees == 0, 'Quote fees'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == 0, 'LP shares'); assert(aft.vault_total_bal == 0, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - base_withdraw, + aft.market_state.base_reserves == bef.market_state.base_reserves + - (base_withdraw - base_fees), 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - quote_withdraw, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + - (quote_withdraw - quote_fees), 'Quote reserve' ); } @@ -308,14 +401,27 @@ fn test_withdraw_more_than_available_correctly_caps_amount_for_private_vault() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); + // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver + let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver .withdraw_private(market_id, base_amount * 2, quote_amount * 2); // Run checks. - assert(approx_eq(base_withdraw, base_amount, 10), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount, 10), 'Quote withdraw'); + assert(approx_eq(base_withdraw, base_amount + amount_in, 1), 'Base withdraw'); + assert(approx_eq(quote_withdraw, quote_amount - amount_out, 1), 'Quote withdraw'); + assert(base_fees == fees, 'Base fees'); + assert(quote_fees == 0, 'Quote fees'); } //////////////////////////////// @@ -339,7 +445,8 @@ fn test_withdraw_public_emits_event() { let mut spy = spy_events(SpyOn::One(solver.contract_address)); // Withdraw. - let (base_withdraw, quote_withdraw) = solver.withdraw_public(market_id, shares); + let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver + .withdraw_public(market_id, shares); // Check events emitted. spy @@ -353,7 +460,9 @@ fn test_withdraw_public_emits_event() { caller: owner(), base_amount: base_withdraw, quote_amount: quote_withdraw, - shares + base_fees, + quote_fees, + shares, } ) ) @@ -378,7 +487,7 @@ fn test_withdraw_private_emits_event() { let mut spy = spy_events(SpyOn::One(solver.contract_address)); // Withdraw. - let (base_withdraw, quote_withdraw) = solver + let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver .withdraw_private(market_id, base_amount, quote_amount); // Check events emitted. @@ -393,7 +502,9 @@ fn test_withdraw_private_emits_event() { caller: owner(), base_amount: base_withdraw, quote_amount: quote_withdraw, - shares + base_fees, + quote_fees, + shares, } ) ) diff --git a/packages/core/src/types.cairo b/packages/core/src/types.cairo index 57ce54c..ac3360e 100644 --- a/packages/core/src/types.cairo +++ b/packages/core/src/types.cairo @@ -23,12 +23,16 @@ pub struct MarketInfo { // // * `base_reserves` - base reserves // * `quote_reserves` - quote reserves +// * `base_fees` - base fees +// * `quote_fees` - quote fees // * `is_paused` - whether market is paused // * `vault_token` - vault token (or 0 if unset) #[derive(Drop, Copy, Serde)] pub struct MarketState { pub base_reserves: u256, pub quote_reserves: u256, + pub base_fees: u256, + pub quote_fees: u256, pub is_paused: bool, pub vault_token: ContractAddress, } @@ -72,6 +76,8 @@ pub impl DefaultMarketState of Default { MarketState { base_reserves: 0, quote_reserves: 0, + base_fees: 0, + quote_fees: 0, is_paused: false, vault_token: contract_address_const::<0x0>(), } @@ -86,12 +92,16 @@ pub impl DefaultMarketState of Default { // // * `slab0` - base reserves (coerced to felt252) // * `slab1` - quote reserves (coerced to felt252) -// * `slab2` - vault_token -// * `slab3` - is_paused +// * `slab2` - base fees (coerced to felt252) +// * `slab3` - quote fees (coerced to felt252) +// * `slab4` - vault_token +// * `slab5` - is_paused #[derive(starknet::Store)] pub struct PackedMarketState { pub slab0: felt252, pub slab1: felt252, pub slab2: felt252, - pub slab3: felt252 + pub slab3: felt252, + pub slab4: felt252, + pub slab5: felt252, } diff --git a/packages/replicating/src/contracts/replicating_solver.cairo b/packages/replicating/src/contracts/replicating_solver.cairo index ed76b10..f025850 100644 --- a/packages/replicating/src/contracts/replicating_solver.cairo +++ b/packages/replicating/src/contracts/replicating_solver.cairo @@ -80,7 +80,7 @@ pub mod ReplicatingSolver { pub(crate) struct QueueMarketParams { #[key] pub market_id: felt252, - pub min_spread: u32, + pub fee_rate: u16, pub range: u32, pub max_delta: u32, pub max_skew: u16, @@ -94,7 +94,7 @@ pub mod ReplicatingSolver { 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, @@ -143,11 +143,12 @@ pub mod ReplicatingSolver { // * `swap_params` - swap parameters // // # Returns - // * `amount_in` - amount in + // * `amount_in` - amount in including fees // * `amount_out` - amount out + // * `fees` - fees fn quote( self: @ContractState, market_id: felt252, swap_params: SwapParams, - ) -> (u256, u256) { + ) -> (u256, u256, u256) { // Run validity checks. let state: MarketState = self.solver.market_state.read(market_id); let market_info: MarketInfo = self.solver.market_info.read(market_id); @@ -162,8 +163,11 @@ pub mod ReplicatingSolver { bid }; - // Calculate swap amount. - let (amount_in, amount_out) = swap_lib::get_swap_amounts(swap_params, position); + // Calculate swap amount. + let params = self.market_params.read(market_id); + let (amount_in, amount_out, fees) = swap_lib::get_swap_amounts( + swap_params, params.fee_rate, position + ); // Fetch oracle price. let (oracle_price, is_valid) = self.get_oracle_price(market_id); @@ -172,11 +176,10 @@ pub mod ReplicatingSolver { // Check deposited amounts does not violate max skew, or if it does, that // the deposit reduces the extent of skew. let (base_reserves, quote_reserves) = if swap_params.is_buy { - (state.base_reserves - amount_out, state.quote_reserves + amount_in) + (state.base_reserves - amount_out, state.quote_reserves + amount_in - fees) } else { - (state.base_reserves + amount_in, state.quote_reserves - amount_out) + (state.base_reserves + amount_in - fees, state.quote_reserves - amount_out) }; - let params = self.market_params.read(market_id); if params.max_skew != 0 { let (skew_before, _) = spread_math::get_skew( state.base_reserves, state.quote_reserves, oracle_price @@ -190,7 +193,7 @@ pub mod ReplicatingSolver { } // Return amounts. - (amount_in, amount_out) + (amount_in, amount_out, fees) } // Callback function to execute any state updates after a swap is completed. @@ -310,7 +313,7 @@ pub mod ReplicatingSolver { Event::QueueMarketParams( QueueMarketParams { market_id, - min_spread: params.min_spread, + fee_rate: params.fee_rate, range: params.range, max_delta: params.max_delta, max_skew: params.max_skew, @@ -363,7 +366,7 @@ pub mod ReplicatingSolver { Event::SetMarketParams( SetMarketParams { market_id, - min_spread: queued_params.min_spread, + fee_rate: queued_params.fee_rate, range: queued_params.range, max_delta: queued_params.max_delta, max_skew: queued_params.max_skew, @@ -431,10 +434,10 @@ pub mod ReplicatingSolver { params.max_delta, state.base_reserves, state.quote_reserves, oracle_price ); let (bid_lower, bid_upper) = spread_math::get_virtual_position_range( - true, params.min_spread, delta, params.range, oracle_price + true, delta, params.range, oracle_price ); let (ask_lower, ask_upper) = spread_math::get_virtual_position_range( - false, params.min_spread, delta, params.range, oracle_price + false, delta, params.range, oracle_price ); // Calculate and return positions. diff --git a/packages/replicating/src/libraries/spread_math.cairo b/packages/replicating/src/libraries/spread_math.cairo index afcacb8..32acfb4 100644 --- a/packages/replicating/src/libraries/spread_math.cairo +++ b/packages/replicating/src/libraries/spread_math.cairo @@ -25,10 +25,8 @@ pub const DENOMINATOR: u256 = 10000; // // # Arguments // `is_bid` - whether to calculate bid or ask position -// `min_spread` - minimum spread to apply -// `delta` - inventory delta (+ve if ask spread, -ve if bid spread) -// `range` - position range -// `oracle_price` - current oracle price (base 10e28) +// `lower_limit` - virtual position lower limit +// `upper_limit` - virtual position upper limit // `amount` - token amount in reserve // // # Returns @@ -55,7 +53,6 @@ pub fn get_virtual_position( // // # Arguments // `is_bid` - whether to calculate bid or ask position -// `min_spread` - minimum spread to apply // `delta` - inventory delta (+ve if ask spread, -ve if bid spread) // note `delta` uses a custom i32 implementation that is [-4294967295, 4294967295] // `range` - position range @@ -65,20 +62,12 @@ pub fn get_virtual_position( // `lower_limit` - virtual position lower limit // `upper_limit` - virtual position upper limit pub fn get_virtual_position_range( - is_bid: bool, min_spread: u32, delta: i32, range: u32, oracle_price: u256 + is_bid: bool, delta: i32, range: u32, oracle_price: u256 ) -> (u32, u32) { // Start with the oracle price, and convert it to limits. assert(oracle_price != 0, 'OraclePriceZero'); let mut limit = price_math::price_to_limit(oracle_price, 1, !is_bid); - // Apply minimum spread. - if is_bid { - assert(limit >= min_spread, 'LimitUF'); - limit -= min_spread; - } else { - limit += min_spread; - } - // Apply delta. if delta.sign { assert(limit >= delta.val, 'LimitUF'); diff --git a/packages/replicating/src/libraries/store_packing.cairo b/packages/replicating/src/libraries/store_packing.cairo index aa0b316..a6d8eb9 100644 --- a/packages/replicating/src/libraries/store_packing.cairo +++ b/packages/replicating/src/libraries/store_packing.cairo @@ -25,7 +25,7 @@ const MASK_64: u256 = 0xffffffffffffffff; pub(crate) impl MarketParamsStorePacking of StorePacking { fn pack(value: MarketParams) -> PackedMarketParams { - let mut slab2: u256 = value.min_spread.into(); + let mut slab2: u256 = value.fee_rate.into(); slab2 += value.range.into() * TWO_POW_32.into(); slab2 += value.max_delta.into() * TWO_POW_64.into(); slab2 += value.max_skew.into() * TWO_POW_96.into(); @@ -41,7 +41,7 @@ pub(crate) impl MarketParamsStorePacking of StorePacking MarketParams { let slab2: u256 = value.slab2.into(); - let min_spread: u32 = (slab2 & MASK_32).try_into().unwrap(); + let fee_rate: u16 = (slab2 & MASK_16).try_into().unwrap(); let range: u32 = ((slab2 / TWO_POW_32.into()) & MASK_32).try_into().unwrap(); let max_delta: u32 = ((slab2 / TWO_POW_64.into()) & MASK_32).try_into().unwrap(); let max_skew: u16 = ((slab2 / TWO_POW_96.into()) & MASK_16).try_into().unwrap(); @@ -49,7 +49,7 @@ pub(crate) impl MarketParamsStorePacking of StorePacking (u256, u256) { +// `fees` - amount of fees +pub fn get_swap_amounts( + swap_params: SwapParams, fee_rate: u16, position: PositionInfo, +) -> (u256, u256, u256) { // Define start and target prices based on swap direction. let start_sqrt_price = if swap_params.is_buy { position.lower_sqrt_price @@ -46,15 +49,16 @@ pub fn get_swap_amounts(swap_params: SwapParams, position: PositionInfo,) -> (u2 }; // Compute swap amounts. - let (amount_in, amount_out, _) = compute_swap_amounts( + let (amount_in, amount_out, fees, _) = compute_swap_amounts( start_sqrt_price, target_sqrt_price, position.liquidity, swap_params.amount, + fee_rate, swap_params.exact_input, ); - (amount_in, amount_out) + (amount_in + fees, amount_out, fees) } // Compute amounts swapped and new price after swapping between two prices. @@ -64,19 +68,22 @@ pub fn get_swap_amounts(swap_params: SwapParams, position: PositionInfo,) -> (u2 // * `target_sqrt_price` - target sqrt price // * `liquidity` - current liquidity // * `amount` - amount remaining to be swapped +// * `fee_rate` - fee rate // * `exact_input` - whether swap amount is exact input or output // // # Returns // * `amount_in` - amount of tokens swapped in // * `amount_out` - amount of tokens swapped out +// * `fee_amount` - amount of fees // * `next_sqrt_price` - next sqrt price pub fn compute_swap_amounts( curr_sqrt_price: u256, target_sqrt_price: u256, liquidity: u128, amount: u256, + fee_rate: u16, exact_input: bool, -) -> (u256, u256, u256) { +) -> (u256, u256, u256, u256) { // Determine whether swap is a buy or sell. let is_buy = target_sqrt_price > curr_sqrt_price; @@ -100,8 +107,9 @@ pub fn compute_swap_amounts( .val; // Calculate next sqrt price. + let amount_less_fee = fee_math::gross_to_net(amount, fee_rate); let filled_max = if exact_input { - amount < amount_in + amount_less_fee < amount_in } else { amount < amount_out }; @@ -110,7 +118,7 @@ pub fn compute_swap_amounts( target_sqrt_price } else { if exact_input { - next_sqrt_price_input(curr_sqrt_price, liquidity, amount, is_buy) + next_sqrt_price_input(curr_sqrt_price, liquidity, amount_less_fee, is_buy) } else { next_sqrt_price_output(curr_sqrt_price, liquidity, amount, is_buy) } @@ -122,7 +130,7 @@ pub fn compute_swap_amounts( if filled_max { amount_in = if exact_input { - amount + amount_less_fee } else { if is_buy { liquidity_math::liquidity_to_quote( @@ -152,8 +160,15 @@ pub fn compute_swap_amounts( }; } + // Calculate fees. + // Amount in is net of fees because we capped amounts by net amount remaining. + // Fees are rounded down by default to prevent overflow when transferring amounts. + // Note that in Uniswap, if target price is not reached, LP takes the remainder + // of the maximum input as fee. We don't do that here. + let fees = fee_math::net_to_fee(amount_in, fee_rate); + // Return amounts. - (amount_in, amount_out, next_sqrt_price) + (amount_in, amount_out, fees, next_sqrt_price) } // Calculates next sqrt price after swapping in certain amount of tokens at given starting diff --git a/packages/replicating/src/tests/helpers/params.cairo b/packages/replicating/src/tests/helpers/params.cairo index 234c524..08280f3 100644 --- a/packages/replicating/src/tests/helpers/params.cairo +++ b/packages/replicating/src/tests/helpers/params.cairo @@ -3,7 +3,7 @@ use haiko_solver_replicating::types::MarketParams; pub fn default_market_params() -> MarketParams { MarketParams { - min_spread: 50, + fee_rate: 50, range: 1000, max_delta: 500, max_skew: 7500, diff --git a/packages/replicating/src/tests/libraries/test_spread_math.cairo b/packages/replicating/src/tests/libraries/test_spread_math.cairo index 98945b3..c0ab328 100644 --- a/packages/replicating/src/tests/libraries/test_spread_math.cairo +++ b/packages/replicating/src/tests/libraries/test_spread_math.cairo @@ -30,7 +30,6 @@ struct PositionTestCase { #[derive(Drop, Copy)] struct PositionRangeTestCase { - min_spread: u32, delta: i32, range: u32, oracle_price: u256, @@ -188,9 +187,8 @@ fn test_get_virtual_position_cases() { fn test_get_virtual_position_range_cases() { // Define test cases. let cases: Span = array![ - // Case 1: No min spread, no delta, range 1 + // Case 1: No delta, range 1 PositionRangeTestCase { - min_spread: 0, delta: I32Trait::new(0, false), range: 1, oracle_price: to_e28(1), @@ -199,126 +197,95 @@ fn test_get_virtual_position_range_cases() { ask_lower_exp: 7906625, ask_upper_exp: 7906626, }, - // Case 2: 500 min spread, no delta, range 1000 + // Case 2: No delta, range 1000 PositionRangeTestCase { - min_spread: 500, delta: I32Trait::new(0, false), range: 1000, oracle_price: to_e28(1), - bid_lower_exp: 7905125, - bid_upper_exp: 7906125, - ask_lower_exp: 7907125, - ask_upper_exp: 7908125, - }, - // Case 3: 100000 min spread, no delta, range 1000 - PositionRangeTestCase { - min_spread: 100000, - delta: I32Trait::new(0, false), - range: 1000, - oracle_price: to_e28(1), - bid_lower_exp: 7805625, - bid_upper_exp: 7806625, - ask_lower_exp: 8006625, - ask_upper_exp: 8007625, - }, - // Case 4: Max min spread, no delta, range 1000 - PositionRangeTestCase { - min_spread: 7905625, - delta: I32Trait::new(0, false), - range: 1000, - oracle_price: to_e28(1), - bid_lower_exp: 0, - bid_upper_exp: 1000, - ask_lower_exp: MAX_LIMIT_SHIFTED - 1000, - ask_upper_exp: MAX_LIMIT_SHIFTED, + bid_lower_exp: 7905625, + bid_upper_exp: 7906625, + ask_lower_exp: 7906625, + ask_upper_exp: 7907625, }, - // Case 5: 500 min spread, 100 bid delta, range 1000 + // Case 3: 100 bid delta, range 1000 PositionRangeTestCase { - min_spread: 500, delta: I32Trait::new(100, true), range: 1000, oracle_price: to_e28(1), - bid_lower_exp: 7905025, - bid_upper_exp: 7906025, - ask_lower_exp: 7907025, - ask_upper_exp: 7908025, + bid_lower_exp: 7905525, + bid_upper_exp: 7906525, + ask_lower_exp: 7906525, + ask_upper_exp: 7907525, }, - // Case 6: 500 min spread, 100 ask delta, range 1000 + // Case 4: 100 ask delta, range 1000 PositionRangeTestCase { - min_spread: 500, delta: I32Trait::new(100, false), range: 1000, oracle_price: to_e28(1), - bid_lower_exp: 7905225, - bid_upper_exp: 7906225, - ask_lower_exp: 7907225, - ask_upper_exp: 7908225, + bid_lower_exp: 7905725, + bid_upper_exp: 7906725, + ask_lower_exp: 7906725, + ask_upper_exp: 7907725, }, - // Case 7: 500 min spread, 5000 bid delta, range 1000 + // Case 5: 5000 bid delta, range 1000 PositionRangeTestCase { - min_spread: 500, delta: I32Trait::new(5000, true), range: 1000, oracle_price: to_e28(1), - bid_lower_exp: 7900125, - bid_upper_exp: 7901125, - ask_lower_exp: 7902125, - ask_upper_exp: 7903125, + bid_lower_exp: 7900625, + bid_upper_exp: 7901625, + ask_lower_exp: 7901625, + ask_upper_exp: 7902625, }, - // Case 8: 500 min spread, 5000 ask delta, range 1000 + // Case 6: 5000 ask delta, range 1000 PositionRangeTestCase { - min_spread: 500, delta: I32Trait::new(5000, false), range: 1000, oracle_price: to_e28(1), - bid_lower_exp: 7910125, - bid_upper_exp: 7911125, - ask_lower_exp: 7912125, - ask_upper_exp: 7913125, + bid_lower_exp: 7910625, + bid_upper_exp: 7911625, + ask_lower_exp: 7911625, + ask_upper_exp: 7912625, }, - // Case 9: 500 min spread, max bid delta (7905125), range 1000 + // Case 7: max bid delta (7905625), range 1000 PositionRangeTestCase { - min_spread: 500, - delta: I32Trait::new(7905125, true), + delta: I32Trait::new(7905625, true), range: 1000, oracle_price: to_e28(1), bid_lower_exp: 0, bid_upper_exp: 1000, - ask_lower_exp: 2000, - ask_upper_exp: 3000, + ask_lower_exp: 1000, + ask_upper_exp: 2000, }, - // Case 10: 500 min spread, max ask delta (7905125), range 1000 + // Case 8: Max ask delta (7905625), range 1000 PositionRangeTestCase { - min_spread: 500, - delta: I32Trait::new(7905125, false), + delta: I32Trait::new(7905625, false), range: 1000, oracle_price: to_e28(1), - bid_lower_exp: MAX_LIMIT_SHIFTED - 3000, - bid_upper_exp: MAX_LIMIT_SHIFTED - 2000, + bid_lower_exp: MAX_LIMIT_SHIFTED - 2000, + bid_upper_exp: MAX_LIMIT_SHIFTED - 1000, ask_lower_exp: MAX_LIMIT_SHIFTED - 1000, ask_upper_exp: MAX_LIMIT_SHIFTED, }, - // Case 11: 500 min spread, no delta, range 1000, very small oracle price + // Case 9: no delta, range 1000, very small oracle price PositionRangeTestCase { - min_spread: 500, delta: I32Trait::new(0, false), range: 1000, oracle_price: 1, // limit: 1459354 - bid_lower_exp: 1457854, - bid_upper_exp: 1458854, - ask_lower_exp: 1459855, - ask_upper_exp: 1460855, + bid_lower_exp: 1458354, + bid_upper_exp: 1459354, + ask_lower_exp: 1459355, + ask_upper_exp: 1460355, }, - // Case 12: 500 min spread, no delta, range 1000, very large oracle price + // Case 10: no delta, range 1000, very large oracle price PositionRangeTestCase { - min_spread: 500, delta: I32Trait::new(0, false), range: 1000, oracle_price: 214459684708337062817548134114224826295263805072231182393896500, // limit: 7905125 (roundup) - bid_lower_exp: MAX_LIMIT_SHIFTED - 3000, - bid_upper_exp: MAX_LIMIT_SHIFTED - 2000, - ask_lower_exp: MAX_LIMIT_SHIFTED - 1000, - ask_upper_exp: MAX_LIMIT_SHIFTED, + bid_lower_exp: MAX_LIMIT_SHIFTED - 2501, + bid_upper_exp: MAX_LIMIT_SHIFTED - 1501, + ask_lower_exp: MAX_LIMIT_SHIFTED - 1500, + ask_upper_exp: MAX_LIMIT_SHIFTED - 500, }, ] .span(); @@ -331,10 +298,10 @@ fn test_get_virtual_position_range_cases() { } let case = *cases.at(i); let (bid_lower, bid_upper) = get_virtual_position_range( - true, case.min_spread, case.delta, case.range, case.oracle_price + true, case.delta, case.range, case.oracle_price ); let (ask_lower, ask_upper) = get_virtual_position_range( - false, case.min_spread, case.delta, case.range, case.oracle_price + false, case.delta, case.range, case.oracle_price ); if (!approx_eq(bid_lower.into(), case.bid_lower_exp.into(), 1)) { panic!("Bid lower {}: {} (act), {} (exp)", i + 1, bid_lower, case.bid_lower_exp); @@ -355,7 +322,7 @@ fn test_get_virtual_position_range_cases() { #[test] #[should_panic(expected: ('LimitUF',))] fn test_get_virtual_position_range_bid_limit_underflow() { - get_virtual_position_range(true, 0, I32Trait::new(0, false), 2000000, 1); + get_virtual_position_range(true, I32Trait::new(0, false), 2000000, 1); } #[test] @@ -363,7 +330,6 @@ fn test_get_virtual_position_range_bid_limit_underflow() { fn test_get_virtual_position_range_ask_limit_overflow() { get_virtual_position_range( false, - 10, I32Trait::new(0, false), 0, 217702988461462792141570404997617821806367875652638254199700027 @@ -373,7 +339,7 @@ fn test_get_virtual_position_range_ask_limit_overflow() { #[test] #[should_panic(expected: ('OraclePriceZero',))] fn test_get_virtual_position_range_oracle_price_zero() { - get_virtual_position_range(true, 0, I32Trait::new(0, false), 0, 0); + get_virtual_position_range(true, I32Trait::new(0, false), 0, 0); } #[test] @@ -381,7 +347,6 @@ fn test_get_virtual_position_range_oracle_price_zero() { fn test_get_virtual_position_range_oracle_price_overflow() { get_virtual_position_range( true, - 0, I32Trait::new(0, false), 0, 217713873828591030783410061480731509152483726437928216295905367 diff --git a/packages/replicating/src/tests/libraries/test_store_packing.cairo b/packages/replicating/src/tests/libraries/test_store_packing.cairo index aab8b30..b0eeb1f 100644 --- a/packages/replicating/src/tests/libraries/test_store_packing.cairo +++ b/packages/replicating/src/tests/libraries/test_store_packing.cairo @@ -31,7 +31,7 @@ fn test_store_packing_market_params() { let store_packing_contract = before(); let market_params = MarketParams { - min_spread: 15, + fee_rate: 15, range: 15000, max_delta: 2532, max_skew: 8888, @@ -44,7 +44,7 @@ fn test_store_packing_market_params() { store_packing_contract.set_market_params(1, market_params); let unpacked = store_packing_contract.get_market_params(1); - assert(unpacked.min_spread == market_params.min_spread, 'Market params: min spread'); + assert(unpacked.fee_rate == market_params.fee_rate, 'Market params: fee rate'); assert(unpacked.range == market_params.range, 'Market params: range'); assert(unpacked.max_delta == market_params.max_delta, 'Market params: max delta'); assert(unpacked.max_skew == market_params.max_skew, 'Market params: max skew'); diff --git a/packages/replicating/src/tests/libraries/test_swap_lib.cairo b/packages/replicating/src/tests/libraries/test_swap_lib.cairo index 435d93d..8e1c8e9 100644 --- a/packages/replicating/src/tests/libraries/test_swap_lib.cairo +++ b/packages/replicating/src/tests/libraries/test_swap_lib.cairo @@ -33,9 +33,11 @@ fn test_get_swap_amounts_succeeds() { upper_sqrt_price: encode_sqrt_price(1, 1), liquidity: to_e18_u128(10000), }; - let (amount_in, amount_out) = get_swap_amounts(swap_params, position); - assert(approx_eq(amount_in, 1000000000000000000, 1000), 'Swap amts: amt in'); - assert(approx_eq(amount_out, 1249860261374659470, 1000), 'Swap amts: amt out'); + let swap_fee_rate = 50; + let (amount_in, amount_out, fees) = get_swap_amounts(swap_params, swap_fee_rate, position); + assert(approx_eq(amount_in, to_e18(1), 1000), 'Swap amts: amt in'); + assert(approx_eq(amount_out, 1243611655190118787, 1000), 'Swap amts: amt out'); + assert(approx_eq(fees, 5000000000000000, 1000), 'Swap amts: fees'); } #[test] @@ -50,11 +52,12 @@ fn test_get_swap_amounts_over_zero_liquidity() { }; let position = PositionInfo { lower_sqrt_price: encode_sqrt_price(1, 1), - upper_sqrt_price: encode_sqrt_price(10, 10), + upper_sqrt_price: encode_sqrt_price(12, 10), liquidity: 0, }; - let (amount_in, amount_out) = get_swap_amounts(swap_params, position); - assert(amount_in == 0 && amount_out == 0, 'Swap amts: 0 liq'); + let swap_fee_rate = 50; + let (amount_in, amount_out, fees) = get_swap_amounts(swap_params, swap_fee_rate, position); + assert(amount_in == 0 && amount_out == 0 && fees == 0, 'Swap amts: 0 liq'); } #[test] @@ -72,9 +75,10 @@ fn test_get_swap_amounts_bid_threshold_sqrt_price() { upper_sqrt_price: encode_sqrt_price(1, 1), liquidity: to_e18_u128(200), }; - let (amount_in, amount_out) = get_swap_amounts(swap_params, position); + let swap_fee_rate = 50; + let (amount_in, amount_out, fees) = get_swap_amounts(swap_params, swap_fee_rate, position); assert( - amount_in < 10000000000000000000 && amount_out < 9523809523809523809, + amount_in < to_e18(10) && amount_out < 9478447249345082162 && fees < 50000000000000000, 'Swap amts: bid threshold' ); } @@ -94,9 +98,10 @@ fn test_get_swap_amounts_ask_threshold_sqrt_price() { upper_sqrt_price: encode_sqrt_price(12, 10), liquidity: to_e18_u128(200), }; - let (amount_in, amount_out) = get_swap_amounts(swap_params, position); + let swap_fee_rate = 50; + let (amount_in, amount_out, fees) = get_swap_amounts(swap_params, swap_fee_rate, position); assert( - amount_in < 10000000000000000000 && amount_out < 9523809523809523809, + amount_in < to_e18(10) && amount_out < 9478447249345082162 && fees < 50000000000000000, 'Swap amts: ask threshold' ); } @@ -111,9 +116,10 @@ fn test_compute_swap_amounts_buy_exact_input_reaches_price_target() { let target_sqrt_price = encode_sqrt_price(101, 100); let liquidity = to_e28_u128(2); let amount_rem = to_e28(1); + let fee_rate = 6; // 0.06% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, true + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, true ); let next_sqrt_price_full_amount = next_sqrt_price_input( curr_sqrt_price, liquidity, amount_rem, true @@ -121,10 +127,11 @@ fn test_compute_swap_amounts_buy_exact_input_reaches_price_target() { assert(amount_in == 99751242241780540438529824, 'comp_swap buy/in/cap in'); assert(amount_out == 99256195800217286694524923, 'comp_swap buy/in/cap out'); + assert(fees == 59886677351479211790192, 'comp_swap buy/in/cap fees'); assert(next_sqrt_price == 10049875621120890270219264912, 'comp_swap buy/in/cap price'); assert(next_sqrt_price == target_sqrt_price, 'comp_swap buy/in/cap target P'); assert(next_sqrt_price < next_sqrt_price_full_amount, 'comp_swap buy/in/cap target Q'); - assert(amount_rem > amount_in, 'comp_swap buy/in/cap amount_rem'); + assert(amount_rem > amount_in + fees, 'comp_swap buy/in/cap amount_rem'); } #[test] @@ -133,9 +140,10 @@ fn test_compute_swap_amounts_buy_exact_output_reaches_price_target() { let target_sqrt_price = encode_sqrt_price(101, 100); let liquidity = to_e28_u128(2); let amount_rem = to_e28(1); + let fee_rate = 6; // 0.06% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, false + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, false ); let next_sqrt_price_full_amount = next_sqrt_price_output( curr_sqrt_price, liquidity, amount_rem, true @@ -143,6 +151,7 @@ fn test_compute_swap_amounts_buy_exact_output_reaches_price_target() { assert(amount_in == 99751242241780540438529824, 'comp_swap buy/out/cap in'); assert(amount_out == 99256195800217286694524923, 'comp_swap buy/out/cap out'); + assert(fees == 59886677351479211790192, 'comp_swap buy/out/cap fees'); assert(next_sqrt_price == 10049875621120890270219264912, 'comp_swap buy/in/cap price'); assert(next_sqrt_price == target_sqrt_price, 'comp_swap buy/out/cap target P'); assert(next_sqrt_price < next_sqrt_price_full_amount, 'comp_swap buy/out/cap target Q'); @@ -155,19 +164,23 @@ fn test_compute_swap_amounts_buy_exact_input_filled_max() { let target_sqrt_price = encode_sqrt_price(1000, 100); let liquidity = to_e28_u128(2); let amount_rem = to_e28(1); + let fee_rate = 6; // 0.06% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, true + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, true ); + let net_amount_rem = gross_to_net(amount_rem, fee_rate); let next_sqrt_price_full_amount = next_sqrt_price_input( - curr_sqrt_price, liquidity, amount_rem, true + curr_sqrt_price, liquidity, net_amount_rem, true ); - assert(amount_in == 10000000000000000000000000000, 'comp_swap buy/in/full in'); - assert(amount_out == 6666666666666666666666666666, 'comp_swap buy/in/full out'); - assert(next_sqrt_price == 15000000000000000000000000000, 'comp_swap buy/in/cap price'); + assert(amount_in == 9994000000000000000000000000, 'comp_swap buy/in/full in'); + assert(amount_out == 6663999466559978662399146495, 'comp_swap buy/in/full out'); + assert(fees == 6000000000000000000000000, 'comp_swap buy/in/full fees'); + assert(next_sqrt_price == 14997000000000000000000000000, 'comp_swap buy/in/cap price'); assert(next_sqrt_price < target_sqrt_price, 'comp_swap buy/in/full target P'); assert(next_sqrt_price == next_sqrt_price_full_amount, 'comp_swap buy/in/full target Q'); + assert(amount_rem == amount_in + fees, 'comp_swap buy/in/full rem'); } #[test] @@ -176,9 +189,10 @@ fn test_compute_swap_amounts_buy_exact_output_filled_max() { let target_sqrt_price = encode_sqrt_price(10000, 100); let liquidity = to_e28_u128(2); let amount_rem = to_e28(1); + let fee_rate = 6; // 0.06% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, false + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, false ); let next_sqrt_price_full_amount = next_sqrt_price_output( curr_sqrt_price, liquidity, amount_rem, true @@ -186,6 +200,7 @@ fn test_compute_swap_amounts_buy_exact_output_filled_max() { assert(amount_in == 20000000000000000000000000000, 'comp_swap buy/out/full in'); assert(amount_out == 10000000000000000000000000000, 'comp_swap buy/out/full out'); + assert(fees == 12007204322593556133680208, 'comp_swap buy/out/full fees'); assert(next_sqrt_price == 20000000000000000000000000000, 'comp_swap buy/out/full price'); assert(next_sqrt_price < target_sqrt_price, 'comp_swap buy/out/full target P'); assert(next_sqrt_price == next_sqrt_price_full_amount, 'comp_swap buy/out/full target Q'); @@ -198,12 +213,14 @@ fn test_compute_swap_amounts_sell_exact_input_reached_price_target() { let target_sqrt_price = to_e28(1); let liquidity = to_e28_u128(2); let amount_rem = to_e28(1); + let fee_rate = 6; // 0.06% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, true + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, true ); assert(amount_in == 6666666666666666666666666667, 'comp_swap sell/in/cap in'); assert(amount_out == 10000000000000000000000000000, 'comp_swap sell/in/cap out'); + assert(fees == 4002401440864518711226736, 'comp_swap sell/in/cap fees'); assert(next_sqrt_price == 10000000000000000000000000000, 'comp_swap sell/in/cap price'); } @@ -213,12 +230,14 @@ fn test_compute_swap_amounts_sell_exact_output_reached_price_target() { let target_sqrt_price = to_e28(1); let liquidity = to_e28_u128(2); let amount_rem = to_e28(1); + let fee_rate = 6; // 0.06% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, false + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, false ); assert(amount_in == 3333333333333333333333333334, 'comp_swap sell/out/cap in'); assert(amount_out == 4000000000000000000000000000, 'comp_swap sell/out/cap out'); + assert(fees == 2001200720432259355613368, 'comp_swap sell/out/cap fees'); assert(next_sqrt_price == 10000000000000000000000000000, 'comp_swap sell/out/cap price'); } @@ -228,13 +247,15 @@ fn test_compute_swap_amounts_sell_exact_input_filled_max() { let target_sqrt_price = encode_sqrt_price(1, 1); let liquidity = to_e28_u128(2); let amount_rem = to_e28(1); + let fee_rate = 6; // 0.06% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, true + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, true ); - assert(amount_in == 10000000000000000000000000000, 'comp_swap sell/in/full in'); - assert(amount_out == 38742588672279311066629784812, 'comp_swap sell/in/full out'); - assert(next_sqrt_price == 12251482265544137786674043038, 'comp_swap sell/in/full price'); + assert(amount_in == 9994000000000000000000000000, 'comp_swap sell/in/full in'); + assert(amount_out == 38733579431920680121737214444, 'comp_swap sell/in/full out'); + assert(fees == 6000000000000000000000000, 'comp_swap sell/in/full fees'); + assert(next_sqrt_price == 12255986885723453259120328222, 'comp_swap sell/in/full price'); } #[test] @@ -243,12 +264,14 @@ fn test_compute_swap_amounts_sell_exact_output_filled_max() { let target_sqrt_price = to_e28(1); let liquidity = to_e28_u128(2); let amount_rem = to_e28(1); + let fee_rate = 6; // 0.06% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, false + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, false ); assert(amount_in == 1333333333333333333333333334, 'comp_swap sell/out/full in'); assert(amount_out == 10000000000000000000000000000, 'comp_swap sell/out/full out'); + assert(fees == 800480288172903742245347, 'comp_swap sell/out/full fees'); assert(next_sqrt_price == 25000000000000000000000000000, 'comp_swap sell/out/full price'); } @@ -258,13 +281,15 @@ fn test_compute_swap_amounts_buy_exact_output_intermediate_insufficient_liquidit let target_sqrt_price = 2816000000000000000000000000000; let liquidity = 1024; let amount_rem = 4; + let fee_rate = 30; // 0.3% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, false + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, false ); assert(amount_in == 26215, 'comp_swap buy/out/iil in'); assert(amount_out == 0, 'comp_swap buy/out/iil out'); + assert(fees == 78, 'comp_swap buy/out/iil fees'); assert(next_sqrt_price == 2816000000000000000000000000000, 'comp_swap buy/out/iil price'); } @@ -274,13 +299,15 @@ fn test_compute_swap_amounts_sell_exact_output_intermediate_insufficient_liquidi let target_sqrt_price = 2304000000000000000000000000000; let liquidity = 1024; let amount_rem = 263000; + let fee_rate = 30; // 0.3% - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( - curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, false + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( + curr_sqrt_price, target_sqrt_price, liquidity, amount_rem, fee_rate, false ); assert(amount_in == 1, 'comp_swap sell/out/iil in'); assert(amount_out == 26214, 'comp_swap sell/out/iil out'); + assert(fees == 0, 'comp_swap sell/out/iil fees'); assert(next_sqrt_price == target_sqrt_price, 'comp_swap sell/out/iil price'); } diff --git a/packages/replicating/src/tests/libraries/test_swap_lib_invariants.cairo b/packages/replicating/src/tests/libraries/test_swap_lib_invariants.cairo index 8d0845b..3e25801 100644 --- a/packages/replicating/src/tests/libraries/test_swap_lib_invariants.cairo +++ b/packages/replicating/src/tests/libraries/test_swap_lib_invariants.cairo @@ -15,17 +15,23 @@ use haiko_lib::types::i128::I128Trait; use haiko_lib::helpers::utils::approx_eq; // Check following invariants: -// 1. If exact input, amount out <= amount remaining +// 1. Amount in + fee <= u256 max +// 2. If exact input, amount out <= amount remaining // If exact output, amount in <= amount remaining -// 2. If current price = target price, amount in = amount out = 0 -// 3. If next price != target price and: -// - Exact input, amount in == amount remaining +// 3. If current price = target price, amount in = amount out = fee = 0 +// 4. If next price != target price and: +// - Exact input, amount in + fee == amount remaining // - Exact output, amount out == amount remaining -// 4. If target price <= curr price, target price <= next price <= curr price +// 5. If target price <= curr price, target price <= next price <= curr price // Else if target price > curr price, curr price <= next price <= target price #[test] fn test_compute_swap_amounts_invariants( - curr_sqrt_price: u128, target_sqrt_price: u128, liquidity: u128, amount_rem: u128, width: u16, + curr_sqrt_price: u128, + target_sqrt_price: u128, + liquidity: u128, + amount_rem: u128, + fee_rate: u8, + width: u16, ) { // Return if invalid if curr_sqrt_price.into() < MIN_SQRT_PRICE @@ -38,48 +44,52 @@ fn test_compute_swap_amounts_invariants( // Compute swap amounts let exact_input = amount_rem % 2 == 0; // bool fuzzing not supported, so use even/odd for rng - let (amount_in, amount_out, next_sqrt_price) = compute_swap_amounts( + let (amount_in, amount_out, fees, next_sqrt_price) = compute_swap_amounts( curr_sqrt_price.into(), target_sqrt_price.into(), liquidity.into(), amount_rem.into(), + fee_rate.into(), exact_input, ); // Invariant 1 + assert(amount_in + fees <= BoundedInt::max(), 'Invariant 1'); + + // Invariant 2 if exact_input { - assert(amount_in <= amount_rem.into(), 'Invariant 1a'); + assert(amount_in <= amount_rem.into(), 'Invariant 2a'); } else { - assert(amount_out <= amount_rem.into(), 'Invariant 1b'); + assert(amount_out <= amount_rem.into(), 'Invariant 2b'); } - // Invariant 2 + // Invariant 3 if curr_sqrt_price == target_sqrt_price { - assert(amount_in == 0 && amount_out == 0, 'Invariant 2'); + assert(amount_in == 0 && amount_out == 0 && fees == 0, 'Invariant 3'); } - // Invariant 3 + // Invariant 4 if next_sqrt_price != target_sqrt_price.into() { if exact_input { // Rounding error due to fee calculation which rounds down `amount_rem` - assert(approx_eq(amount_in, amount_rem.into(), 1), 'Invariant 3a'); + assert(approx_eq(amount_in + fees, amount_rem.into(), 10), 'Invariant 4a'); } else { - assert(approx_eq(amount_out, amount_rem.into(), 1), 'Invariant 3b'); + assert(approx_eq(amount_out, amount_rem.into(), 10), 'Invariant 4b'); } } - // Invariant 4 + // Invariant 5 if target_sqrt_price <= curr_sqrt_price { assert( target_sqrt_price.into() <= next_sqrt_price && next_sqrt_price <= curr_sqrt_price.into(), - 'Invariant 4a' + 'Invariant 5a' ); } else { assert( curr_sqrt_price.into() <= next_sqrt_price && next_sqrt_price <= target_sqrt_price.into(), - 'Invariant 4b' + 'Invariant 5b' ); } } diff --git a/packages/replicating/src/tests/solver/debug_swap.cairo b/packages/replicating/src/tests/solver/debug_swap.cairo index e982a69..9e2058e 100644 --- a/packages/replicating/src/tests/solver/debug_swap.cairo +++ b/packages/replicating/src/tests/solver/debug_swap.cairo @@ -51,7 +51,7 @@ fn test_debug_swap() { let is_buy = false; let exact_input = false; let amount = 10000000000; - let min_spread = 25; + let fee_rate = 25; let range = 5000; let max_delta = 500; let max_skew = 6000; @@ -77,7 +77,7 @@ fn test_debug_swap() { start_warp(CheatTarget::One(solver.contract_address), 1000); let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; let market_params = MarketParams { - min_spread, + fee_rate, range, max_delta, max_skew, diff --git a/packages/replicating/src/tests/solver/test_e2e.cairo b/packages/replicating/src/tests/solver/test_e2e.cairo index e3d5d1d..c296573 100644 --- a/packages/replicating/src/tests/solver/test_e2e.cairo +++ b/packages/replicating/src/tests/solver/test_e2e.cairo @@ -26,6 +26,7 @@ use haiko_solver_replicating::{ }; // Haiko imports. +use haiko_lib::math::math; use haiko_lib::helpers::{ params::{owner, alice, bob, treasury, default_token_params}, actions::{ @@ -76,22 +77,34 @@ fn test_solver_e2e_private_market() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out) = solver.swap(market_id, params); + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Withdraw. - let (base_withdraw, quote_withdraw) = solver + let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver .withdraw_private(market_id, to_e18(50), to_e18(300)); // Run checks. - let (base_reserves, quote_reserves) = solver.get_balances(market_id); + let (base_reserves, quote_reserves, _, _) = solver.get_balances(market_id); assert( - base_reserves == base_deposit + base_deposit_init - amount_out - base_withdraw, + base_reserves == base_deposit + + base_deposit_init + - amount_out + - (base_withdraw - base_fees_withdraw), 'Base reserves' ); assert( - quote_reserves == quote_deposit + quote_deposit_init + amount_in - quote_withdraw, + quote_reserves == quote_deposit + + quote_deposit_init + + amount_in + - fees + - (quote_withdraw - quote_fees_withdraw), 'Quote reserves' ); + assert(base_fees_withdraw == 0, 'Base fees'); + assert( + approx_eq(quote_fees_withdraw, fees * to_e18(300) / (to_e18(1500) + amount_in), 1), + 'Quote fees' + ); assert(shares_init == 0, 'Shares init'); assert(shares == 0, 'Shares'); } @@ -114,20 +127,11 @@ fn test_solver_e2e_public_market() { // Deposit initial. let (base_deposit_init, quote_deposit_init, shares_init) = solver .deposit_initial(market_id, to_e18(100), to_e18(1000)); - println!( - "base_deposit_init: {}, quote_deposit_init: {}, shares_init: {}", - base_deposit_init, - quote_deposit_init, - shares_init - ); // Deposit as LP. start_prank(CheatTarget::One(solver.contract_address), alice()); let (base_deposit, quote_deposit, shares) = solver .deposit(market_id, to_e18(50), to_e18(600)); // Contains extra, should coerce. - println!( - "base_deposit: {}, quote_deposit: {}, shares: {}", base_deposit, quote_deposit, shares - ); // Swap. start_prank(CheatTarget::One(solver.contract_address), owner()); @@ -139,49 +143,66 @@ fn test_solver_e2e_public_market() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out) = solver.swap(market_id, params); - println!("amount_in: {}, amount_out: {}", amount_in, amount_out); + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw) = solver.withdraw_public(market_id, shares); - println!("base_withdraw: {}, quote_withdraw: {}", base_withdraw, quote_withdraw); + let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver + .withdraw_public(market_id, shares); // Collect withdraw fees. start_prank(CheatTarget::One(solver.contract_address), owner()); - let base_fees = solver + let base_withdraw_fees = solver .collect_withdraw_fees(solver.contract_address, base_token.contract_address); - let quote_fees = solver + let quote_withdraw_fees = solver .collect_withdraw_fees(solver.contract_address, quote_token.contract_address); - println!("base_fees: {}, quote_fees: {}", base_fees, quote_fees); // Run checks. - let (base_reserves, quote_reserves) = solver.get_balances(market_id); - println!("base_reserves: {}, quote_reserves: {}", base_reserves, quote_reserves); + let (base_reserves, quote_reserves, base_fee_reserves, quote_fee_reserves) = solver + .get_balances(market_id); let base_deposit_exp = to_e18(50); let quote_deposit_exp = to_e18(500); - println!( - "base_reserves_exp: {}, quote_reserves_exp: {}", - base_deposit_init + base_deposit_exp - amount_out - base_withdraw - base_fees, - quote_deposit_init + quote_deposit_exp + amount_in - quote_withdraw - quote_fees - ); assert(base_deposit == base_deposit_exp, 'Base deposit'); assert(quote_deposit == quote_deposit_exp, 'Quote deposit'); + assert( + approx_eq( + base_withdraw, math::mul_div(base_deposit_exp - amount_out / 3, 995, 1000, false), 10 + ), + 'Base withdraw' + ); + assert( + approx_eq( + quote_withdraw, math::mul_div(quote_deposit_exp + amount_in / 3, 995, 1000, false), 10 + ), + 'Quote withdraw' + ); + assert(base_fees_withdraw == 0, 'Base fees'); + assert( + approx_eq(quote_fees_withdraw, math::mul_div(fees / 3, 995, 1000, false), 10), 'Quote fees' + ); assert(shares == shares_init / 2, 'Shares'); assert( - base_reserves == base_deposit_init - + base_deposit_exp - - amount_out - - base_withdraw - - base_fees, + approx_eq(base_reserves, (base_deposit_init + base_deposit_exp - amount_out) * 2 / 3, 10), 'Base reserves' ); assert( - quote_reserves == quote_deposit_init - + quote_deposit_exp - + amount_in - - quote_withdraw - - quote_fees, + approx_eq( + quote_reserves, (quote_deposit_init + quote_deposit_exp + amount_in - fees) * 2 / 3, 10 + ), 'Quote reserves' ); + assert( + approx_eq( + base_withdraw_fees, math::mul_div(base_deposit_exp - amount_out / 3, 5, 1000, false), 10 + ), + 'Base withdraw fees' + ); + assert( + approx_eq( + quote_withdraw_fees, + math::mul_div(quote_deposit_exp + amount_in / 3, 5, 1000, false), + 10 + ), + 'Quote withdraw fees' + ); } diff --git a/packages/replicating/src/tests/solver/test_market_params.cairo b/packages/replicating/src/tests/solver/test_market_params.cairo index 2f964be..de0ddbe 100644 --- a/packages/replicating/src/tests/solver/test_market_params.cairo +++ b/packages/replicating/src/tests/solver/test_market_params.cairo @@ -49,7 +49,7 @@ fn test_queue_and_set_market_params_no_delay() { // Set market params. let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; let params = MarketParams { - min_spread: 987, + fee_rate: 987, range: 12345, max_delta: 676, max_skew: 9989, @@ -65,7 +65,7 @@ fn test_queue_and_set_market_params_no_delay() { let market_params = repl_solver.market_params(market_id); // Run checks. - assert(market_params.min_spread == params.min_spread, 'Min spread'); + assert(market_params.fee_rate == params.fee_rate, 'Fee rate'); assert(market_params.range == params.range, 'Range'); assert(market_params.max_delta == params.max_delta, 'Max delta'); assert(market_params.max_skew == params.max_skew, 'Max skew'); @@ -93,7 +93,7 @@ fn test_queue_and_set_market_params_with_delay() { // Queue market params. start_warp(CheatTarget::One(solver.contract_address), 1000); let params = MarketParams { - min_spread: 987, + fee_rate: 987, range: 12345, max_delta: 676, max_skew: 9989, @@ -113,7 +113,7 @@ fn test_queue_and_set_market_params_with_delay() { let queued_params = repl_solver.queued_market_params(market_id); // Run checks. - assert(market_params.min_spread == params.min_spread, 'Min spread'); + assert(market_params.fee_rate == params.fee_rate, 'Fee rate'); assert(market_params.range == params.range, 'Range'); assert(market_params.max_delta == params.max_delta, 'Max delta'); assert(market_params.max_skew == params.max_skew, 'Max skew'); @@ -145,7 +145,7 @@ fn test_queue_and_set_market_params_with_delay_first_initialisation() { // Run checks. let params = default_market_params(); - assert(market_params.min_spread == params.min_spread, 'Min spread'); + assert(market_params.fee_rate == params.fee_rate, 'Fee rate'); assert(market_params.range == params.range, 'Range'); assert(market_params.max_delta == params.max_delta, 'Max delta'); assert(market_params.max_skew == params.max_skew, 'Max skew'); @@ -212,7 +212,7 @@ fn test_update_queued_market_params() { // Queue market params. let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; let params = MarketParams { - min_spread: 987, + fee_rate: 987, range: 12345, max_delta: 676, max_skew: 9989, @@ -225,7 +225,7 @@ fn test_update_queued_market_params() { // Update queued market params. let updated_params = MarketParams { - min_spread: 123, + fee_rate: 123, range: 456, max_delta: 789, max_skew: 987, @@ -238,7 +238,7 @@ fn test_update_queued_market_params() { // Run checks. let queued_params = repl_solver.queued_market_params(market_id); - assert(queued_params.min_spread == updated_params.min_spread, 'Min spread'); + assert(queued_params.fee_rate == updated_params.fee_rate, 'Fee rate'); assert(queued_params.range == updated_params.range, 'Range'); assert(queued_params.max_delta == updated_params.max_delta, 'Max delta'); assert(queued_params.max_skew == updated_params.max_skew, 'Max skew'); @@ -262,7 +262,7 @@ fn test_cancel_queued_market_params() { // Queue market params. let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; let params = MarketParams { - min_spread: 987, + fee_rate: 987, range: 12345, max_delta: 676, max_skew: 9989, @@ -278,7 +278,7 @@ fn test_cancel_queued_market_params() { // Run checks. let queued_params = repl_solver.queued_market_params(market_id); - assert(queued_params.min_spread == 0, 'Min spread'); + assert(queued_params.fee_rate == 0, 'Fee rate'); assert(queued_params.range == 0, 'Range'); assert(queued_params.max_delta == 0, 'Max delta'); assert(queued_params.max_skew == 0, 'Max skew'); @@ -306,7 +306,7 @@ fn test_set_market_params_skips_delay_for_private_vault() { // Queue and immediately set market params. let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; let params = MarketParams { - min_spread: 987, + fee_rate: 987, range: 12345, max_delta: 676, max_skew: 9989, @@ -342,7 +342,7 @@ fn test_queue_market_params_emits_event() { // Queue market params. let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; let params = MarketParams { - min_spread: 987, + fee_rate: 987, range: 12345, max_delta: 676, max_skew: 9989, @@ -362,7 +362,7 @@ fn test_queue_market_params_emits_event() { ReplicatingSolver::Event::QueueMarketParams( ReplicatingSolver::QueueMarketParams { market_id, - min_spread: params.min_spread, + fee_rate: params.fee_rate, range: params.range, max_delta: params.max_delta, max_skew: params.max_skew, @@ -421,7 +421,7 @@ fn test_set_market_params_emits_event() { // Set market params. let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; let params = MarketParams { - min_spread: 987, + fee_rate: 987, range: 12345, max_delta: 676, max_skew: 9989, @@ -442,7 +442,7 @@ fn test_set_market_params_emits_event() { ReplicatingSolver::Event::SetMarketParams( ReplicatingSolver::SetMarketParams { market_id, - min_spread: params.min_spread, + fee_rate: params.fee_rate, range: params.range, max_delta: params.max_delta, max_skew: params.max_skew, @@ -614,7 +614,7 @@ fn test_set_market_params_fails_if_not_owner() { start_prank(CheatTarget::One(solver.contract_address), owner()); let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; let mut params = repl_solver.market_params(market_id); - params.min_spread = 987; + params.fee_rate = 987; repl_solver.queue_market_params(market_id, params); // Set market params. @@ -641,7 +641,7 @@ fn test_set_market_params_fails_before_delay_complete() { // Queue new market params. start_warp(CheatTarget::One(solver.contract_address), 1000); let mut params = default_market_params(); - params.min_spread = 987; + params.fee_rate = 987; repl_solver.queue_market_params(market_id, params); // Set new market params. @@ -689,7 +689,7 @@ fn test_set_market_params_fails_none_queued_null_params() { // Queue market params. start_warp(CheatTarget::One(solver.contract_address), 1000); let mut params = default_market_params(); - params.min_spread = 987; + params.fee_rate = 987; repl_solver.queue_market_params(market_id, params); // Update queued market params to zero. diff --git a/packages/replicating/src/tests/solver/test_swap.cairo b/packages/replicating/src/tests/solver/test_swap.cairo index ed4749c..8eae8ba 100644 --- a/packages/replicating/src/tests/solver/test_swap.cairo +++ b/packages/replicating/src/tests/solver/test_swap.cairo @@ -53,7 +53,7 @@ struct TestCase { // LIQUIDITY pub base_reserves: u256, pub quote_reserves: u256, - pub min_spread: u32, + pub fee_rate: u16, pub range: u32, pub max_delta: u32, pub max_skew: u16, @@ -70,16 +70,17 @@ struct SwapCase { pub exact_input: bool, pub amount_in: u256, pub amount_out: u256, + pub fees: u256, } fn get_test_cases_1() -> Span { let cases: Array = array![ TestCase { - description: "1) Full range liq, price 1, no spread", + description: "1) Full range liq, price 1, no fees", oracle_price: 1_00000000, base_reserves: to_e18(1000), quote_reserves: to_e18(1000), - min_spread: 0, + fee_rate: 0, range: 7906625, max_delta: 0, max_skew: 0, @@ -91,35 +92,39 @@ fn get_test_cases_1() -> Span { is_buy: true, exact_input: true, amount_in: to_e18(100), - amount_out: 90909090909090909146 + amount_out: 90909090909090909146, + fees: 0, }, SwapCase { is_buy: false, exact_input: true, amount_in: to_e18(100), - amount_out: 90909090909090909146 + amount_out: 90909090909090909146, + fees: 0, }, SwapCase { is_buy: true, exact_input: false, amount_in: 111111111111111111027, - amount_out: to_e18(100) + amount_out: to_e18(100), + fees: 0, }, SwapCase { is_buy: false, exact_input: false, amount_in: 111111111111111111027, - amount_out: to_e18(100) + amount_out: to_e18(100), + fees: 0, }, ] .span(), }, TestCase { - description: "2) Full range liq, price 0.1, no spread", + description: "2) Full range liq, price 0.1, no fees", oracle_price: 0_10000000, base_reserves: to_e18(100), quote_reserves: to_e18(1000), - min_spread: 0, + fee_rate: 0, range: 7676365, max_delta: 0, max_skew: 0, @@ -131,35 +136,39 @@ fn get_test_cases_1() -> Span { is_buy: true, exact_input: true, amount_in: to_e18(10), - amount_out: 49999834853317669644 + amount_out: 49999834853317669644, + fees: 0, }, SwapCase { is_buy: false, exact_input: true, amount_in: to_e18(10), - amount_out: 998997611702025557 + amount_out: 998997611702025557, + fees: 0, }, SwapCase { is_buy: true, exact_input: false, amount_in: 1111118450987902274, - amount_out: to_e18(10) + amount_out: to_e18(10), + fees: 0, }, SwapCase { is_buy: false, exact_input: false, amount_in: 101010443847319896836, - amount_out: to_e18(10) + amount_out: to_e18(10), + fees: 0, }, ] .span(), }, TestCase { - description: "3) Full range liq, price 10, no spread", + description: "3) Full range liq, price 10, no fees", oracle_price: 10_00000000, base_reserves: to_e18(1000), quote_reserves: to_e18(100), - min_spread: 0, + fee_rate: 0, range: 7676365, max_delta: 0, max_skew: 0, @@ -171,35 +180,39 @@ fn get_test_cases_1() -> Span { is_buy: true, exact_input: true, amount_in: to_e18(100), - amount_out: 9900956827006555844 + amount_out: 9900956827006555844, + fees: 0, }, SwapCase { is_buy: false, exact_input: true, amount_in: to_e18(100), - amount_out: 90909036314998803578 + amount_out: 90909036314998803578, + fees: 0, }, SwapCase { is_buy: true, exact_input: false, amount_in: 1111114882320518862795, - amount_out: to_e18(100) + amount_out: to_e18(100), + fees: 0, }, SwapCase { is_buy: false, exact_input: false, amount_in: 466588818773133962045136853193659825, - amount_out: to_e18(100) + amount_out: to_e18(100), + fees: 0, }, ] .span(), }, TestCase { - description: "4) Concentrated liq, price 1, no spread", + description: "4) Concentrated liq, price 1, no fees", oracle_price: 1_00000000, base_reserves: to_e18(1000), quote_reserves: to_e18(1000), - min_spread: 0, + fee_rate: 0, range: 5000, max_delta: 0, max_skew: 0, @@ -211,35 +224,39 @@ fn get_test_cases_1() -> Span { is_buy: true, exact_input: true, amount_in: to_e18(100), - amount_out: 99753708432456984326 + amount_out: 99753708432456984326, + fees: 0, }, SwapCase { is_buy: false, exact_input: true, amount_in: to_e18(100), - amount_out: 99753708432456984326 + amount_out: 99753708432456984326, + fees: 0, }, SwapCase { is_buy: true, exact_input: false, amount_in: 100247510763823131034, - amount_out: to_e18(100) + amount_out: to_e18(100), + fees: 0, }, SwapCase { is_buy: false, exact_input: false, amount_in: 100247510763823131034, - amount_out: to_e18(100) + amount_out: to_e18(100), + fees: 0, }, ] .span(), }, TestCase { - description: "5) Concentrated liq, price 1, 100 spread", + description: "5) Concentrated liq, price 1, 1% fees", oracle_price: 1_00000000, base_reserves: to_e18(1000), quote_reserves: to_e18(1000), - min_spread: 100, + fee_rate: 100, range: 5000, max_delta: 0, max_skew: 0, @@ -251,25 +268,29 @@ fn get_test_cases_1() -> Span { is_buy: true, exact_input: true, amount_in: to_e18(100), - amount_out: 99654250398634336911 + amount_out: 98758603689263513299, + fees: to_e18(1), }, SwapCase { is_buy: false, exact_input: true, amount_in: to_e18(100), - amount_out: 99654250398634336911 + amount_out: 98758603689263513299, + fees: to_e18(1), }, SwapCase { is_buy: true, exact_input: false, - amount_in: 100347807913318736434, - amount_out: to_e18(100) + amount_in: 101260111882649627307, + amount_out: to_e18(100), + fees: 1012601118826496273, }, SwapCase { is_buy: false, exact_input: false, - amount_in: 100347807913318736434, - amount_out: to_e18(100) + amount_in: 101260111882649627307, + amount_out: to_e18(100), + fees: 1012601118826496273, }, ] .span(), @@ -281,11 +302,11 @@ fn get_test_cases_1() -> Span { fn get_test_cases_2() -> Span { let cases: Array = array![ TestCase { - description: "6) Concentrated liq, price 1, 50000 spread", + description: "6) Concentrated liq, price 1, 50% fees", oracle_price: 10_00000000, base_reserves: to_e18(1000), quote_reserves: to_e18(1000), - min_spread: 50000, + fee_rate: 5000, range: 5000, max_delta: 0, max_skew: 0, @@ -297,35 +318,39 @@ fn get_test_cases_2() -> Span { is_buy: true, exact_input: true, amount_in: to_e18(100), - amount_out: 6064393018672684143 + amount_out: 4999365860842892496, + fees: 50000000000000000000, }, SwapCase { is_buy: false, exact_input: true, amount_in: to_e18(100), - amount_out: 597579323442496790520 + amount_out: 493899555720718382442, + fees: 50000000000000000000, }, SwapCase { is_buy: true, exact_input: false, - amount_in: 1652803511080475795527, - amount_out: to_e18(100) + amount_in: 2004957020254865157395, + amount_out: to_e18(100), + fees: 1002478510127432578697, }, SwapCase { is_buy: false, exact_input: false, - amount_in: 16528088195378414840, - amount_out: to_e18(100) + amount_in: 20049634597552599158, + amount_out: to_e18(100), + fees: 10024817298776299579, }, ] .span(), }, TestCase { - description: "7) Concentrated liq, price 1, 100 spread, 500 max delta", + description: "7) Concentrated liq, price 1, 1% fees, 500 max delta", oracle_price: 1_00000000, base_reserves: to_e18(500), quote_reserves: to_e18(1000), - min_spread: 100, + fee_rate: 100, range: 5000, max_delta: 500, max_skew: 0, @@ -337,35 +362,39 @@ fn get_test_cases_2() -> Span { is_buy: true, exact_input: true, amount_in: to_e18(100), - amount_out: 99245582637747914747 + amount_out: 98355771317925390873, + fees: to_e18(1), }, SwapCase { is_buy: false, exact_input: true, amount_in: to_e18(100), - amount_out: 99819404970139967372 + amount_out: 98922277561466653632, + fees: to_e18(1), }, SwapCase { is_buy: true, exact_input: false, - amount_in: 100763924334713808578, - amount_out: to_e18(100) + amount_in: 101680011392792093046, + amount_out: to_e18(100), + fees: 1016800113927920930, }, SwapCase { is_buy: false, exact_input: false, - amount_in: 100181369566420499919, - amount_out: to_e18(100) + amount_in: 101092160374998987555, + amount_out: to_e18(100), + fees: 1010921603749989875, }, ] .span(), }, TestCase { - description: "8) Concentrated liq, price 0.1, 100 spread, 20000 max delta", + description: "8) Concentrated liq, price 0.1, 1% fees, 20000 max delta", oracle_price: 0_10000000, base_reserves: to_e18(500), quote_reserves: to_e18(1000), - min_spread: 100, + fee_rate: 100, range: 5000, max_delta: 20000, max_skew: 0, @@ -376,26 +405,30 @@ fn get_test_cases_2() -> Span { SwapCase { is_buy: true, exact_input: true, - amount_in: 61495781354774112619, - amount_out: 499999999999999999999 + amount_in: 62054865270942237718, + amount_out: 499999999999999999999, + fees: 620548652709422377, }, SwapCase { is_buy: false, exact_input: true, amount_in: to_e18(100), - amount_out: 11967866534829175688 + amount_out: 11860073497841049336, + fees: to_e18(1), }, SwapCase { is_buy: true, exact_input: false, - amount_in: 12055018117706607774, - amount_out: to_e18(100) + amount_in: 12164615338690643305, + amount_out: to_e18(100), + fees: 121646153386906433, }, SwapCase { is_buy: false, exact_input: false, - amount_in: 837391432418576103404, - amount_out: to_e18(100) + amount_in: 845004508813220005468, + amount_out: to_e18(100), + fees: 8450045088132200054, }, ] .span(), @@ -405,7 +438,7 @@ fn get_test_cases_2() -> Span { oracle_price: 1_00000000, base_reserves: to_e18(100), quote_reserves: to_e18(100), - min_spread: 100, + fee_rate: 100, range: 5000, max_delta: 0, max_skew: 0, @@ -416,14 +449,16 @@ fn get_test_cases_2() -> Span { SwapCase { is_buy: true, exact_input: true, - amount_in: 102634081505001697489, - amount_out: 99999999999999999999 + amount_in: 103567170945545576580, + amount_out: 99999999999999999999, + fees: 1035671709455455765, }, SwapCase { is_buy: false, exact_input: true, - amount_in: 102634081505001697489, - amount_out: 99999999999999999999 + amount_in: 103567170945545576580, + amount_out: 99999999999999999999, + fees: 1035671709455455765, }, ] .span(), @@ -433,7 +468,7 @@ fn get_test_cases_2() -> Span { oracle_price: 1_000_000_000_000_000_00000000, base_reserves: to_e18(1000), quote_reserves: to_e18(1000), - min_spread: 100, + fee_rate: 100, range: 5000, max_delta: 0, max_skew: 0, @@ -442,25 +477,32 @@ fn get_test_cases_2() -> Span { threshold_amount: Option::None(()), exp: array![ SwapCase { - is_buy: true, exact_input: true, amount_in: to_e18(100), amount_out: 99899, + is_buy: true, + exact_input: true, + amount_in: to_e18(100), + amount_out: 98999, + fees: to_e18(1) }, SwapCase { is_buy: false, exact_input: true, - amount_in: 1026350, + amount_in: 1035681, amount_out: 999999999999999999999, + fees: 10356, }, SwapCase { is_buy: true, exact_input: false, - amount_in: 100347899379444510663740995366407145, - amount_out: 99999999999999999999, + amount_in: 101260204180332276952555257675541019, + amount_out: 100000000000000000000, + fees: 1012602041803322769525552576755410, }, SwapCase { is_buy: false, exact_input: false, - amount_in: 100348, + amount_in: 101261, amount_out: 100000000000000000000, + fees: 1012, }, ] .span(), @@ -476,7 +518,7 @@ fn get_test_cases_3() -> Span { oracle_price: 1, base_reserves: to_e18(1000), quote_reserves: to_e18(1000), - min_spread: 100, + fee_rate: 100, range: 5000, max_delta: 0, max_skew: 0, @@ -487,26 +529,30 @@ fn get_test_cases_3() -> Span { SwapCase { is_buy: true, exact_input: true, - amount_in: 10263437372397, + amount_in: 10356746582120, amount_out: 999999999999999999999, + fees: 103567465821, }, SwapCase { is_buy: false, exact_input: true, - amount_in: 99999999999999999999, - amount_out: 998993359216, + amount_in: 100000000000000000000, + amount_out: 989992918767, + fees: 1000000000000000000, }, SwapCase { is_buy: true, exact_input: false, - amount_in: 1003480936228, - amount_out: 100000000000000000000, + amount_in: 1012604001896, + amount_out: 99999999999999999999, + fees: 10126040018, }, SwapCase { is_buy: false, exact_input: false, - amount_in: 10034852567983843807643429092, + amount_in: 10126083617468551702944516926, amount_out: 100000000000000000000, + fees: 101260836174685517029445169, }, ] .span(), @@ -516,7 +562,7 @@ fn get_test_cases_3() -> Span { oracle_price: 1_00000000, base_reserves: to_e18(100), quote_reserves: to_e18(100), - min_spread: 100, + fee_rate: 100, range: 50000, max_delta: 0, max_skew: 0, @@ -527,14 +573,16 @@ fn get_test_cases_3() -> Span { SwapCase { is_buy: true, exact_input: true, - amount_in: 21850483612303829529, - amount_out: 20823204527740984512, + amount_in: 22288543558601668321, + amount_out: 21038779527378539768, + fees: 222885435586016683, }, SwapCase { is_buy: true, exact_input: false, - amount_in: 21850483612303829529, - amount_out: 20823204527740984512, + amount_in: 22288543558601668321, + amount_out: 21038779527378539768, + fees: 222885435586016683, }, ] .span(), @@ -544,7 +592,7 @@ fn get_test_cases_3() -> Span { oracle_price: 1_00000000, base_reserves: to_e18(100), quote_reserves: to_e18(100), - min_spread: 100, + fee_rate: 100, range: 50000, max_delta: 0, max_skew: 0, @@ -555,14 +603,16 @@ fn get_test_cases_3() -> Span { SwapCase { is_buy: false, exact_input: true, - amount_in: 24240352373112801069, - amount_out: 22984922158048704819, + amount_in: 24701345711211794538, + amount_out: 23199416574442336449, + fees: 247013457112117945, }, SwapCase { is_buy: false, exact_input: false, - amount_in: 24240352373112801069, - amount_out: 22984922158048704819, + amount_in: 24701345711211794538, + amount_out: 23199416574442336449, + fees: 247013457112117945, }, ] .span(), @@ -572,25 +622,27 @@ fn get_test_cases_3() -> Span { oracle_price: 1_00000000, base_reserves: to_e18(1000), quote_reserves: to_e18(1000), - min_spread: 100, + fee_rate: 100, range: 5000, max_delta: 0, max_skew: 0, amount: to_e18(100), threshold_sqrt_price: Option::None(()), - threshold_amount: Option::Some(99650000000000000000), + threshold_amount: Option::Some(98750000000000000000), exp: array![ SwapCase { is_buy: true, exact_input: true, amount_in: 99999999999999999999, - amount_out: 99654250398634336911, + amount_out: 98758603689263513299, + fees: 999999999999999999, }, SwapCase { is_buy: false, exact_input: true, amount_in: 100000000000000000000, - amount_out: 99654250398634336911, + amount_out: 98758603689263513299, + fees: 1000000000000000000, }, ] .span(), @@ -600,25 +652,27 @@ fn get_test_cases_3() -> Span { oracle_price: 1_00000000, base_reserves: to_e18(1000), quote_reserves: to_e18(1000), - min_spread: 100, + fee_rate: 100, range: 5000, max_delta: 0, max_skew: 0, amount: to_e18(100), threshold_sqrt_price: Option::None(()), - threshold_amount: Option::Some(100350000000000000000), + threshold_amount: Option::Some(101500000000000000000), exp: array![ SwapCase { is_buy: true, exact_input: false, - amount_in: 100347807913318736434, + amount_in: 101260111882649627307, amount_out: 99999999999999999999, + fees: 1012601118826496273, }, SwapCase { is_buy: false, exact_input: false, - amount_in: 100347807913318736434, + amount_in: 101260111882649627307, amount_out: 100000000000000000000, + fees: 1012601118826496273, }, ] .span(), @@ -679,7 +733,7 @@ fn run_swap_cases(cases: Span) { contract_address: solver.contract_address }; let mut market_params = repl_solver.market_params(market_id); - market_params.min_spread = case.min_spread; + market_params.fee_rate = case.fee_rate; market_params.range = case.range; market_params.max_delta = case.max_delta; market_params.max_skew = case.max_skew; @@ -705,7 +759,7 @@ fn run_swap_cases(cases: Span) { // Obtain quotes and execute swaps. start_prank(CheatTarget::One(solver.contract_address), alice()); let solver_hooks = ISolverHooksDispatcher { contract_address: solver.contract_address }; - let (quote_in, quote_out) = solver_hooks + let (quote_in, quote_out, quote_fees) = solver_hooks .quote( market_id, SwapParams { @@ -719,7 +773,7 @@ fn run_swap_cases(cases: Span) { ); // Execute swap. - let (amount_in, amount_out) = solver + let (amount_in, amount_out, fees) = solver .swap( market_id, SwapParams { @@ -733,20 +787,51 @@ fn run_swap_cases(cases: Span) { ); // Check results. - println!("amount in: {}, amount out: {}", amount_in, amount_out); - assert( - approx_eq_pct(amount_in, swap_case.amount_in, 10) - || approx_eq(amount_in, swap_case.amount_in, 1000), - 'Amount in' - ); - assert( - approx_eq_pct(amount_out, swap_case.amount_out, 10) - || approx_eq(amount_out, swap_case.amount_out, 1000), - 'Amount out' - ); + println!("amount in: {}, amount out: {}, fees: {}", amount_in, amount_out, fees); + if !(approx_eq_pct(amount_in, swap_case.amount_in, 10) + || approx_eq(amount_in, swap_case.amount_in, 1000)) { + panic( + array![ + 'Amount in', + i.into() + 1, + j.into() + 1, + amount_in.low.into(), + amount_in.high.into(), + swap_case.amount_in.low.into(), + swap_case.amount_in.high.into() + ] + ); + } + if !(approx_eq_pct(amount_out, swap_case.amount_out, 10) + || approx_eq(amount_out, swap_case.amount_out, 1000)) { + panic( + array![ + 'Amount out', + i.into() + 1, + j.into() + 1, + amount_out.low.into(), + amount_out.high.into(), + swap_case.amount_out.low.into(), + swap_case.amount_out.high.into() + ] + ); + } + if !(approx_eq_pct(fees, swap_case.fees, 10) || approx_eq(fees, swap_case.fees, 1000)) { + panic( + array![ + 'Fees', + i.into() + 1, + j.into() + 1, + fees.low.into(), + fees.high.into(), + swap_case.fees.low.into(), + swap_case.fees.high.into() + ] + ); + } assert(amount_in == quote_in, 'Quote in'); assert(amount_out == quote_out, 'Quote out'); - + assert(fees == quote_fees, 'Quote fees'); j += 1; }; @@ -850,7 +935,7 @@ fn test_swap_fails_if_invalid_oracle_price() { } #[test] -#[should_panic(expected: ('ThresholdAmount', 1004501675692245436, 0))] +#[should_panic(expected: ('ThresholdAmount', 999979051909438890, 0))] fn test_swap_fails_if_swap_buy_below_threshold_amount() { let ( _base_token, _quote_token, _oracle, _vault_token_class, solver, market_id, _vault_token_opt @@ -885,7 +970,7 @@ fn test_swap_fails_if_swap_buy_below_threshold_amount() { } #[test] -#[should_panic(expected: ('ThresholdAmount', 99492002490779814763, 0))] +#[should_panic(expected: ('ThresholdAmount', 99044273158283891908, 0))] fn test_swap_fails_if_swap_sell_below_threshold_amount() { let ( _base_token, _quote_token, _oracle, _vault_token_class, solver, market_id, _vault_token_opt diff --git a/packages/replicating/src/tests/solver/test_withdraw.cairo b/packages/replicating/src/tests/solver/test_withdraw.cairo index d112831..ad3656f 100644 --- a/packages/replicating/src/tests/solver/test_withdraw.cairo +++ b/packages/replicating/src/tests/solver/test_withdraw.cairo @@ -4,7 +4,7 @@ use starknet::contract_address_const; // Local imports. use haiko_solver_core::{ contracts::solver::SolverComponent, - interfaces::ISolver::{ISolverDispatcher, ISolverDispatcherTrait}, + interfaces::ISolver::{ISolverDispatcher, ISolverDispatcherTrait}, types::SwapParams, }; use haiko_solver_replicating::{ contracts::mocks::mock_pragma_oracle::{ @@ -56,30 +56,46 @@ fn test_withdraw_partial_shares_from_public_vault() { start_prank(CheatTarget::One(solver.contract_address), alice()); let (_, _, shares) = solver.deposit(market_id, base_amount, quote_amount); + // Swap. + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); + // Snapshot before. let vault_token = vault_token_opt.unwrap(); let bef = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw) = solver.withdraw_public(market_id, shares / 2); + let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver + .withdraw_public(market_id, shares / 2); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(base_withdraw, base_amount / 2, 10), 'Base deposit'); - assert(approx_eq(quote_withdraw, quote_amount / 2, 10), 'Quote deposit'); + assert(approx_eq(base_withdraw, (base_amount * 2 + amount_in) / 4, 10), 'Base deposit'); + assert(approx_eq(quote_withdraw, (quote_amount * 2 - amount_out) / 4, 10), 'Quote deposit'); + assert(approx_eq(base_fees_withdraw, fees / 4, 10), 'Base fees withdraw'); + assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal - shares / 2, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal - shares / 2, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - base_withdraw, + aft.market_state.base_reserves == bef.market_state.base_reserves + - (base_withdraw - base_fees_withdraw), 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - quote_withdraw, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + - (quote_withdraw - quote_fees_withdraw), 'Quote reserve' ); assert(aft.bid.lower_sqrt_price == bef.bid.lower_sqrt_price, 'Bid lower sqrt price'); @@ -111,6 +127,17 @@ fn test_withdraw_remaining_shares_from_public_vault() { start_prank(CheatTarget::One(solver.contract_address), alice()); let (_, _, shares) = solver.deposit(market_id, base_amount, quote_amount); + // Swap. + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); + // Withdraw owner. start_prank(CheatTarget::One(solver.contract_address), owner()); solver.withdraw_public(market_id, shares_init); @@ -121,24 +148,29 @@ fn test_withdraw_remaining_shares_from_public_vault() { // Withdraw LP. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw) = solver.withdraw_public(market_id, shares); + let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver + .withdraw_public(market_id, shares); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(base_withdraw, base_amount, 10), 'Base deposit'); - assert(approx_eq(quote_withdraw, quote_amount, 10), 'Quote deposit'); + assert(approx_eq(base_withdraw, (base_amount * 2 + amount_in) / 2, 1), 'Base deposit'); + assert(approx_eq(quote_withdraw, (quote_amount * 2 - amount_out) / 2, 1), 'Quote deposit'); + assert(approx_eq(base_fees_withdraw, fees / 2, 1), 'Base fees withdraw'); + assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal - shares, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal - shares, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - base_withdraw, + aft.market_state.base_reserves == bef.market_state.base_reserves + - (base_withdraw - base_fees_withdraw), 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - quote_withdraw, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + - (quote_withdraw - quote_fees_withdraw), 'Quote reserve' ); assert(aft.bid.lower_sqrt_price == 0, 'Bid lower sqrt price'); @@ -200,20 +232,35 @@ fn test_withdraw_private_base_only() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, _, fees) = solver.swap(market_id, params); + // Snapshot before. let vault_token = contract_address_const::<0x0>(); let bef = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver.withdraw_private(market_id, base_amount, 0); + let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver + .withdraw_private(market_id, base_amount + amount_in, 0); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount, 10), 'Base withdraw'); + assert(approx_eq(base_withdraw, base_amount + amount_in, 10), 'Base withdraw'); assert(quote_withdraw == 0, 'Quote withdraw'); + assert(approx_eq(base_fees_withdraw, fees, 10), 'Base fees withdraw'); + assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); @@ -243,20 +290,35 @@ fn test_withdraw_private_quote_only() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (_, amount_out, _) = solver.swap(market_id, params); + // Snapshot before. let vault_token = contract_address_const::<0x0>(); let bef = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver.withdraw_private(market_id, 0, quote_amount); + let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver + .withdraw_private(market_id, 0, quote_amount - amount_out); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. assert(base_withdraw == 0, 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount, 10), 'Quote withdraw'); + assert(approx_eq(quote_withdraw, quote_amount - amount_out, 10), 'Quote withdraw'); + assert(base_fees_withdraw == 0, 'Base fees withdraw'); + assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); @@ -278,31 +340,49 @@ fn test_withdraw_private_partial_amounts() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); + // Snapshot before. let vault_token = contract_address_const::<0x0>(); let bef = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver - .withdraw_private(market_id, base_amount / 2, quote_amount / 2); + let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver + .withdraw_private( + market_id, (base_amount + amount_in) / 2, (quote_amount - amount_out) / 2 + ); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount / 2, 10), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount / 2, 10), 'Quote withdraw'); + assert(approx_eq(base_withdraw, (base_amount + amount_in) / 2, 10), 'Base withdraw'); + assert(approx_eq(quote_withdraw, (quote_amount - amount_out) / 2, 10), 'Quote withdraw'); + assert(approx_eq(base_fees_withdraw, fees / 2, 10), 'Base fees withdraw'); + assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - base_withdraw, + aft.market_state.base_reserves == bef.market_state.base_reserves + - (base_withdraw - base_fees_withdraw), 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - quote_withdraw, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + - (quote_withdraw - quote_fees_withdraw), 'Quote reserve' ); assert(aft.bid.lower_sqrt_price == bef.bid.lower_sqrt_price, 'Bid lower sqrt price'); @@ -332,31 +412,46 @@ fn test_withdraw_all_remaining_balances_from_private_vault() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Snapshot before. let vault_token = contract_address_const::<0x0>(); let bef = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver - .withdraw_private(market_id, base_amount, quote_amount); + let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver + .withdraw_private(market_id, base_amount + amount_in, quote_amount + amount_out); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount, 10), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount, 10), 'Quote withdraw'); + assert(approx_eq(base_withdraw, base_amount + amount_in, 10), 'Base withdraw'); + assert(approx_eq(quote_withdraw, quote_amount - amount_out, 10), 'Quote withdraw'); + assert(approx_eq(base_fees_withdraw, fees, 10), 'Base fees withdraw'); + assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); assert(aft.vault_lp_bal == 0, 'LP shares'); assert(aft.vault_total_bal == 0, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - base_withdraw, + aft.market_state.base_reserves == bef.market_state.base_reserves + - (base_withdraw - base_fees_withdraw), 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - quote_withdraw, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + - (quote_withdraw - quote_fees_withdraw), 'Quote reserve' ); assert(aft.bid.lower_sqrt_price == 0, 'Bid lower sqrt price'); @@ -382,12 +477,25 @@ fn test_withdraw_more_than_available_correctly_caps_amount_for_private_vault() { let quote_amount = to_e18(500); solver.deposit_initial(market_id, base_amount, quote_amount); + // Swap. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let (amount_in, amount_out, fees) = solver.swap(market_id, params); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw) = solver + let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver .withdraw_private(market_id, base_amount * 2, quote_amount * 2); // Run checks. - assert(approx_eq(base_withdraw, base_amount, 10), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount, 10), 'Quote withdraw'); + assert(approx_eq(base_withdraw, base_amount + amount_in, 10), 'Base withdraw'); + assert(approx_eq(quote_withdraw, quote_amount - amount_out, 10), 'Quote withdraw'); + assert(base_fees_withdraw == fees, 'Base fees withdraw'); + assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); } diff --git a/packages/replicating/src/types.cairo b/packages/replicating/src/types.cairo index 7da6da8..5731ee0 100644 --- a/packages/replicating/src/types.cairo +++ b/packages/replicating/src/types.cairo @@ -7,7 +7,7 @@ use starknet::ContractAddress; // Solver market parameters. // -// * `min_spread` - default spread between reference price and bid/ask price +// * `fee_rate` - swap fee rate (base 10000) // * `range` - default range of spread applied on an imbalanced portfolio // * `max_delta` - inventory delta, or the max additional single-sided spread applied on an imbalanced portfolio // * `max_skew` - max skew of the portfolio (out of 10000) @@ -17,7 +17,7 @@ use starknet::ContractAddress; // * `max_age` - maximum age of quoted oracle price #[derive(Drop, Copy, Serde, PartialEq, Default)] pub struct MarketParams { - pub min_spread: u32, + pub fee_rate: u16, pub range: u32, pub max_delta: u32, pub max_skew: u16, @@ -36,7 +36,7 @@ pub struct MarketParams { // // * `slab0` - base currency id // * `slab1` - quote currency id -// * `slab2` - `min_spread` + `range` + `max_delta` + `max_skew` + `min_sources` + `max_age` +// * `slab2` - `fee_rate` + `range` + `max_delta` + `max_skew` + `min_sources` + `max_age` #[derive(starknet::Store)] pub struct PackedMarketParams { pub slab0: felt252, diff --git a/packages/reversion/src/contracts/reversion_solver.cairo b/packages/reversion/src/contracts/reversion_solver.cairo index 6143666..d80824e 100644 --- a/packages/reversion/src/contracts/reversion_solver.cairo +++ b/packages/reversion/src/contracts/reversion_solver.cairo @@ -81,7 +81,7 @@ pub mod ReversionSolver { pub(crate) struct SetMarketParams { #[key] pub market_id: felt252, - pub spread: u32, + pub fee_rate: u16, pub range: u32, pub base_currency_id: felt252, pub quote_currency_id: felt252, @@ -123,11 +123,12 @@ pub mod ReversionSolver { // * `swap_params` - swap parameters // // # Returns - // * `amount_in` - amount in + // * `amount_in` - amount in including fees // * `amount_out` - amount out + // * `fees` - amount of fees fn quote( self: @ContractState, market_id: felt252, swap_params: SwapParams, - ) -> (u256, u256) { + ) -> (u256, u256, u256) { // Run validity checks. let state: MarketState = self.solver.market_state.read(market_id); let market_info: MarketInfo = self.solver.market_info.read(market_id); @@ -139,7 +140,8 @@ pub mod ReversionSolver { let position = if swap_params.is_buy { ask } else { bid }; // Calculate and return swap amounts. - swap_lib::get_swap_amounts(swap_params, position) + let market_params = self.market_params.read(market_id); + swap_lib::get_swap_amounts(swap_params, market_params.fee_rate, position) } // Get the initial token supply to mint when first depositing to a market. @@ -304,7 +306,7 @@ pub mod ReversionSolver { Event::SetMarketParams( SetMarketParams { market_id, - spread: params.spread, + fee_rate: params.fee_rate, range: params.range, base_currency_id: params.base_currency_id, quote_currency_id: params.quote_currency_id, @@ -360,7 +362,7 @@ pub mod ReversionSolver { let mut bid: PositionInfo = Default::default(); let mut ask: PositionInfo = Default::default(); let (bid_lower, bid_upper, ask_lower, ask_upper) = spread_math::get_virtual_position_range( - trend_state.trend, params.spread, params.range, cached_price, oracle_price + trend_state.trend, params.range, cached_price, oracle_price ); if state.quote_reserves != 0 { bid = diff --git a/packages/reversion/src/libraries/spread_math.cairo b/packages/reversion/src/libraries/spread_math.cairo index 17a77ca..d5145f7 100644 --- a/packages/reversion/src/libraries/spread_math.cairo +++ b/packages/reversion/src/libraries/spread_math.cairo @@ -17,9 +17,9 @@ use haiko_lib::constants::MAX_LIMIT_SHIFTED; // // # Arguments // `is_bid` - whether to calculate bid or ask position -// `spread` - spread to apply to the oracle price -// `oracle_price` - current oracle price (base 10e28) -// `amount` - token amount in reserve +// `lower_limit` - lower limit price +// `upper_limit` - upper limit price +// `amount` - amount to swap // // # Returns // `position` - virtual liquidity position to execute swap over @@ -56,7 +56,6 @@ pub fn get_virtual_position( // // # Arguments // `trend` - current market trend -// `spread` - spread to apply to the oracle price // `range` - position range // `cached_price` - cached oracle price (base 10e28) // `oracle_price` - current oracle price (base 10e28) @@ -67,69 +66,73 @@ pub fn get_virtual_position( // `ask_lower` - virtual ask lower limit // `ask_upper` - virtual ask upper limit pub fn get_virtual_position_range( - trend: Trend, - spread: u32, - range: u32, - cached_price: u256, - oracle_price: u256 + trend: Trend, range: u32, cached_price: u256, oracle_price: u256 ) -> (u32, u32, u32, u32) { - // Convert oracle and cached oracle prices to limits. assert(oracle_price != 0, 'OraclePriceZero'); + + // Calculate position ranges on the new oracle price. let oracle_limit = price_math::price_to_limit(oracle_price, 1, false); - let cached_limit = price_math::price_to_limit(cached_price, 1, false); + assert(oracle_limit >= range, 'OracleLimitUF'); + let new_bid_lower = oracle_limit - range; + let new_bid_upper = oracle_limit; + let new_ask_lower = oracle_limit; + let new_ask_upper = oracle_limit + range; + + // Handle special case. If cached price is unset, set it to the oracle price. + let mut cached_price_set = cached_price; + if cached_price == 0 { + cached_price_set = oracle_price; + } + + // Calculate position ranges on the cached price. + let cached_limit = price_math::price_to_limit(cached_price_set, 1, false); + assert(cached_limit >= range, 'CachedLimitUF'); + let bid_lower = cached_limit - range; + let bid_upper = cached_limit; + let ask_lower = cached_limit; + let ask_upper = cached_limit + range; - // First, calculate position ranges on the cached price. - assert(cached_limit >= spread && cached_limit >= range, 'LimitUF'); - let bid_lower = cached_limit - spread - range; - let bid_upper = cached_limit - spread; - let ask_lower = cached_limit + spread; - let ask_upper = cached_limit + spread + range; - - // Then, cap bid upper or ask lower at oracle price and apply conditions for + // Otherwise, cap bid upper or ask lower at oracle price and apply conditions for // quoting single sided liquidity. - // If price is trending UP and: - // 1. oracle price > cached price, disable ask and recalculate bid ranges over new oracle price - // 2. oracle limit < bid lower limit, disable bid and quote for ask side liquidity over bid range - // 3. otherwise, quote for both bid and ask over bid range - // If price is trending DOWN and: - // 4. oracle price < cached price, disable bid and recalculate ask ranges over new oracle price - // 5. oracle limit > ask upper limit, disable ask and quote for bid side liquidity over ask range - // 6. otherwise, quote for both bid and ask over ask range + // A) If price is trending UP and: + // A1. oracle price > bid position, disable ask and recalculate bid ranges over new oracle price + // A2. oracle price < bid position, disable bid and quote for ask side liquidity over bid range + // A3. otherwise, quote for both bid and ask over bid range + // B) If price is trending DOWN and: + // B1. oracle price < ask position, disable bid and recalculate ask ranges over new oracle price + // B2. oracle price > ask position, disable ask and quote for bid side liquidity over ask range + // B3. otherwise, quote for both bid and ask over ask range // If price is RANGING, quote for both bid and ask. // Note that if a position should be disabled, ranges are returned as 0. match trend { Trend::Up => { - if oracle_price > cached_price { - let new_bid_lower = oracle_limit - spread - range; - let new_bid_upper = oracle_limit - spread; + if new_bid_upper > bid_upper { (new_bid_lower, new_bid_upper, 0, 0) - } else if oracle_limit - spread < bid_lower { + } else if new_bid_upper <= bid_lower { (0, 0, bid_lower, bid_upper) } else { // Handle special case: oracle limit + spread can exceed bid upper, so disable ask - if oracle_limit + spread >= bid_upper { - (bid_lower, oracle_limit - spread, 0, 0) + if new_ask_lower >= bid_upper { + (bid_lower, new_bid_upper, 0, 0) } else { - (bid_lower, oracle_limit - spread, oracle_limit + spread, bid_upper) + (bid_lower, new_bid_upper, new_ask_lower, bid_upper) } } }, Trend::Down => { - if oracle_price < cached_price { - let new_ask_lower = oracle_limit + spread; - let new_ask_upper = oracle_limit + spread + range; + if new_ask_lower < ask_lower { (0, 0, new_ask_lower, new_ask_upper) - } else if oracle_limit + spread > ask_upper { + } else if new_ask_lower >= ask_upper { (ask_lower, ask_upper, 0, 0) } else { // Handle special case: oracle limit - spread can be less than ask lower, disable bid - if oracle_limit - spread <= ask_lower { - (0, 0, oracle_limit + spread, ask_upper) + if new_bid_upper <= ask_lower { + (0, 0, new_ask_lower, ask_upper) } else { - (ask_lower, oracle_limit - spread, oracle_limit + spread, ask_upper) + (ask_lower, new_bid_upper, new_ask_lower, ask_upper) } } }, - Trend::Range => (bid_lower, bid_upper, ask_lower, ask_upper), + Trend::Range => (new_bid_lower, new_bid_upper, new_ask_lower, new_ask_upper), } } \ No newline at end of file diff --git a/packages/reversion/src/libraries/store_packing.cairo b/packages/reversion/src/libraries/store_packing.cairo index b6b0645..39560bc 100644 --- a/packages/reversion/src/libraries/store_packing.cairo +++ b/packages/reversion/src/libraries/store_packing.cairo @@ -28,7 +28,7 @@ const MASK_128: u256 = 0xffffffffffffffffffffffffffffffff; pub impl MarketParamsStorePacking of StorePacking { fn pack(value: MarketParams) -> PackedMarketParams { - let mut slab2: u256 = value.spread.into(); + let mut slab2: u256 = value.fee_rate.into(); slab2 += value.range.into() * TWO_POW_32.into(); slab2 += value.min_sources.into() * TWO_POW_64.into(); slab2 += value.max_age.into() * TWO_POW_96.into(); @@ -42,13 +42,13 @@ pub impl MarketParamsStorePacking of StorePacking MarketParams { let slab2: u256 = value.slab2.into(); - let spread: u32 = (slab2 & MASK_32).try_into().unwrap(); + let fee_rate: u16 = (slab2 & MASK_16).try_into().unwrap(); let range: u32 = ((slab2 / TWO_POW_32.into()) & MASK_32).try_into().unwrap(); let min_sources: u32 = ((slab2 / TWO_POW_64.into()) & MASK_32).try_into().unwrap(); let max_age: u64 = ((slab2 / TWO_POW_96.into()) & MASK_32).try_into().unwrap(); MarketParams { - spread, + fee_rate, range, base_currency_id: value.slab0, quote_currency_id: value.slab1, diff --git a/packages/reversion/src/libraries/swap_lib.cairo b/packages/reversion/src/libraries/swap_lib.cairo index 33ddf84..1ad7c0a 100644 --- a/packages/reversion/src/libraries/swap_lib.cairo +++ b/packages/reversion/src/libraries/swap_lib.cairo @@ -4,10 +4,9 @@ use core::integer::{u512, u256_wide_mul}; // Local imports. use haiko_solver_core::types::{MarketState, PositionInfo, SwapParams}; -use haiko_solver_reversion::types::MarketParams; // Haiko imports. -use haiko_lib::math::{math, liquidity_math}; +use haiko_lib::math::{math, fee_math, liquidity_math}; use haiko_lib::constants::{ONE, MAX_SQRT_PRICE}; use haiko_lib::types::i128::I128Trait; @@ -15,12 +14,16 @@ use haiko_lib::types::i128::I128Trait; // // # Arguments // `swap_params` - swap parameters +// `fee_rate` - fee rate // `position` - virtual liquidity position to execute swap over // // # Returns -// `amount_in` - amount swapped in +// `amount_in` - amount swapped in including fees // `amount_out` - amount swapped out -pub fn get_swap_amounts(swap_params: SwapParams, position: PositionInfo,) -> (u256, u256) { +// `fees` - amount of fees +pub fn get_swap_amounts( + swap_params: SwapParams, fee_rate: u16, position: PositionInfo, +) -> (u256, u256, u256) { // Define start and target prices based on swap direction. let start_sqrt_price = if swap_params.is_buy { position.lower_sqrt_price @@ -46,15 +49,16 @@ pub fn get_swap_amounts(swap_params: SwapParams, position: PositionInfo,) -> (u2 }; // Compute swap amounts. - let (amount_in, amount_out, _) = compute_swap_amounts( + let (amount_in, amount_out, fees, _) = compute_swap_amounts( start_sqrt_price, target_sqrt_price, position.liquidity, swap_params.amount, + fee_rate, swap_params.exact_input, ); - (amount_in, amount_out) + (amount_in + fees, amount_out, fees) } // Compute amounts swapped and new price after swapping between two prices. @@ -64,19 +68,22 @@ pub fn get_swap_amounts(swap_params: SwapParams, position: PositionInfo,) -> (u2 // * `target_sqrt_price` - target sqrt price // * `liquidity` - current liquidity // * `amount` - amount remaining to be swapped +// * `fee_rate` - fee rate // * `exact_input` - whether swap amount is exact input or output // // # Returns // * `amount_in` - amount of tokens swapped in // * `amount_out` - amount of tokens swapped out +// * `fee_amount` - amount of fees // * `next_sqrt_price` - next sqrt price pub fn compute_swap_amounts( curr_sqrt_price: u256, target_sqrt_price: u256, liquidity: u128, amount: u256, + fee_rate: u16, exact_input: bool, -) -> (u256, u256, u256) { +) -> (u256, u256, u256, u256) { // Determine whether swap is a buy or sell. let is_buy = target_sqrt_price > curr_sqrt_price; @@ -100,8 +107,9 @@ pub fn compute_swap_amounts( .val; // Calculate next sqrt price. + let amount_less_fee = fee_math::gross_to_net(amount, fee_rate); let filled_max = if exact_input { - amount < amount_in + amount_less_fee < amount_in } else { amount < amount_out }; @@ -110,7 +118,7 @@ pub fn compute_swap_amounts( target_sqrt_price } else { if exact_input { - next_sqrt_price_input(curr_sqrt_price, liquidity, amount, is_buy) + next_sqrt_price_input(curr_sqrt_price, liquidity, amount_less_fee, is_buy) } else { next_sqrt_price_output(curr_sqrt_price, liquidity, amount, is_buy) } @@ -122,7 +130,7 @@ pub fn compute_swap_amounts( if filled_max { amount_in = if exact_input { - amount + amount_less_fee } else { if is_buy { liquidity_math::liquidity_to_quote( @@ -152,8 +160,15 @@ pub fn compute_swap_amounts( }; } + // Calculate fees. + // Amount in is net of fees because we capped amounts by net amount remaining. + // Fees are rounded down by default to prevent overflow when transferring amounts. + // Note that in Uniswap, if target price is not reached, LP takes the remainder + // of the maximum input as fee. We don't do that here. + let fees = fee_math::net_to_fee(amount_in, fee_rate); + // Return amounts. - (amount_in, amount_out, next_sqrt_price) + (amount_in, amount_out, fees, next_sqrt_price) } // Calculates next sqrt price after swapping in certain amount of tokens at given starting diff --git a/packages/reversion/src/types.cairo b/packages/reversion/src/types.cairo index 9c49e03..1c6c574 100644 --- a/packages/reversion/src/types.cairo +++ b/packages/reversion/src/types.cairo @@ -20,7 +20,7 @@ pub enum Trend { // Solver market parameters. // -// * `spread` - default spread between reference price and bid/ask price +// * `fee_rate` - swap fee rate applied to swap amounts // * `range` - default range of spread applied on an imbalanced portfolio // * `base_currency_id` - Pragma oracle base currency id // * `quote_currency_id` - Pragma oracle quote currency id @@ -28,7 +28,7 @@ pub enum Trend { // * `max_age` - maximum age of quoted oracle price #[derive(Drop, Copy, Serde, PartialEq)] pub struct MarketParams { - pub spread: u32, + pub fee_rate: u16, pub range: u32, // Oracle params pub base_currency_id: felt252, @@ -60,7 +60,7 @@ pub struct TrendState { // // * `slab0` - base currency id // * `slab1` - quote currency id -// * `slab2` - `spread` + `range` + `min_sources` + `max_age` +// * `slab2` - `fee_rate` + `range` + `min_sources` + `max_age` #[derive(starknet::Store)] pub struct PackedMarketParams { pub slab0: felt252, diff --git a/scripts/src/configs.ts b/scripts/src/configs.ts index 20faf69..d09b348 100644 --- a/scripts/src/configs.ts +++ b/scripts/src/configs.ts @@ -16,7 +16,7 @@ export type RunnerMarketParams = { quote_token: string; owner: string; is_public: boolean; - min_spread: number; + fee_rate: number; range: number; max_delta: number; max_skew: number; @@ -50,7 +50,7 @@ export const MARKET_PARAMS: RunnerMarketParams[] = [ quote_token: ENV.USDC_ADDRESS, owner: ENV.OWNER_ADDRESS, is_public: true, - min_spread: 25, + fee_rate: 25, range: 5000, max_delta: 500, max_skew: 5000, @@ -81,7 +81,7 @@ export const MARKET_PARAMS: RunnerMarketParams[] = [ quote_token: ENV.USDC_ADDRESS, owner: ENV.OWNER_ADDRESS, is_public: true, - min_spread: 25, + fee_rate: 25, range: 5000, max_delta: 500, max_skew: 5000, @@ -112,7 +112,7 @@ export const MARKET_PARAMS: RunnerMarketParams[] = [ quote_token: ENV.ETH_ADDRESS, owner: ENV.OWNER_ADDRESS, is_public: true, - min_spread: 25, + fee_rate: 25, range: 5000, max_delta: 500, max_skew: 5000, @@ -143,7 +143,7 @@ export const MARKET_PARAMS: RunnerMarketParams[] = [ quote_token: ENV.USDT_ADDRESS, owner: ENV.OWNER_ADDRESS, is_public: true, - min_spread: 5, + fee_rate: 5, range: 100, max_delta: 0, max_skew: 5000, @@ -174,7 +174,7 @@ export const MARKET_PARAMS: RunnerMarketParams[] = [ quote_token: ENV.USDC_ADDRESS, owner: ENV.OWNER_ADDRESS, is_public: true, - min_spread: 25, + fee_rate: 25, range: 5000, max_delta: 500, max_skew: 5000, diff --git a/scripts/src/index.ts b/scripts/src/index.ts index dcdb3fd..24e7bec 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -280,7 +280,7 @@ const execute = async (configs: RunnerConfigs) => { ); try { const marketParams = { - min_spread: market.min_spread, + fee_rate: market.fee_rate, range: market.range, max_delta: market.max_delta, max_skew: market.max_skew, From f476dd54b60e8eff82407e860d235c21252a9501 Mon Sep 17 00:00:00 2001 From: parketh Date: Thu, 12 Sep 2024 09:22:32 +0100 Subject: [PATCH 04/10] minor fixes --- docs/3-events.md | 1 + models/src/libraries/SwapLib.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/3-events.md b/docs/3-events.md index 0f232b2..e52b145 100644 --- a/docs/3-events.md +++ b/docs/3-events.md @@ -43,6 +43,7 @@ pub struct Swap { pub exact_input: bool, pub amount_in: u256, pub amount_out: u256, + pub fees: u256, } ``` diff --git a/models/src/libraries/SwapLib.ts b/models/src/libraries/SwapLib.ts index 7ff66ca..70bc217 100644 --- a/models/src/libraries/SwapLib.ts +++ b/models/src/libraries/SwapLib.ts @@ -45,17 +45,19 @@ export const getSwapAmounts = ( 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: new Decimal(amountIn).add(fees), amountOut, fees }; + return { amountIn: grossAmountIn, amountOut, fees }; }; export const computeSwapAmount = ( From 0365a283a1982e023ff504eb526a9d8846a14e39 Mon Sep 17 00:00:00 2001 From: parketh Date: Thu, 12 Sep 2024 10:48:56 +0100 Subject: [PATCH 05/10] chore: deploy new solvers --- scripts/sepolia.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/sepolia.sh b/scripts/sepolia.sh index 49ba3f0..2b65784 100644 --- a/scripts/sepolia.sh +++ b/scripts/sepolia.sh @@ -30,6 +30,10 @@ starkli deploy --rpc $STARKNET_RPC $REPLICATING_SOLVER_CLASS $OWNER $ORACLE $VAU # Deployments ############################# +# 12 September 2024 +export REPLICATING_SOLVER=0x03423c755f3639b78be2705278bb067e59608ca479ac21d604e15c47e1ebf8be +export REPLICATING_SOLVER_CLASS=0x07236ed1addff06e12721bd5e152bd3eeeecdefb4702539147d7d72701227ad5 + # 22 August 2024 export REPLICATING_SOLVER_CLASS=0x0589858bd41fc0c922ff5c656f3373f7072fb8a69fcd02c8cdad0dce6666d4fb From 15a53db6fa07b696acce0c527ccee561c9b5407d Mon Sep 17 00:00:00 2001 From: parketh Date: Thu, 12 Sep 2024 18:03:16 +0100 Subject: [PATCH 06/10] feat: add fees per share, update fn response, fix tests --- docs/1-technical-architecture.md | 44 ++- docs/3-events.md | 19 ++ models/src/libraries/SwapLib.ts | 71 +++-- models/tests/solver/DebugSwap.test.ts | 28 +- .../src/contracts/mocks/mock_solver.cairo | 8 +- .../mocks/store_packing_contract.cairo | 25 +- packages/core/src/contracts/solver.cairo | 291 ++++++++++++------ packages/core/src/interfaces/ISolver.cairo | 38 ++- .../core/src/libraries/store_packing.cairo | 16 +- .../tests/libraries/test_store_packing.cairo | 19 +- .../core/src/tests/solver/test_deposit.cairo | 174 ++++++----- .../tests/solver/test_deposit_initial.cairo | 88 +++--- .../src/tests/solver/test_get_balances.cairo | 53 ++-- .../core/src/tests/solver/test_pause.cairo | 4 +- .../core/src/tests/solver/test_swap.cairo | 40 +-- .../core/src/tests/solver/test_withdraw.cairo | 199 ++++++------ packages/core/src/types.cairo | 64 ++++ .../src/contracts/replicating_solver.cairo | 6 +- .../replicating/src/libraries/swap_lib.cairo | 9 + .../src/tests/solver/debug_swap.cairo | 26 +- .../src/tests/solver/test_deposit.cairo | 137 ++++----- .../tests/solver/test_deposit_initial.cairo | 66 ++-- .../src/tests/solver/test_e2e.cairo | 84 ++--- .../src/tests/solver/test_swap.cairo | 40 ++- .../src/tests/solver/test_withdraw.cairo | 165 +++++----- 25 files changed, 995 insertions(+), 719 deletions(-) diff --git a/docs/1-technical-architecture.md b/docs/1-technical-architecture.md index 476da28..87c35e5 100644 --- a/docs/1-technical-architecture.md +++ b/docs/1-technical-architecture.md @@ -41,7 +41,8 @@ pub trait ISolverHooks { // # 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. // @@ -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. // @@ -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). @@ -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`. @@ -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 @@ -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. // @@ -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; diff --git a/docs/3-events.md b/docs/3-events.md index e52b145..20363d7 100644 --- a/docs/3-events.md +++ b/docs/3-events.md @@ -53,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` @@ -66,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, } ``` @@ -74,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. diff --git a/models/src/libraries/SwapLib.ts b/models/src/libraries/SwapLib.ts index 70bc217..f06baaa 100644 --- a/models/src/libraries/SwapLib.ts +++ b/models/src/libraries/SwapLib.ts @@ -3,19 +3,31 @@ 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, - swapFeeRate: Decimal.Value, - thresholdSqrtPrice: Decimal.Value | null, - thresholdAmount: Decimal.Value | null, - lowerSqrtPrice: Decimal.Value, - upperSqrtPrice: Decimal.Value, - liquidity: Decimal.Value, - baseDecimals: number, - quoteDecimals: number -): { +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; @@ -36,14 +48,14 @@ export const getSwapAmounts = ( ? Decimal.max(thresholdSqrtPrice, scaledLowerSqrtPrice) : scaledLowerSqrtPrice; - const { amountIn, amountOut, fees, nextSqrtPrice } = computeSwapAmount( - startSqrtPrice, + const { amountIn, amountOut, fees, nextSqrtPrice } = computeSwapAmount({ + currSqrtPrice: startSqrtPrice, targetSqrtPrice, liquidity, - amount, + amountRem: amount, swapFeeRate, - exactInput - ); + exactInput, + }); const grossAmountIn = new Decimal(amountIn).add(fees); @@ -60,14 +72,21 @@ export const getSwapAmounts = ( return { amountIn: grossAmountIn, amountOut, fees }; }; -export const computeSwapAmount = ( - currSqrtPrice: Decimal.Value, - targetSqrtPrice: Decimal.Value, - liquidity: Decimal.Value, - amountRem: Decimal.Value, - swapFeeRate: 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"; diff --git a/models/tests/solver/DebugSwap.test.ts b/models/tests/solver/DebugSwap.test.ts index 355f81d..6b09106 100644 --- a/models/tests/solver/DebugSwap.test.ts +++ b/models/tests/solver/DebugSwap.test.ts @@ -7,15 +7,15 @@ import { getSwapAmounts } from "../../src/libraries/SwapLib"; const isBuy = false; const exactInput = false; -const amount = "10000000000"; -const swapFeeRate = 25; -const range = 5000; -const maxDelta = 500; -const oraclePrice = "0.37067545"; -const baseReserves = "268762.195878807302077639"; -const quoteReserves = "96834.519855"; +const amount = "1"; +const swapFeeRate = 0.003; +const range = 11400; +const maxDelta = 0; +const oraclePrice = "0.00016718"; +const baseReserves = "10000"; +const quoteReserves = "16.707738619115042000"; const baseDecimals = 18; -const quoteDecimals = 6; +const quoteDecimals = 18; const delta = getDelta(maxDelta, baseReserves, quoteReserves, oraclePrice); const { lowerLimit, upperLimit } = getVirtualPositionRange( @@ -34,17 +34,17 @@ const { lowerSqrtPrice, upperSqrtPrice, liquidity } = getVirtualPosition( isBuy ? baseReserves : quoteReserves ); console.log({ lowerSqrtPrice, upperSqrtPrice, liquidity }); -const { amountIn, amountOut } = getSwapAmounts( +const { amountIn, amountOut } = getSwapAmounts({ isBuy, exactInput, - swapFeeRate, amount, - null, - null, + swapFeeRate, + thresholdSqrtPrice: null, + thresholdAmount: null, lowerSqrtPrice, upperSqrtPrice, liquidity, baseDecimals, - quoteDecimals -); + quoteDecimals, +}); console.log({ amountIn, amountOut }); diff --git a/packages/core/src/contracts/mocks/mock_solver.cairo b/packages/core/src/contracts/mocks/mock_solver.cairo index 2a273c7..99d0fe6 100644 --- a/packages/core/src/contracts/mocks/mock_solver.cairo +++ b/packages/core/src/contracts/mocks/mock_solver.cairo @@ -41,7 +41,7 @@ pub mod MockSolver { use haiko_solver_core::contracts::solver::SolverComponent; use haiko_solver_core::libraries::math::fast_sqrt; use haiko_solver_core::interfaces::ISolver::ISolverHooks; - use haiko_solver_core::types::{PositionInfo, MarketState, MarketInfo, SwapParams}; + use haiko_solver_core::types::{PositionInfo, MarketState, MarketInfo, SwapParams, SwapAmounts}; // Haiko imports. use haiko_lib::math::{math, fee_math}; @@ -114,7 +114,7 @@ pub mod MockSolver { // * `fees` - fees fn quote( self: @ContractState, market_id: felt252, swap_params: SwapParams, - ) -> (u256, u256, u256) { + ) -> SwapAmounts { // Run validity checks. let state: MarketState = self.solver.market_state.read(market_id); let market_info: MarketInfo = self.solver.market_info.read(market_id); @@ -165,7 +165,9 @@ pub mod MockSolver { }; let fees_capped = fee_math::calc_fee(amount_in_capped, fee_rate); - (amount_in_capped, amount_out_capped, fees_capped) + SwapAmounts { + amount_in: amount_in_capped, amount_out: amount_out_capped, fees: fees_capped, + } } // Initial token supply to mint when first depositing to a market. diff --git a/packages/core/src/contracts/mocks/store_packing_contract.cairo b/packages/core/src/contracts/mocks/store_packing_contract.cairo index 78245dd..78f9a23 100644 --- a/packages/core/src/contracts/mocks/store_packing_contract.cairo +++ b/packages/core/src/contracts/mocks/store_packing_contract.cairo @@ -1,16 +1,24 @@ -use haiko_solver_core::types::MarketState; +use haiko_solver_core::types::{MarketState, FeesPerShare}; #[starknet::interface] pub trait IStorePackingContract { fn get_market_state(self: @TContractState, market_id: felt252) -> MarketState; fn set_market_state(ref self: TContractState, market_id: felt252, market_state: MarketState); + + fn get_fees_per_share(self: @TContractState, market_id: felt252) -> FeesPerShare; + + fn set_fees_per_share( + ref self: TContractState, market_id: felt252, fees_per_share: FeesPerShare + ); } #[starknet::contract] pub mod StorePackingContract { - use haiko_solver_core::types::MarketState; - use haiko_solver_core::libraries::store_packing::MarketStateStorePacking; + use haiko_solver_core::types::{MarketState, FeesPerShare}; + use haiko_solver_core::libraries::store_packing::{ + MarketStateStorePacking, FeesPerShareStorePacking + }; use super::IStorePackingContract; //////////////////////////////// @@ -20,6 +28,7 @@ pub mod StorePackingContract { #[storage] struct Storage { market_state: LegacyMap::, + fees_per_share: LegacyMap::, } #[constructor] @@ -35,6 +44,10 @@ pub mod StorePackingContract { self.market_state.read(market_id) } + fn get_fees_per_share(self: @ContractState, market_id: felt252) -> FeesPerShare { + self.fees_per_share.read(market_id) + } + //////////////////////////////// // EXTERNAL FUNCTIONS //////////////////////////////// @@ -44,5 +57,11 @@ pub mod StorePackingContract { ) { self.market_state.write(market_id, market_state); } + + fn set_fees_per_share( + ref self: ContractState, market_id: felt252, fees_per_share: FeesPerShare + ) { + self.fees_per_share.write(market_id, fees_per_share); + } } } diff --git a/packages/core/src/contracts/solver.cairo b/packages/core/src/contracts/solver.cairo index 54206d1..31f9fdc 100644 --- a/packages/core/src/contracts/solver.cairo +++ b/packages/core/src/contracts/solver.cairo @@ -12,13 +12,16 @@ pub mod SolverComponent { // Local imports. use haiko_solver_core::libraries::{ - id, erc20_versioned_call, store_packing::MarketStateStorePacking + id, erc20_versioned_call, store_packing::{MarketStateStorePacking, FeesPerShareStorePacking} }; use haiko_solver_core::interfaces::{ ISolver::{ISolver, ISolverHooksDispatcher, ISolverHooksDispatcherTrait}, IVaultToken::{IVaultTokenDispatcher, IVaultTokenDispatcherTrait}, }; - use haiko_solver_core::types::{MarketInfo, MarketState, PositionInfo, SwapParams}; + use haiko_solver_core::types::{ + MarketInfo, MarketState, FeesPerShare, PositionInfo, SwapParams, Amounts, AmountsWithShares, + SwapAmounts + }; // Haiko imports. use haiko_lib::{math::{math, fee_math}, constants::{ONE, LOG2_1_00001, MAX_FEE_RATE}}; @@ -47,6 +50,10 @@ pub mod SolverComponent { market_info: LegacyMap::, // Indexed by market id market_state: LegacyMap::, + // Indexed by market id + fees_per_share: LegacyMap::, + // Indexed by (market_id, user) + user_fees_per_share: LegacyMap::<(felt252, ContractAddress), FeesPerShare>, // Indexed by market_id withdraw_fee_rate: LegacyMap::, // Indexed by asset @@ -111,6 +118,8 @@ pub mod SolverComponent { pub market_id: felt252, pub base_amount: u256, pub quote_amount: u256, + pub base_fees: u256, + pub quote_fees: u256, pub shares: u256, } @@ -276,11 +285,14 @@ pub mod SolverComponent { // * `quote_amount` - total quote tokens owned // * `base_fees` - total base fees owned // * `quote_fees` - total quote fees owned - fn get_balances( - self: @ComponentState, market_id: felt252 - ) -> (u256, u256, u256, u256) { + fn get_balances(self: @ComponentState, market_id: felt252) -> Amounts { let state: MarketState = self.market_state.read(market_id); - (state.base_reserves, state.quote_reserves, state.base_fees, state.quote_fees) + Amounts { + base_amount: state.base_reserves, + quote_amount: state.quote_reserves, + base_fees: state.base_fees, + quote_fees: state.quote_fees + } } // Get user token balances held in solver market. @@ -296,27 +308,39 @@ pub mod SolverComponent { // * `quote_fees` - quote fees owned by user fn get_user_balances( self: @ComponentState, user: ContractAddress, market_id: felt252 - ) -> (u256, u256, u256, u256) { + ) -> Amounts { let state: MarketState = self.market_state.read(market_id); // Handle non-existent vault token. if state.vault_token == contract_address_const::<0x0>() { - return (0, 0, 0, 0); + return Default::default(); } // Handle divison by 0 case. let vault_token = ERC20ABIDispatcher { contract_address: state.vault_token }; let total_shares = vault_token.totalSupply(); if total_shares == 0 { - return (0, 0, 0, 0); + return Default::default(); } - // Calculate balances and shares + // Calculate user balances let user_shares = vault_token.balanceOf(user); - let (base_balance, quote_balance, base_fees, quote_fees) = self.get_balances(market_id); - let user_base = math::mul_div(base_balance, user_shares, total_shares, false); - let user_quote = math::mul_div(quote_balance, user_shares, total_shares, false); - let user_base_fees = math::mul_div(base_fees, user_shares, total_shares, false); - let user_quote_fees = math::mul_div(quote_fees, user_shares, total_shares, false); + let res = self.get_balances(market_id); + let base_amount = math::mul_div(res.base_amount, user_shares, total_shares, false); + let quote_amount = math::mul_div(res.quote_amount, user_shares, total_shares, false); + + // Calculate user fee balances + let market_fps: FeesPerShare = self.fees_per_share.read(market_id); + let user_fps: FeesPerShare = self.user_fees_per_share.read((market_id, user)); + let base_fees = if user_shares == 0 || market_fps.base_fps == user_fps.base_fps { + 0 + } else { + math::mul_div(user_shares, market_fps.base_fps - user_fps.base_fps, ONE, false) + }; + let quote_fees = if user_shares == 0 || market_fps.quote_fps == user_fps.quote_fps { + 0 + } else { + math::mul_div(user_shares, market_fps.quote_fps - user_fps.quote_fps, ONE, false) + }; - (user_base, user_quote, user_base_fees, user_quote_fees) + Amounts { base_amount, quote_amount, base_fees, quote_fees } } // Create market for solver. @@ -395,7 +419,7 @@ pub mod SolverComponent { // * `fees` - fees fn swap( ref self: ComponentState, market_id: felt252, swap_params: SwapParams, - ) -> (u256, u256, u256) { + ) -> SwapAmounts { // Run validity checks. let state: MarketState = self.market_state.read(market_id); let market_info: MarketInfo = self.market_info.read(market_id); @@ -407,7 +431,8 @@ pub mod SolverComponent { // Get amounts. let solver_hooks = ISolverHooksDispatcher { contract_address: get_contract_address() }; - let (amount_in, amount_out, fees) = solver_hooks.quote(market_id, swap_params); + let SwapAmounts { amount_in, amount_out, fees } = solver_hooks + .quote(market_id, swap_params); // Check amounts non-zero and satisfy threshold amounts. assert(amount_in != 0 && amount_out != 0, 'AmountZero'); @@ -422,6 +447,17 @@ pub mod SolverComponent { } } + // Update fees per share. + let mut market_fps: FeesPerShare = self.fees_per_share.read(market_id); + let vault_token = ERC20ABIDispatcher { contract_address: state.vault_token }; + let total_supply = vault_token.totalSupply(); + if swap_params.is_buy { + market_fps.quote_fps += math::mul_div(fees, ONE, total_supply, false); + } else { + market_fps.base_fps += math::mul_div(fees, ONE, total_supply, false); + } + self.fees_per_share.write(market_id, market_fps); + // Transfer tokens. let market_info: MarketInfo = self.market_info.read(market_id); let base_token = ERC20ABIDispatcher { contract_address: market_info.base_token }; @@ -470,7 +506,7 @@ pub mod SolverComponent { ) ); - (amount_in, amount_out, fees) + SwapAmounts { amount_in, amount_out, fees } } // Deposit initial liquidity to market. @@ -485,13 +521,15 @@ pub mod SolverComponent { // # Returns // * `base_deposit` - base asset deposited // * `quote_deposit` - quote asset deposited + // * `base_fees` - base fees withdrawn (gross of withdraw fees) + // * `quote_fees` - quote fees withdrawn (gross of withdraw fees) // * `shares` - pool shares minted in the form of liquidity fn deposit_initial( ref self: ComponentState, market_id: felt252, base_amount: u256, quote_amount: u256 - ) -> (u256, u256, u256) { + ) -> AmountsWithShares { // Fetch market info and state. let market_info = self.market_info.read(market_id); let mut state: MarketState = self.market_state.read(market_id); @@ -504,6 +542,9 @@ pub mod SolverComponent { self.assert_market_owner(market_id); } + // Collect fees (if any) and set / reset fee per share values. + let (base_fees, quote_fees) = self._collect_fees(market_id); + // Cap deposit at available. let caller = get_caller_address(); let base_token = ERC20ABIDispatcher { contract_address: market_info.base_token }; @@ -554,12 +595,20 @@ pub mod SolverComponent { caller, base_amount: base_deposit, quote_amount: quote_deposit, + base_fees, + quote_fees, shares } ) ); - (base_deposit, quote_deposit, shares) + AmountsWithShares { + base_amount: base_deposit, + quote_amount: quote_deposit, + base_fees, + quote_fees, + shares + } } // Same as `deposit_initial`, but with a referrer. @@ -573,6 +622,8 @@ pub mod SolverComponent { // # Returns // * `base_deposit` - base asset deposited // * `quote_deposit` - quote asset deposited + // * `base_fees` - base fees withdrawn (gross of withdraw fees) + // * `quote_fees` - quote fees withdrawn (gross of withdraw fees) // * `shares` - pool shares minted in the form of liquidity fn deposit_initial_with_referrer( ref self: ComponentState, @@ -580,7 +631,7 @@ pub mod SolverComponent { base_amount: u256, quote_amount: u256, referrer: ContractAddress - ) -> (u256, u256, u256) { + ) -> AmountsWithShares { // Check referrer is non-null. assert(referrer != contract_address_const::<0x0>(), 'ReferrerZero'); @@ -607,13 +658,15 @@ pub mod SolverComponent { // # Returns // * `base_deposit` - base asset deposited // * `quote_deposit` - quote asset deposited + // * `base_fees` - base fees withdrawn (gross of withdraw fees) + // * `quote_fees` - quote fees withdrawn (gross of withdraw fees) // * `shares` - pool shares minted fn deposit( ref self: ComponentState, market_id: felt252, base_amount: u256, quote_amount: u256 - ) -> (u256, u256, u256) { + ) -> AmountsWithShares { // Fetch market info and state. let market_info = self.market_info.read(market_id); let mut state: MarketState = self.market_state.read(market_id); @@ -626,6 +679,9 @@ pub mod SolverComponent { self.assert_market_owner(market_id); } + // Collect fees (if any) and set / reset fee per share values. + let (base_fees, quote_fees) = self._collect_fees(market_id); + // Evaluate the lower of requested and available balances. let caller = get_caller_address(); let base_token = ERC20ABIDispatcher { contract_address: market_info.base_token }; @@ -670,6 +726,9 @@ pub mod SolverComponent { } // Mint shares. + // If the market has existing accrued swap fees, we will be buying into a portion of those + // fees, so the minted shares must be calculated as a portion of total reserves, inclusive + // of accrued fees. We add let mut shares = 0; if market_info.is_public { let total_supply = ERC20ABIDispatcher { contract_address: state.vault_token } @@ -697,12 +756,20 @@ pub mod SolverComponent { caller, base_amount: base_deposit, quote_amount: quote_deposit, + base_fees, + quote_fees, shares, } ) ); - (base_deposit, quote_deposit, shares) + AmountsWithShares { + base_amount: base_deposit, + quote_amount: quote_deposit, + base_fees, + quote_fees, + shares + } } // Same as `deposit`, but with a referrer. @@ -716,6 +783,8 @@ pub mod SolverComponent { // # Returns // * `base_deposit` - base asset deposited // * `quote_deposit` - quote asset deposited + // * `base_fees` - base fees withdrawn (gross of withdraw fees) + // * `quote_fees` - quote fees withdrawn (gross of withdraw fees) // * `shares` - pool shares minted fn deposit_with_referrer( ref self: ComponentState, @@ -723,7 +792,7 @@ pub mod SolverComponent { base_amount: u256, quote_amount: u256, referrer: ContractAddress - ) -> (u256, u256, u256) { + ) -> AmountsWithShares { // Check referrer is non-null. assert(referrer != contract_address_const::<0x0>(), 'ReferrerZero'); @@ -745,13 +814,13 @@ pub mod SolverComponent { // * `shares` - pool shares to burn // // # Returns - // * `base_amount` - base asset withdrawn, including fees - // * `quote_amount` - quote asset withdrawn, including fees + // * `base_amount` - base asset withdrawn, excluding fees + // * `quote_amount` - quote asset withdrawn, excluding fees // * `base_fees` - base fees withdrawn // * `quote_fees` - quote fees withdrawn fn withdraw_public( ref self: ComponentState, market_id: felt252, shares: u256 - ) -> (u256, u256, u256, u256) { + ) -> Amounts { // Fetch state. let market_info = self.market_info.read(market_id); let mut state: MarketState = self.market_state.read(market_id); @@ -766,22 +835,23 @@ pub mod SolverComponent { let total_supply = vault_token.totalSupply(); assert(total_supply != 0, 'SupplyZero'); + // Collect fees (if any) and set / reset fee per share values. + let (base_fees, quote_fees) = self._collect_fees(market_id); + // Burn shares. IVaultTokenDispatcher { contract_address: state.vault_token }.burn(caller, shares); // Calculate share of reserves to withdraw. Commit state changes. let base_amount = math::mul_div(state.base_reserves, shares, total_supply, false); let quote_amount = math::mul_div(state.quote_reserves, shares, total_supply, false); - let base_fees = math::mul_div(state.base_fees, shares, total_supply, false); - let quote_fees = math::mul_div(state.quote_fees, shares, total_supply, false); state.base_reserves -= base_amount; state.quote_reserves -= quote_amount; - state.base_fees -= base_fees; - state.quote_fees -= quote_fees; self.market_state.write(market_id, state); // Deduct applicable fees, emit events and return withdrawn amounts. - self._withdraw(market_id, base_amount, quote_amount, base_fees, quote_fees, shares) + self._withdraw(market_id, base_amount, quote_amount, base_fees, quote_fees, shares); + + Amounts { base_amount, quote_amount, base_fees, quote_fees } } // Withdraw exact token amounts from market. @@ -793,8 +863,8 @@ pub mod SolverComponent { // * `quote_amount` - quote amount requested // // # Returns - // * `base_amount` - base asset withdrawn, including fees - // * `quote_amount` - quote asset withdrawn, including fees + // * `base_amount` - base asset withdrawn, excluding fees + // * `quote_amount` - quote asset withdrawn, excluding fees // * `base_fees` - base fees withdrawn // * `quote_fees` - quote fees withdrawn fn withdraw_private( @@ -802,7 +872,7 @@ pub mod SolverComponent { market_id: felt252, base_amount: u256, quote_amount: u256 - ) -> (u256, u256, u256, u256) { + ) -> Amounts { // Fetch state. let market_info = self.market_info.read(market_id); let mut state: MarketState = self.market_state.read(market_id); @@ -812,43 +882,24 @@ pub mod SolverComponent { assert(!market_info.is_public, 'UseWithdrawPublic'); self.assert_market_owner(market_id); + // Collect fees (if any) and set / reset fee per share values. + let (base_fees, quote_fees) = self._collect_fees(market_id); + // Cap withdraw amount at available. Commit state changes. - let base_withdraw = min(base_amount, state.base_reserves + state.base_fees); - let quote_withdraw = min(quote_amount, state.quote_reserves + state.quote_fees); - let base_fees_withdraw = if base_withdraw < state.base_reserves + state.base_fees { - math::mul_div( - state.base_fees, base_withdraw, state.base_reserves + state.base_fees, false - ) - } else { - state.base_fees - }; - let quote_fees_withdraw = if quote_withdraw < state.quote_reserves + state.quote_fees { - math::mul_div( - state.quote_fees, quote_withdraw, state.quote_reserves + state.quote_fees, false - ) - } else { - state.quote_fees - }; - let base_withdraw_excl_fees = base_withdraw - base_fees_withdraw; - let quote_withdraw_excl_fees = quote_withdraw - quote_fees_withdraw; + let base_withdraw = min(base_amount, state.base_reserves); + let quote_withdraw = min(quote_amount, state.quote_reserves); // Commit state updates. - state.base_reserves -= base_withdraw_excl_fees; - state.quote_reserves -= quote_withdraw_excl_fees; - state.base_fees -= base_fees_withdraw; - state.quote_fees -= quote_fees_withdraw; + state.base_reserves -= base_withdraw; + state.quote_reserves -= quote_withdraw; self.market_state.write(market_id, state); // Deduct applicable fees, emit events and return withdrawn amounts. - self - ._withdraw( - market_id, - base_withdraw_excl_fees, - quote_withdraw_excl_fees, - base_fees_withdraw, - quote_fees_withdraw, - 0 - ) + self._withdraw(market_id, base_withdraw, quote_withdraw, base_fees, quote_fees, 0); + + Amounts { + base_amount: base_withdraw, quote_amount: quote_withdraw, base_fees, quote_fees + } } // Collect withdrawal fees. @@ -1058,22 +1109,95 @@ pub mod SolverComponent { token } + // Internal function to collect outstanding fee balances (if any) and update fee per share values. + // Returned amounts are gross of withdraw fees. + // + // # Arguments + // * `market_id` - market id + // + // # Returns + // * `base_fees` - collected base fees + // * `quote_fees` - collected quote fees + fn _collect_fees( + ref self: ComponentState, market_id: felt252, + ) -> (u256, u256) { + // Check if user has accrued fee balances. + let user = get_caller_address(); + let market_info: MarketInfo = self.market_info.read(market_id); + let mut market_state: MarketState = self.market_state.read(market_id); + let vault_token = ERC20ABIDispatcher { contract_address: market_state.vault_token }; + let user_shares = vault_token.balanceOf(user); + let user_fps: FeesPerShare = self.user_fees_per_share.read((market_id, user)); + let fps: FeesPerShare = self.fees_per_share.read(market_id); + + // No accrued fee balances exist. Set user fps to pool fps and return. + if user_shares == 0 + || (user_fps.base_fps == fps.base_fps && user_fps.quote_fps == fps.quote_fps) { + self.user_fees_per_share.write((market_id, user), fps); + return (0, 0); + } + + // Accrued fee balances exist, calculate fee balances to collect. + let base_fees = math::mul_div( + user_shares, fps.base_fps - user_fps.base_fps, ONE, false + ); + let quote_fees = math::mul_div( + user_shares, fps.quote_fps - user_fps.quote_fps, ONE, false + ); + + // Update fee reserves and commit state changes. + market_state.base_fees -= base_fees; + market_state.quote_fees -= quote_fees; + self.market_state.write(market_id, market_state); + + // Calculate withdraw fees. + let withdraw_fee_rate = self.withdraw_fee_rate.read(market_id); + let mut base_withdraw_fees = 0; + let mut quote_withdraw_fees = 0; + if withdraw_fee_rate != 0 { + base_withdraw_fees = fee_math::calc_fee(base_fees, withdraw_fee_rate); + quote_withdraw_fees = fee_math::calc_fee(quote_fees, withdraw_fee_rate); + } + + // Update withdraw fee balances. + if base_withdraw_fees != 0 { + let mut base_withdraw_fees_bal = self.withdraw_fees.read(market_info.base_token); + base_withdraw_fees_bal += base_withdraw_fees; + self.withdraw_fees.write(market_info.base_token, base_withdraw_fees_bal); + } + if quote_withdraw_fees != 0 { + let mut quote_withdraw_fees_bal = self.withdraw_fees.read(market_info.quote_token); + quote_withdraw_fees_bal += quote_withdraw_fees; + self.withdraw_fees.write(market_info.quote_token, quote_withdraw_fees_bal); + } + + // Transfer fees (net of withdraw fees) to user. + if base_fees != 0 { + let base_token = ERC20ABIDispatcher { contract_address: market_info.base_token }; + base_token.transfer(user, base_fees - base_withdraw_fees); + } + if quote_fees != 0 { + let quote_token = ERC20ABIDispatcher { contract_address: market_info.quote_token }; + quote_token.transfer(user, quote_fees - quote_withdraw_fees); + } + + // Set user fps to pool fps. + self.user_fees_per_share.write((market_id, user), fps); + + // Return collected fees gross of withdraw fees. + (base_fees, quote_fees) + } + // Internal function to withdraw funds from market. // Amounts passed in should be before deducting applicable withdraw fees. // // # Arguments // * `market_id` - market id - // * `base_amount` - amount of base assets to withdraw, excluding earned swap fees and withdraw fees - // * `quote_amount` - amount of quote assets to withdraw, excluding earned swap fees and withdraw fees - // * `base_fees` - amount of earned base fees, gross of withdraw fees - // * `quote_fees` - amount of earned quote fees, gross of withdraw fees + // * `base_amount` - amount of base assets to withdraw, excluding swap fees and gross of withdraw fees + // * `quote_amount` - amount of quote assets to withdraw, excluding swap fees and gross of withdraw fees + // * `base_fees` - earned base fees, gross of withdraw fees + // * `quote_fees` - earned quote fees, gross of withdraw fees // * `shares` - pool shares to burn for public vaults, or 0 for private vaults - // - // # Returns - // * `base_withdraw` - base assets withdrawn, including fees - // * `quote_withdraw` - quote assets withdrawn, including fees - // * `base_fees` - base fees withdrawn - // * `quote_fees` - quote fees withdrawn fn _withdraw( ref self: ComponentState, market_id: felt252, @@ -1082,12 +1206,10 @@ pub mod SolverComponent { base_fees: u256, quote_fees: u256, shares: u256 - ) -> (u256, u256, u256, u256) { + ) { // Initialise values. - let mut base_withdraw = base_amount + base_fees; - let mut quote_withdraw = quote_amount + quote_fees; - let mut base_fees_withdraw = base_fees; - let mut quote_fees_withdraw = quote_fees; + let mut base_withdraw = base_amount; + let mut quote_withdraw = quote_amount; let mut base_withdraw_fees = 0; let mut quote_withdraw_fees = 0; @@ -1098,8 +1220,6 @@ pub mod SolverComponent { quote_withdraw_fees = fee_math::calc_fee(quote_withdraw, withdraw_fee_rate); base_withdraw -= base_withdraw_fees; quote_withdraw -= quote_withdraw_fees; - base_fees_withdraw -= fee_math::calc_fee(base_fees, withdraw_fee_rate); - quote_fees_withdraw -= fee_math::calc_fee(quote_fees, withdraw_fee_rate); } // Transfer tokens to caller. @@ -1169,9 +1289,6 @@ pub mod SolverComponent { ) ); } - - // Return withdrawn amounts. - (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) } } } diff --git a/packages/core/src/interfaces/ISolver.cairo b/packages/core/src/interfaces/ISolver.cairo index 026d110..d861edf 100644 --- a/packages/core/src/interfaces/ISolver.cairo +++ b/packages/core/src/interfaces/ISolver.cairo @@ -3,7 +3,9 @@ use starknet::ContractAddress; use starknet::class_hash::ClassHash; // Local imports. -use haiko_solver_core::types::{MarketState, MarketInfo, PositionInfo, SwapParams}; +use haiko_solver_core::types::{ + MarketState, MarketInfo, PositionInfo, SwapParams, Amounts, AmountsWithShares, SwapAmounts +}; #[starknet::interface] pub trait ISolver { @@ -53,7 +55,7 @@ pub trait ISolver { // * `quote_amount` - total quote tokens owned // * `base_fees` - total base fees owned // * `quote_fees` - total quote fees owned - fn get_balances(self: @TContractState, market_id: felt252) -> (u256, u256, u256, u256); + fn get_balances(self: @TContractState, market_id: felt252) -> Amounts; // Get user token balances held in solver market. // @@ -68,7 +70,7 @@ pub trait ISolver { // * `quote_fees` - quote fees owned by user fn get_user_balances( self: @TContractState, user: ContractAddress, market_id: felt252 - ) -> (u256, u256, u256, u256); + ) -> Amounts; // Create market for solver. @@ -95,9 +97,7 @@ pub trait ISolver { // * `amount_in` - amount in including fees // * `amount_out` - amount out // * `fees` - fees - fn swap( - ref self: TContractState, market_id: felt252, swap_params: SwapParams, - ) -> (u256, u256, u256); + fn swap(ref self: TContractState, market_id: felt252, swap_params: SwapParams,) -> SwapAmounts; // Deposit initial liquidity to market. // Should be used whenever total deposits in a market are zero. This can happen both @@ -111,10 +111,12 @@ pub trait ISolver { // # Returns // * `base_deposit` - base asset deposited // * `quote_deposit` - quote asset deposited + // * `base_fees` - base fees deposited + // * `quote_fees` - quote fees deposited // * `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; // Same as `deposit_initial`, but with a referrer. // @@ -127,6 +129,8 @@ pub trait ISolver { // # Returns // * `base_deposit` - base asset deposited // * `quote_deposit` - quote asset deposited + // * `base_fees` - base fees deposited + // * `quote_fees` - quote fees deposited // * `shares` - pool shares minted in the form of liquidity fn deposit_initial_with_referrer( ref self: TContractState, @@ -134,7 +138,7 @@ pub trait ISolver { base_amount: u256, quote_amount: u256, referrer: ContractAddress - ) -> (u256, u256, u256); + ) -> AmountsWithShares; // Deposit liquidity to market. // For public markets, this will take the lower of requested and available balances, @@ -149,10 +153,12 @@ pub trait ISolver { // # Returns // * `base_deposit` - base asset deposited // * `quote_deposit` - quote asset deposited + // * `base_fees` - base fees deposited + // * `quote_fees` - quote fees deposited // * `shares` - pool shares minted fn deposit( ref self: TContractState, market_id: felt252, base_amount: u256, quote_amount: u256 - ) -> (u256, u256, u256); + ) -> AmountsWithShares; // Same as `deposit`, but with a referrer. // @@ -165,6 +171,8 @@ pub trait ISolver { // # Returns // * `base_amount` - base asset deposited // * `quote_amount` - quote asset deposited + // * `base_fees` - base fees deposited + // * `quote_fees` - quote fees deposited // * `shares` - pool shares minted fn deposit_with_referrer( ref self: TContractState, @@ -172,7 +180,7 @@ pub trait ISolver { base_amount: u256, quote_amount: u256, referrer: ContractAddress - ) -> (u256, u256, u256); + ) -> AmountsWithShares; // Burn pool shares and withdraw funds from market. // Called for public vaults. For private vaults, use `withdraw_private`. @@ -186,9 +194,7 @@ pub trait ISolver { // * `quote_amount` - quote asset withdrawn, including fees // * `base_fees` - base fees withdrawn // * `quote_fees` - quote fees withdrawn - fn withdraw_public( - ref self: TContractState, market_id: felt252, shares: u256 - ) -> (u256, u256, u256, u256); + fn withdraw_public(ref self: TContractState, market_id: felt252, shares: u256) -> Amounts; // Withdraw exact token amounts from market. // Called for private vaults. For public vaults, use `withdraw_public`. @@ -205,7 +211,7 @@ pub trait ISolver { // * `quote_fees` - quote fees withdrawn fn withdraw_private( ref self: TContractState, market_id: felt252, base_amount: u256, quote_amount: u256 - ) -> (u256, u256, u256, u256); + ) -> Amounts; // Collect withdrawal fees. // Only callable by contract owner. @@ -277,9 +283,7 @@ pub trait ISolverHooks { // * `amount_in` - amount in including fees // * `amount_out` - amount out // * `fees` - fees - fn quote( - self: @TContractState, market_id: felt252, swap_params: SwapParams, - ) -> (u256, u256, u256); + fn quote(self: @TContractState, market_id: felt252, swap_params: SwapParams,) -> SwapAmounts; // Get the initial token supply to mint when first depositing to a market. // diff --git a/packages/core/src/libraries/store_packing.cairo b/packages/core/src/libraries/store_packing.cairo index f4c2b49..8cce158 100644 --- a/packages/core/src/libraries/store_packing.cairo +++ b/packages/core/src/libraries/store_packing.cairo @@ -2,7 +2,7 @@ use starknet::storage_access::StorePacking; // Local imports. -use haiko_solver_core::types::{MarketState, PackedMarketState}; +use haiko_solver_core::types::{MarketState, FeesPerShare, PackedMarketState, PackedFeesPerShare}; //////////////////////////////// // IMPLS @@ -31,6 +31,20 @@ pub(crate) impl MarketStateStorePacking of StorePacking { + fn pack(value: FeesPerShare) -> PackedFeesPerShare { + let slab0: felt252 = value.base_fps.try_into().unwrap(); + let slab1: felt252 = value.quote_fps.try_into().unwrap(); + PackedFeesPerShare { slab0, slab1 } + } + + fn unpack(value: PackedFeesPerShare) -> FeesPerShare { + FeesPerShare { + base_fps: value.slab0.try_into().unwrap(), quote_fps: value.slab1.try_into().unwrap(), + } + } +} + //////////////////////////////// // INTERNAL HELPERS //////////////////////////////// diff --git a/packages/core/src/tests/libraries/test_store_packing.cairo b/packages/core/src/tests/libraries/test_store_packing.cairo index 6529916..1a7ef57 100644 --- a/packages/core/src/tests/libraries/test_store_packing.cairo +++ b/packages/core/src/tests/libraries/test_store_packing.cairo @@ -3,7 +3,7 @@ use starknet::syscalls::deploy_syscall; use starknet::contract_address::contract_address_const; // Local imports. -use haiko_solver_core::types::MarketState; +use haiko_solver_core::types::{MarketState, FeesPerShare}; use haiko_solver_core::contracts::mocks::store_packing_contract::{ StorePackingContract, IStorePackingContractDispatcher, IStorePackingContractDispatcherTrait }; @@ -25,7 +25,7 @@ fn before() -> IStorePackingContractDispatcher { //////////////////////////////// // TESTS //////////////////////////////// -/// + #[test] fn test_store_packing_market_state() { let store_packing_contract = before(); @@ -49,3 +49,18 @@ fn test_store_packing_market_state() { assert(unpacked.is_paused == market_state.is_paused, 'Market state: is paused'); assert(unpacked.vault_token == market_state.vault_token, 'Market state: vault token'); } + +#[test] +fn test_store_packing_fees_per_share() { + let store_packing_contract = before(); + + let fees_per_share = FeesPerShare { + base_fps: 1389123122000000000, quote_fps: 240129999999999999, + }; + + store_packing_contract.set_fees_per_share(1, fees_per_share); + let unpacked = store_packing_contract.get_fees_per_share(1); + + assert(unpacked.base_fps == fees_per_share.base_fps, 'Fees per share: base fps'); + assert(unpacked.quote_fps == fees_per_share.quote_fps, 'Fees per share: quote fps'); +} diff --git a/packages/core/src/tests/solver/test_deposit.cairo b/packages/core/src/tests/solver/test_deposit.cairo index 5bcea38..67a1db9 100644 --- a/packages/core/src/tests/solver/test_deposit.cairo +++ b/packages/core/src/tests/solver/test_deposit.cairo @@ -44,7 +44,7 @@ fn test_deposit_public_vault_both_tokens_at_ratio() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -52,20 +52,19 @@ fn test_deposit_public_vault_both_tokens_at_ratio() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep_init.shares == dep.shares, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, 'Base reserve' @@ -87,17 +86,16 @@ fn test_deposit_public_vault_both_tokens_above_base_ratio() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount + to_e18(50), quote_amount); + let dep = solver.deposit(market_id, base_amount + to_e18(50), quote_amount); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep.shares == dep_init.shares, 'Shares'); } #[test] @@ -111,17 +109,16 @@ fn test_deposit_public_vault_both_tokens_above_quote_ratio() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount, quote_amount + to_e18(500)); + let dep = solver.deposit(market_id, base_amount, quote_amount + to_e18(500)); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep.shares == dep_init.shares, 'Shares'); } #[test] @@ -146,12 +143,11 @@ fn test_deposit_public_vault_both_tokens_above_available() { // Deposit should be capped at available. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_deposit, quote_deposit, _) = solver - .deposit(market_id, base_amount * 2, quote_amount * 2); + let dep = solver.deposit(market_id, base_amount * 2, quote_amount * 2); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); } #[test] @@ -164,7 +160,7 @@ fn test_deposit_public_vault_base_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 0; - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -172,21 +168,20 @@ fn test_deposit_public_vault_base_token_only() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep.shares == dep_init.shares, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, 'Base reserve' @@ -207,7 +202,7 @@ fn test_deposit_public_vault_quote_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = 0; let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -215,21 +210,20 @@ fn test_deposit_public_vault_quote_token_only() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep.shares == dep_init.shares, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, 'Base reserve' @@ -267,10 +261,9 @@ fn test_deposit_public_vault_multiple_lps_capped_at_available() { start_prank(CheatTarget::One(solver.contract_address), alice()); let base_deposit = to_e18(40000); let quote_deposit = to_e18(20); - let (base_amount, quote_amount, shares) = solver - .deposit(market_id, base_deposit, quote_deposit); - assert(base_amount == base_available, 'Base deposit'); - assert(quote_amount == quote_available, 'Quote deposit'); + let dep = solver.deposit(market_id, base_deposit, quote_deposit); + assert(dep.base_amount == base_available, 'Base deposit'); + assert(dep.quote_amount == quote_available, 'Quote deposit'); // Check vault balances. let vault_token_addr = vault_token_opt.unwrap(); @@ -289,7 +282,13 @@ fn test_deposit_public_vault_multiple_lps_capped_at_available() { solver.contract_address, SolverComponent::Event::Deposit( SolverComponent::Deposit { - market_id, caller: alice(), base_amount, quote_amount, shares + market_id, + caller: alice(), + base_amount: dep.base_amount, + quote_amount: dep.quote_amount, + base_fees: dep.base_fees, + quote_fees: dep.quote_fees, + shares: dep.shares } ) ) @@ -316,26 +315,25 @@ fn test_deposit_private_vault_both_tokens_at_arbitrary_ratio() { // Deposit at arbitrary ratio. let base_deposit = to_e18(50); let quote_deposit = to_e18(600); - let (base_amount, quote_amount, shares) = solver - .deposit(market_id, base_deposit, quote_deposit); + let dep = solver.deposit(market_id, base_deposit, quote_deposit); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); - assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(base_deposit == dep.base_amount, 'Base deposit'); + assert(quote_deposit == dep.quote_amount, 'Quote deposit'); + assert(dep.shares == 0, 'Shares'); + assert(aft.lp_base_bal == bef.lp_base_bal - dep.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal - dep.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, + aft.market_state.base_reserves == bef.market_state.base_reserves + dep.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves + quote_amount, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + dep.quote_amount, 'Quote reserve' ); } @@ -359,26 +357,25 @@ fn test_deposit_private_vault_base_token_only() { // Deposit at arbitrary ratio. let base_deposit = to_e18(50); let quote_deposit = 0; - let (base_amount, quote_amount, shares) = solver - .deposit(market_id, base_deposit, quote_deposit); + let dep = solver.deposit(market_id, base_deposit, quote_deposit); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); - assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(base_deposit == dep.base_amount, 'Base deposit'); + assert(quote_deposit == dep.quote_amount, 'Quote deposit'); + assert(dep.shares == 0, 'Shares'); + assert(aft.lp_base_bal == bef.lp_base_bal - dep.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal - dep.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, + aft.market_state.base_reserves == bef.market_state.base_reserves + dep.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves + quote_amount, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + dep.quote_amount, 'Quote reserve' ); } @@ -402,26 +399,25 @@ fn test_deposit_private_vault_quote_token_only() { // Deposit at arbitrary ratio. let base_deposit = 0; let quote_deposit = to_e18(600); - let (base_amount, quote_amount, shares) = solver - .deposit(market_id, base_deposit, quote_deposit); + let dep = solver.deposit(market_id, base_deposit, quote_deposit); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); - assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(base_deposit == dep.base_amount, 'Base deposit'); + assert(quote_deposit == dep.quote_amount, 'Quote deposit'); + assert(dep.shares == 0, 'Shares'); + assert(aft.lp_base_bal == bef.lp_base_bal - dep.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal - dep.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, + aft.market_state.base_reserves == bef.market_state.base_reserves + dep.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves + quote_amount, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + dep.quote_amount, 'Quote reserve' ); } @@ -447,8 +443,7 @@ fn test_deposit_emits_event() { let mut spy = spy_events(SpyOn::One(solver.contract_address)); // Deposit. - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Check events emitted. spy @@ -460,9 +455,11 @@ fn test_deposit_emits_event() { SolverComponent::Deposit { market_id, caller: owner(), - base_amount: base_deposit, - quote_amount: quote_deposit, - shares + base_amount: dep.base_amount, + quote_amount: dep.quote_amount, + base_fees: dep.base_fees, + quote_fees: dep.quote_fees, + shares: dep.shares } ) ) @@ -488,8 +485,7 @@ fn test_deposit_with_referrer_emits_event() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_deposit, quote_deposit, shares) = solver - .deposit_with_referrer(market_id, base_amount, quote_amount, alice()); + let dep = solver.deposit_with_referrer(market_id, base_amount, quote_amount, alice()); // Check events emitted. spy @@ -507,9 +503,11 @@ fn test_deposit_with_referrer_emits_event() { SolverComponent::Deposit { market_id, caller: owner(), - base_amount: base_deposit, - quote_amount: quote_deposit, - shares + base_amount: dep.base_amount, + quote_amount: dep.quote_amount, + base_fees: dep.base_fees, + quote_fees: dep.quote_fees, + shares: dep.shares } ) ) diff --git a/packages/core/src/tests/solver/test_deposit_initial.cairo b/packages/core/src/tests/solver/test_deposit_initial.cairo index 455d56c..82b4fe0 100644 --- a/packages/core/src/tests/solver/test_deposit_initial.cairo +++ b/packages/core/src/tests/solver/test_deposit_initial.cairo @@ -45,20 +45,19 @@ fn test_deposit_initial_public_both_tokens() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let res = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares != 0, 'Shares'); + assert(res.base_amount == base_amount, 'Base deposit'); + assert(res.quote_amount == quote_amount, 'Quote deposit'); + assert(res.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + res.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + res.shares, 'LP total shares'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); assert(aft.market_state.quote_reserves == quote_amount, 'Quote reserve'); } @@ -77,20 +76,19 @@ fn test_deposit_initial_public_base_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 0; - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let res = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares != 0, 'Shares'); + assert(res.base_amount == base_amount, 'Base deposit'); + assert(res.quote_amount == quote_amount, 'Quote deposit'); + assert(res.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + res.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + res.shares, 'LP total shares'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); assert(aft.market_state.quote_reserves == quote_amount, 'Quote reserve'); } @@ -109,20 +107,19 @@ fn test_deposit_initial_public_quote_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = 0; let quote_amount = to_e18(500); - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let res = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares != 0, 'Shares'); + assert(res.base_amount == base_amount, 'Base deposit'); + assert(res.quote_amount == quote_amount, 'Quote deposit'); + assert(res.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + res.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + res.shares, 'LP total shares'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); assert(aft.market_state.quote_reserves == quote_amount, 'Quote reserve'); } @@ -141,16 +138,15 @@ fn test_deposit_initial_private_both_tokens() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let res = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); + assert(res.base_amount == base_amount, 'Base deposit'); + assert(res.quote_amount == quote_amount, 'Quote deposit'); + assert(res.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); @@ -171,16 +167,15 @@ fn test_deposit_initial_private_base_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 0; - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let res = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); + assert(res.base_amount == base_amount, 'Base deposit'); + assert(res.quote_amount == quote_amount, 'Quote deposit'); + assert(res.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); @@ -201,16 +196,15 @@ fn test_deposit_initial_private_quote_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = 0; let quote_amount = to_e18(500); - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let res = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); + assert(res.base_amount == base_amount, 'Base deposit'); + assert(res.quote_amount == quote_amount, 'Quote deposit'); + assert(res.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); @@ -235,8 +229,7 @@ fn test_deposit_initial_emits_event() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 1000_000000; - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let res = solver.deposit_initial(market_id, base_amount, quote_amount); // Check events emitted. spy @@ -248,9 +241,11 @@ fn test_deposit_initial_emits_event() { SolverComponent::Deposit { market_id, caller: owner(), - base_amount: base_deposit, - quote_amount: quote_deposit, - shares + base_amount: res.base_amount, + quote_amount: res.quote_amount, + base_fees: res.base_fees, + quote_fees: res.quote_fees, + shares: res.shares } ) ) @@ -272,8 +267,7 @@ fn test_deposit_initial_with_referrer_emits_event() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 1000_000000; - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial_with_referrer(market_id, base_amount, quote_amount, alice()); + let res = solver.deposit_initial_with_referrer(market_id, base_amount, quote_amount, alice()); // Check events emitted. spy @@ -291,9 +285,11 @@ fn test_deposit_initial_with_referrer_emits_event() { SolverComponent::Deposit { market_id, caller: owner(), - base_amount: base_deposit, - quote_amount: quote_deposit, - shares + base_amount: res.base_amount, + quote_amount: res.quote_amount, + base_fees: res.base_fees, + quote_fees: res.quote_fees, + shares: res.shares } ) ) diff --git a/packages/core/src/tests/solver/test_get_balances.cairo b/packages/core/src/tests/solver/test_get_balances.cairo index 30040f7..839aa11 100644 --- a/packages/core/src/tests/solver/test_get_balances.cairo +++ b/packages/core/src/tests/solver/test_get_balances.cairo @@ -49,14 +49,14 @@ fn test_get_balances() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Get balances. - let (base_amount, quote_amount, base_fees, quote_fees) = solver.get_balances(market_id); - assert(base_amount == base_owner - amount_out, 'Base amount'); - assert(quote_amount == quote_owner + amount_in - fees, 'Quote amount'); - assert(base_fees == 0, 'Base fees'); - assert(quote_fees == fees, 'Quote fees'); + let bal = solver.get_balances(market_id); + assert(bal.base_amount == base_owner - swap.amount_out, 'Base amount'); + assert(bal.quote_amount == quote_owner + swap.amount_in - swap.fees, 'Quote amount'); + assert(bal.base_fees == 0, 'Base fees'); + assert(bal.quote_fees == swap.fees, 'Quote fees'); } #[test] @@ -88,25 +88,36 @@ fn test_get_user_balances() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Get balances. - let (base_owner, quote_owner, base_fees_owner, quote_fees_owner) = solver - .get_user_balances(owner(), market_id); - let (base_alice, quote_alice, base_fees_alice, quote_fees_alice) = solver - .get_user_balances(alice(), market_id); + let bal_owner = solver.get_user_balances(owner(), market_id); + let bal_alice = solver.get_user_balances(alice(), market_id); // Run checks. - assert(amount_in == params.amount, 'Amount in'); - assert(fees == math::mul_div(params.amount, 50, 10000, true), 'Fees'); - assert(approx_eq(base_owner, base_deposit_owner - amount_out * 2 / 3, 1), 'Base owner'); + assert(swap.amount_in == params.amount, 'Amount in'); + assert(swap.fees == math::mul_div(params.amount, 50, 10000, true), 'Fees'); assert( - approx_eq(quote_owner, quote_deposit_owner + (amount_in - fees) * 2 / 3, 1), 'Quote owner' + approx_eq(bal_owner.base_amount, base_deposit_owner - swap.amount_out * 2 / 3, 1), + 'Base owner' ); - assert(base_fees_owner == 0, 'Base fees owner'); - assert(approx_eq(quote_fees_owner, fees * 2 / 3, 1), 'Quote fees owner'); - assert(approx_eq(base_alice, base_deposit_alice - amount_out / 3, 1), 'Base alice'); - assert(approx_eq(quote_alice, quote_deposit_alice + (amount_in - fees) / 3, 1), 'Quote alice'); - assert(base_fees_alice == 0, 'Base fees alice'); - assert(approx_eq(quote_fees_alice, fees / 3, 1), 'Quote fees alice'); + assert( + approx_eq( + bal_owner.quote_amount, quote_deposit_owner + (swap.amount_in - swap.fees) * 2 / 3, 1 + ), + 'Quote owner' + ); + assert(bal_owner.base_fees == 0, 'Base fees owner'); + assert(approx_eq(bal_owner.quote_fees, swap.fees * 2 / 3, 1), 'Quote fees owner'); + assert( + approx_eq(bal_alice.base_amount, base_deposit_alice - swap.amount_out / 3, 1), 'Base alice' + ); + assert( + approx_eq( + bal_alice.quote_amount, quote_deposit_alice + (swap.amount_in - swap.fees) / 3, 1 + ), + 'Quote alice' + ); + assert(bal_alice.base_fees == 0, 'Base fees alice'); + assert(approx_eq(bal_alice.quote_fees, swap.fees / 3, 1), 'Quote fees alice'); } diff --git a/packages/core/src/tests/solver/test_pause.cairo b/packages/core/src/tests/solver/test_pause.cairo index 9681779..1322f17 100644 --- a/packages/core/src/tests/solver/test_pause.cairo +++ b/packages/core/src/tests/solver/test_pause.cairo @@ -40,7 +40,7 @@ fn test_pause_allows_withdraws() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (_, _, shares) = solver.deposit(market_id, to_e18(100), to_e18(500)); + let dep = solver.deposit(market_id, to_e18(100), to_e18(500)); // Pause. start_prank(CheatTarget::One(solver.contract_address), owner()); @@ -48,7 +48,7 @@ fn test_pause_allows_withdraws() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), alice()); - solver.withdraw_public(market_id, shares); + solver.withdraw_public(market_id, dep.shares); } #[test] diff --git a/packages/core/src/tests/solver/test_swap.cairo b/packages/core/src/tests/solver/test_swap.cairo index 604ce08..69e4f26 100644 --- a/packages/core/src/tests/solver/test_swap.cairo +++ b/packages/core/src/tests/solver/test_swap.cairo @@ -55,12 +55,12 @@ fn test_swap_buy_exact_in() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Run checks. - assert(amount_in == params.amount, 'Amount in'); - assert(amount_out == 995000000000000000, 'Amount out'); - assert(fees == math::mul_div(params.amount, 50, 10000, true), 'Fees'); + assert(swap.amount_in == params.amount, 'Amount in'); + assert(swap.amount_out == 995000000000000000, 'Amount out'); + assert(swap.fees == math::mul_div(params.amount, 50, 10000, true), 'Fees'); } #[test] @@ -89,12 +89,12 @@ fn test_swap_sell_exact_in() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Run checks. - assert(amount_in == params.amount, 'Amount in'); - assert(amount_out == 4975000000000000000, 'Amount out'); - assert(fees == math::mul_div(params.amount, 50, 10000, true), 'Fees'); + assert(swap.amount_in == params.amount, 'Amount in'); + assert(swap.amount_out == 4975000000000000000, 'Amount out'); + assert(swap.fees == math::mul_div(params.amount, 50, 10000, true), 'Fees'); } #[test] @@ -123,12 +123,12 @@ fn test_swap_buy_exact_out() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Run checks. - assert(approx_eq(amount_in, math::mul_div(to_e18(5), 10000, 9950, true), 1), 'Amount in'); - assert(amount_out == params.amount, 'Amount out'); - assert(approx_eq(fees, math::mul_div(to_e18(5), 50, 9950, true), 1), 'Fees'); + assert(approx_eq(swap.amount_in, math::mul_div(to_e18(5), 10000, 9950, true), 1), 'Amount in'); + assert(swap.amount_out == params.amount, 'Amount out'); + assert(approx_eq(swap.fees, math::mul_div(to_e18(5), 50, 9950, true), 1), 'Fees'); } #[test] @@ -157,12 +157,12 @@ fn test_swap_sell_exact_out() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Run checks. - assert(approx_eq(amount_in, math::mul_div(to_e18(1), 10000, 9950, true), 1), 'Amount in'); - assert(amount_out == params.amount, 'Amount out'); - assert(approx_eq(fees, math::mul_div(to_e18(1), 50, 9950, true), 1), 'Fees'); + assert(approx_eq(swap.amount_in, math::mul_div(to_e18(1), 10000, 9950, true), 1), 'Amount in'); + assert(swap.amount_out == params.amount, 'Amount out'); + assert(approx_eq(swap.fees, math::mul_div(to_e18(1), 50, 9950, true), 1), 'Fees'); } //////////////////////////////// @@ -193,7 +193,7 @@ fn test_swap_should_emit_event() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Check events emitted. spy @@ -205,9 +205,9 @@ fn test_swap_should_emit_event() { SolverComponent::Swap { market_id, caller: alice(), - amount_in, - amount_out, - fees, + amount_in: swap.amount_in, + amount_out: swap.amount_out, + fees: swap.fees, is_buy: params.is_buy, exact_input: params.exact_input } diff --git a/packages/core/src/tests/solver/test_withdraw.cairo b/packages/core/src/tests/solver/test_withdraw.cairo index 203cd1a..ea5b759 100644 --- a/packages/core/src/tests/solver/test_withdraw.cairo +++ b/packages/core/src/tests/solver/test_withdraw.cairo @@ -42,7 +42,7 @@ fn test_withdraw_partial_shares_from_public_vault() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (_, _, shares) = solver.deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Swap. let params = SwapParams { @@ -53,7 +53,7 @@ fn test_withdraw_partial_shares_from_public_vault() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -61,29 +61,28 @@ fn test_withdraw_partial_shares_from_public_vault() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver - .withdraw_public(market_id, shares / 2); + let wd = solver.withdraw_public(market_id, dep.shares / 2); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(base_withdraw, (base_amount * 2 + amount_in) / 4, 10), 'Base deposit'); - assert(approx_eq(quote_withdraw, (quote_amount * 2 - amount_out) / 4, 10), 'Quote deposit'); - assert(approx_eq(base_fees, fees / 4, 1), 'Base fees'); - assert(quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal - shares / 2, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal - shares / 2, 'LP total shares'); + assert(approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in) / 4, 10), 'Base deposit'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - - (base_withdraw - base_fees), + approx_eq(wd.quote_amount, (quote_amount * 2 - swap.amount_out) / 4, 10), 'Quote deposit' + ); + assert(approx_eq(wd.base_fees, swap.fees / 4, 1), 'Base fees'); + assert(wd.quote_fees == 0, 'Quote fees'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal - dep.shares / 2, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal - dep.shares / 2, 'LP total shares'); + assert( + aft.market_state.base_reserves == bef.market_state.base_reserves - wd.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - - (quote_withdraw - quote_fees), + aft.market_state.quote_reserves == bef.market_state.quote_reserves - wd.quote_amount, 'Quote reserve' ); } @@ -98,11 +97,11 @@ fn test_withdraw_remaining_shares_from_public_vault() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (_, _, shares_owner) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (_, _, shares_alice) = solver.deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Swap. let params = SwapParams { @@ -113,11 +112,11 @@ fn test_withdraw_remaining_shares_from_public_vault() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Withdraw owner. start_prank(CheatTarget::One(solver.contract_address), owner()); - solver.withdraw_public(market_id, shares_owner); + solver.withdraw_public(market_id, dep_init.shares); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -125,29 +124,28 @@ fn test_withdraw_remaining_shares_from_public_vault() { // Withdraw LP. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver - .withdraw_public(market_id, shares_alice); + let wd = solver.withdraw_public(market_id, dep.shares); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(base_withdraw, (base_amount * 2 + amount_in) / 2, 1), 'Base deposit'); - assert(approx_eq(quote_withdraw, (quote_amount * 2 - amount_out) / 2, 1), 'Quote deposit'); - assert(base_fees == fees / 2, 'Base fees'); - assert(quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal - shares_alice, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal - shares_alice, 'LP total shares'); + assert(approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in) / 2, 1), 'Base deposit'); + assert( + approx_eq(wd.quote_amount, (quote_amount * 2 - swap.amount_out) / 2, 1), 'Quote deposit' + ); + assert(wd.base_fees == swap.fees / 2, 'Base fees'); + assert(wd.quote_fees == 0, 'Quote fees'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal - dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal - dep.shares, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - - (base_withdraw - base_fees), + aft.market_state.base_reserves == bef.market_state.base_reserves - wd.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - - (quote_withdraw - quote_fees), + aft.market_state.quote_reserves == bef.market_state.quote_reserves - wd.quote_amount, 'Quote reserve' ); } @@ -167,7 +165,7 @@ fn test_withdraw_allowed_even_if_paused() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (_, _, shares) = solver.deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Pause. start_prank(CheatTarget::One(solver.contract_address), owner()); @@ -175,7 +173,7 @@ fn test_withdraw_allowed_even_if_paused() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), alice()); - solver.withdraw_public(market_id, shares); + solver.withdraw_public(market_id, dep.shares); } #[test] @@ -200,7 +198,7 @@ fn test_withdraw_private_base_only() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, _, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = contract_address_const::<0x0>(); @@ -208,19 +206,18 @@ fn test_withdraw_private_base_only() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver - .withdraw_private(market_id, base_amount + amount_in, 0); + let wd = solver.withdraw_private(market_id, base_amount + swap.amount_in, 0); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount + amount_in, 1), 'Base withdraw'); - assert(quote_withdraw == 0, 'Quote withdraw'); - assert(approx_eq(base_fees, fees, 1), 'Base fees'); - assert(quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); + assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 1), 'Base withdraw'); + assert(wd.quote_amount == 0, 'Quote withdraw'); + assert(approx_eq(wd.base_fees, swap.fees, 1), 'Base fees'); + assert(wd.quote_fees == 0, 'Quote fees'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); } @@ -247,7 +244,7 @@ fn test_withdraw_private_quote_only() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (_, amount_out, _) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = contract_address_const::<0x0>(); @@ -255,19 +252,18 @@ fn test_withdraw_private_quote_only() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver - .withdraw_private(market_id, 0, quote_amount); // will be capped at available + let wd = solver.withdraw_private(market_id, 0, quote_amount); // will be capped at available // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(base_withdraw == 0, 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount - amount_out, 1), 'Quote withdraw'); - assert(base_fees == 0, 'Base fees'); - assert(quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); + assert(wd.base_amount == 0, 'Base withdraw'); + assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 1), 'Quote withdraw'); + assert(wd.base_fees == 0, 'Base fees'); + assert(wd.quote_fees == 0, 'Quote fees'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); } @@ -294,7 +290,7 @@ fn test_withdraw_private_partial_amounts() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = contract_address_const::<0x0>(); @@ -302,31 +298,29 @@ fn test_withdraw_private_partial_amounts() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver + let wd = solver .withdraw_private( - market_id, (base_amount + amount_in) / 2, (quote_amount - amount_out) / 2 + market_id, (base_amount + swap.amount_in) / 2, (quote_amount - swap.amount_out) / 2 ); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, (base_amount + amount_in) / 2, 1), 'Base withdraw'); - assert(approx_eq(quote_withdraw, (quote_amount - amount_out) / 2, 1), 'Quote withdraw'); - assert(approx_eq(base_fees, fees / 2, 1), 'Base fees'); - assert(quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); + assert(approx_eq(wd.base_amount, (base_amount + swap.amount_in) / 2, 1), 'Base withdraw'); + assert(approx_eq(wd.quote_amount, (quote_amount - swap.amount_out) / 2, 1), 'Quote withdraw'); + assert(approx_eq(wd.base_fees, swap.fees / 2, 1), 'Base fees'); + assert(wd.quote_fees == 0, 'Quote fees'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - - (base_withdraw - base_fees), + aft.market_state.base_reserves == bef.market_state.base_reserves - wd.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - - (quote_withdraw - quote_fees), + aft.market_state.quote_reserves == bef.market_state.quote_reserves - wd.quote_amount, 'Quote reserve' ); } @@ -353,7 +347,7 @@ fn test_withdraw_all_remaining_balances_from_private_vault() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = contract_address_const::<0x0>(); @@ -361,29 +355,27 @@ fn test_withdraw_all_remaining_balances_from_private_vault() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver - .withdraw_private(market_id, base_amount + amount_in, quote_amount - amount_out); + let wd = solver + .withdraw_private(market_id, base_amount + swap.amount_in, quote_amount - swap.amount_out); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount + amount_in, 1), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount - amount_out, 1), 'Quote withdraw'); - assert(base_fees == fees, 'Base fees'); - assert(quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); + assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 1), 'Base withdraw'); + assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 1), 'Quote withdraw'); + assert(wd.base_fees == swap.fees, 'Base fees'); + assert(wd.quote_fees == 0, 'Quote fees'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); assert(aft.vault_lp_bal == 0, 'LP shares'); assert(aft.vault_total_bal == 0, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - - (base_withdraw - base_fees), + aft.market_state.base_reserves == bef.market_state.base_reserves - wd.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - - (quote_withdraw - quote_fees), + aft.market_state.quote_reserves == bef.market_state.quote_reserves - wd.quote_amount, 'Quote reserve' ); } @@ -410,18 +402,17 @@ fn test_withdraw_more_than_available_correctly_caps_amount_for_private_vault() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver - .withdraw_private(market_id, base_amount * 2, quote_amount * 2); + let wd = solver.withdraw_private(market_id, base_amount * 2, quote_amount * 2); // Run checks. - assert(approx_eq(base_withdraw, base_amount + amount_in, 1), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount - amount_out, 1), 'Quote withdraw'); - assert(base_fees == fees, 'Base fees'); - assert(quote_fees == 0, 'Quote fees'); + assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 1), 'Base withdraw'); + assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 1), 'Quote withdraw'); + assert(wd.base_fees == swap.fees, 'Base fees'); + assert(wd.quote_fees == 0, 'Quote fees'); } //////////////////////////////// @@ -439,14 +430,13 @@ fn test_withdraw_public_emits_event() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 1000_000000; - let (_, _, shares) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Spy on events. let mut spy = spy_events(SpyOn::One(solver.contract_address)); // Withdraw. - let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver - .withdraw_public(market_id, shares); + let wd = solver.withdraw_public(market_id, dep_init.shares); // Check events emitted. spy @@ -458,11 +448,11 @@ fn test_withdraw_public_emits_event() { SolverComponent::Withdraw { market_id, caller: owner(), - base_amount: base_withdraw, - quote_amount: quote_withdraw, - base_fees, - quote_fees, - shares, + base_amount: wd.base_amount, + quote_amount: wd.quote_amount, + base_fees: wd.base_fees, + quote_fees: wd.quote_fees, + shares: dep_init.shares, } ) ) @@ -481,14 +471,13 @@ fn test_withdraw_private_emits_event() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 1000_000000; - let (_, _, shares) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Spy on events. let mut spy = spy_events(SpyOn::One(solver.contract_address)); // Withdraw. - let (base_withdraw, quote_withdraw, base_fees, quote_fees) = solver - .withdraw_private(market_id, base_amount, quote_amount); + let wd = solver.withdraw_private(market_id, base_amount, quote_amount); // Check events emitted. spy @@ -500,11 +489,11 @@ fn test_withdraw_private_emits_event() { SolverComponent::Withdraw { market_id, caller: owner(), - base_amount: base_withdraw, - quote_amount: quote_withdraw, - base_fees, - quote_fees, - shares, + base_amount: wd.base_amount, + quote_amount: wd.quote_amount, + base_fees: wd.base_fees, + quote_fees: wd.quote_fees, + shares: dep_init.shares, } ) ) @@ -542,10 +531,10 @@ fn test_withdraw_public_more_shares_than_available() { // Deposit initial. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (_, _, shares) = solver.deposit_initial(market_id, to_e18(100), to_e18(500)); + let dep_init = solver.deposit_initial(market_id, to_e18(100), to_e18(500)); // Withdraw. - solver.withdraw_public(market_id, shares + 1); + solver.withdraw_public(market_id, dep_init.shares + 1); } #[test] diff --git a/packages/core/src/types.cairo b/packages/core/src/types.cairo index ac3360e..43d579d 100644 --- a/packages/core/src/types.cairo +++ b/packages/core/src/types.cairo @@ -37,6 +37,16 @@ pub struct MarketState { pub vault_token: ContractAddress, } +// Fees per share. +// +// * `base_fps` - base fees per share +// * `quote_fps` - quote fees per share +#[derive(Drop, Copy, Serde)] +pub struct FeesPerShare { + pub base_fps: u256, + pub quote_fps: u256, +} + // Information about a swap. // // * `is_buy` - whether swap is buy or sell @@ -55,6 +65,50 @@ pub struct SwapParams { pub deadline: Option, } +// Token amounts (function response). +// +// * `base_amount` - base amount deposited +// * `quote_amount` - quote amount deposited +// * `base_fees` - base fees collected at deposit +// * `quote_fees` - quote fees collected at deposit +#[derive(Copy, Drop, Serde, Default)] +pub struct Amounts { + pub base_amount: u256, + pub quote_amount: u256, + pub base_fees: u256, + pub quote_fees: u256, +} + +// Token amounts with shares (function response). +// +// * `base_amount` - base amount deposited +// * `quote_amount` - quote amount deposited +// * `base_fees` - base fees collected at deposit +// * `quote_fees` - quote fees collected at deposit +// * `shares` - number of shares minted +#[derive(Copy, Drop, Serde)] +pub struct AmountsWithShares { + pub base_amount: u256, + pub quote_amount: u256, + pub base_fees: u256, + pub quote_fees: u256, + pub shares: u256, +} + +// Swap amounts (function response). +// +// * `base_amount` - base amount deposited +// * `quote_amount` - quote amount deposited +// * `base_fees` - base fees collected at deposit +// * `quote_fees` - quote fees collected at deposit +// * `shares` - number of shares minted +#[derive(Copy, Drop, Serde)] +pub struct SwapAmounts { + pub amount_in: u256, + pub amount_out: u256, + pub fees: u256, +} + // Virtual liquidity position. // // * `lower_sqrt_price` - lower limit of position @@ -105,3 +159,13 @@ pub struct PackedMarketState { pub slab4: felt252, pub slab5: felt252, } + +// Packed fees per share. +// +// * `slab0` - base fees per share (coerced to felt252) +// * `slab1` - quote fees per share (coerced to felt252) +#[derive(starknet::Store)] +pub struct PackedFeesPerShare { + pub slab0: felt252, + pub slab1: felt252, +} diff --git a/packages/replicating/src/contracts/replicating_solver.cairo b/packages/replicating/src/contracts/replicating_solver.cairo index f025850..e088a8a 100644 --- a/packages/replicating/src/contracts/replicating_solver.cairo +++ b/packages/replicating/src/contracts/replicating_solver.cairo @@ -19,7 +19,7 @@ pub mod ReplicatingSolver { IOracleABIDispatcherTrait }, }; - use haiko_solver_core::types::{PositionInfo, MarketState, MarketInfo, SwapParams}; + use haiko_solver_core::types::{PositionInfo, MarketState, MarketInfo, SwapParams, SwapAmounts}; use haiko_solver_replicating::types::MarketParams; // Haiko imports. @@ -148,7 +148,7 @@ pub mod ReplicatingSolver { // * `fees` - fees fn quote( self: @ContractState, market_id: felt252, swap_params: SwapParams, - ) -> (u256, u256, u256) { + ) -> SwapAmounts { // Run validity checks. let state: MarketState = self.solver.market_state.read(market_id); let market_info: MarketInfo = self.solver.market_info.read(market_id); @@ -193,7 +193,7 @@ pub mod ReplicatingSolver { } // Return amounts. - (amount_in, amount_out, fees) + SwapAmounts { amount_in, amount_out, fees } } // Callback function to execute any state updates after a swap is completed. diff --git a/packages/replicating/src/libraries/swap_lib.cairo b/packages/replicating/src/libraries/swap_lib.cairo index 1ad7c0a..0505b2c 100644 --- a/packages/replicating/src/libraries/swap_lib.cairo +++ b/packages/replicating/src/libraries/swap_lib.cairo @@ -84,6 +84,15 @@ pub fn compute_swap_amounts( fee_rate: u16, exact_input: bool, ) -> (u256, u256, u256, u256) { + println!( + "curr_sqrt_price: {}, target_sqrt_price: {}, liquidity: {}, amount: {}, fee_rate: {}, exact_input: {}", + curr_sqrt_price, + target_sqrt_price, + liquidity, + amount, + fee_rate, + exact_input + ); // Determine whether swap is a buy or sell. let is_buy = target_sqrt_price > curr_sqrt_price; diff --git a/packages/replicating/src/tests/solver/debug_swap.cairo b/packages/replicating/src/tests/solver/debug_swap.cairo index 9e2058e..31d23aa 100644 --- a/packages/replicating/src/tests/solver/debug_swap.cairo +++ b/packages/replicating/src/tests/solver/debug_swap.cairo @@ -50,21 +50,21 @@ fn test_debug_swap() { // Hard code params. let is_buy = false; let exact_input = false; - let amount = 10000000000; - let fee_rate = 25; - let range = 5000; - let max_delta = 500; + let amount = 1000000000000000000; + let fee_rate = 30; + let range = 11400; + let max_delta = 0; let max_skew = 6000; let base_currency_id = 1398035019; - let quote_currency_id = 1431520323; - let min_sources = 2; - let max_age = 1000; - let oracle_price = 37067545; + let quote_currency_id = 4543560; + let min_sources = 3; + let max_age = 600; + let oracle_price = 16718; let oracle_decimals = 8; - let base_reserves = 268762195878807302077639; - let quote_reserves = 96834519855; + let base_reserves = 100000000000000000000000; + let quote_reserves = 16707738619115042000; let base_decimals = 18; - let quote_decimals = 6; + let quote_decimals = 18; let ( _base_token, _quote_token, oracle, _vault_token_class, solver, market_id, _vault_token_opt @@ -110,6 +110,6 @@ fn test_debug_swap() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out) = solver.swap(market_id, params); - println!("amount_in: {}, amount_out: {}", amount_in, amount_out); + let (amount_in, amount_out, fees) = solver.swap(market_id, params); + println!("amount_in: {}, amount_out: {}, fees: {}", amount_in, amount_out, fees); } diff --git a/packages/replicating/src/tests/solver/test_deposit.cairo b/packages/replicating/src/tests/solver/test_deposit.cairo index 6d83ab6..4393308 100644 --- a/packages/replicating/src/tests/solver/test_deposit.cairo +++ b/packages/replicating/src/tests/solver/test_deposit.cairo @@ -47,7 +47,7 @@ fn test_deposit_public_vault_both_tokens_at_ratio() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -55,20 +55,19 @@ fn test_deposit_public_vault_both_tokens_at_ratio() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep.shares == dep_init.shares, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, 'Base reserve' @@ -102,17 +101,16 @@ fn test_deposit_public_vault_both_tokens_above_base_ratio() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount + to_e18(50), quote_amount); + let dep = solver.deposit(market_id, base_amount + to_e18(50), quote_amount); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep.shares == dep_init.shares, 'Shares'); } #[test] @@ -128,17 +126,16 @@ fn test_deposit_public_vault_both_tokens_above_quote_ratio() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount, quote_amount + to_e18(500)); + let dep = solver.deposit(market_id, base_amount, quote_amount + to_e18(500)); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep.shares == dep_init.shares, 'Shares'); } #[test] @@ -174,12 +171,11 @@ fn test_deposit_public_vault_both_tokens_above_available() { // Deposit should be capped at available. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_deposit, quote_deposit, _) = solver - .deposit(market_id, base_amount * 2, quote_amount * 2); + let dep = solver.deposit(market_id, base_amount * 2, quote_amount * 2); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); } #[test] @@ -201,7 +197,7 @@ fn test_deposit_public_vault_base_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 0; - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -209,21 +205,20 @@ fn test_deposit_public_vault_base_token_only() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep.shares == dep_init.shares, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, 'Base reserve' @@ -261,7 +256,7 @@ fn test_deposit_public_vault_quote_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = 0; let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -269,21 +264,20 @@ fn test_deposit_public_vault_quote_token_only() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == shares_init, 'Shares'); + assert(dep.base_amount == base_amount, 'Base deposit'); + assert(dep.quote_amount == quote_amount, 'Quote deposit'); + assert(dep.shares == dep_init.shares, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, 'Base reserve' @@ -324,26 +318,25 @@ fn test_deposit_private_vault_both_tokens_at_arbitrary_ratio() { // Deposit at arbitrary ratio. let base_deposit = to_e18(50); let quote_deposit = to_e18(600); - let (base_amount, quote_amount, shares) = solver - .deposit(market_id, base_deposit, quote_deposit); + let dep = solver.deposit(market_id, base_deposit, quote_deposit); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); - assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(dep.base_amount == base_deposit, 'Base deposit'); + assert(dep.quote_amount == quote_deposit, 'Quote deposit'); + assert(dep.shares == 0, 'Shares'); + assert(aft.lp_base_bal == bef.lp_base_bal - dep.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal - dep.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, + aft.market_state.base_reserves == bef.market_state.base_reserves + dep.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves + quote_amount, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + dep.quote_amount, 'Quote reserve' ); // Portfolio now more skewed towards quote asset, so we expect a bid skew. @@ -377,26 +370,25 @@ fn test_deposit_private_vault_base_token_only() { // Deposit at arbitrary ratio. let base_deposit = to_e18(50); let quote_deposit = 0; - let (base_amount, quote_amount, shares) = solver - .deposit(market_id, base_deposit, quote_deposit); + let dep = solver.deposit(market_id, base_deposit, quote_deposit); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); - assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(base_deposit == dep.base_amount, 'Base deposit'); + assert(quote_deposit == dep.quote_amount, 'Quote deposit'); + assert(dep.shares == 0, 'Shares'); + assert(aft.lp_base_bal == bef.lp_base_bal - dep.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal - dep.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, + aft.market_state.base_reserves == bef.market_state.base_reserves + dep.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves + quote_amount, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + dep.quote_amount, 'Quote reserve' ); // Portfolio now more skewed towards base asset, so we expect a ask skew. @@ -428,26 +420,25 @@ fn test_deposit_private_vault_quote_token_only() { // Deposit at arbitrary ratio. let base_deposit = 0; let quote_deposit = to_e18(600); - let (base_amount, quote_amount, shares) = solver - .deposit(market_id, base_deposit, quote_deposit); + let dep = solver.deposit(market_id, base_deposit, quote_deposit); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); - assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(base_deposit == dep.base_amount, 'Base deposit'); + assert(quote_deposit == dep.quote_amount, 'Quote deposit'); + assert(dep.shares == 0, 'Shares'); + assert(aft.lp_base_bal == bef.lp_base_bal - dep.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal - dep.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep.shares, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves + base_amount, + aft.market_state.base_reserves == bef.market_state.base_reserves + dep.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves + quote_amount, + aft.market_state.quote_reserves == bef.market_state.quote_reserves + dep.quote_amount, 'Quote reserve' ); // Portfolio now more skewed towards quote asset, so we expect a bid skew. diff --git a/packages/replicating/src/tests/solver/test_deposit_initial.cairo b/packages/replicating/src/tests/solver/test_deposit_initial.cairo index 661826e..c2f3584 100644 --- a/packages/replicating/src/tests/solver/test_deposit_initial.cairo +++ b/packages/replicating/src/tests/solver/test_deposit_initial.cairo @@ -49,20 +49,19 @@ fn test_deposit_initial_public_both_tokens() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares != 0, 'Shares'); + assert(dep_init.base_amount == base_amount, 'Base deposit'); + assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); + assert(dep_init.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep_init.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep_init.shares, 'LP total shares'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); assert(aft.market_state.quote_reserves == quote_amount, 'Quote reserve'); assert( @@ -98,20 +97,19 @@ fn test_deposit_initial_public_base_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 0; - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares != 0, 'Shares'); + assert(dep_init.base_amount == base_amount, 'Base deposit'); + assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); + assert(dep_init.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep_init.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep_init.shares, 'LP total shares'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); assert(aft.market_state.quote_reserves == quote_amount, 'Quote reserve'); assert( @@ -147,20 +145,19 @@ fn test_deposit_initial_public_quote_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = 0; let quote_amount = to_e18(500); - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares != 0, 'Shares'); + assert(dep_init.base_amount == base_amount, 'Base deposit'); + assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); + assert(dep_init.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal + shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal + shares, 'LP total shares'); + assert(aft.vault_lp_bal == bef.vault_lp_bal + dep_init.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal + dep_init.shares, 'LP total shares'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); assert(aft.market_state.quote_reserves == quote_amount, 'Quote reserve'); assert( @@ -190,16 +187,15 @@ fn test_deposit_initial_private_both_tokens() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); + assert(dep_init.base_amount == base_amount, 'Base deposit'); + assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); + assert(dep_init.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); @@ -239,16 +235,15 @@ fn test_deposit_initial_private_base_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 0; - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); + assert(dep_init.base_amount == base_amount, 'Base deposit'); + assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); + assert(dep_init.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); @@ -288,16 +283,15 @@ fn test_deposit_initial_private_quote_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = 0; let quote_amount = to_e18(500); - let (base_deposit, quote_deposit, shares) = solver - .deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(base_deposit == base_amount, 'Base deposit'); - assert(quote_deposit == quote_amount, 'Quote deposit'); - assert(shares == 0, 'Shares'); + assert(dep_init.base_amount == base_amount, 'Base deposit'); + assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); + assert(dep_init.shares != 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); diff --git a/packages/replicating/src/tests/solver/test_e2e.cairo b/packages/replicating/src/tests/solver/test_e2e.cairo index c296573..3f89440 100644 --- a/packages/replicating/src/tests/solver/test_e2e.cairo +++ b/packages/replicating/src/tests/solver/test_e2e.cairo @@ -62,11 +62,10 @@ fn test_solver_e2e_private_market() { // Deposit initial. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_deposit_init, quote_deposit_init, shares_init) = solver - .deposit_initial(market_id, to_e18(100), to_e18(1000)); + let dep_init = solver.deposit_initial(market_id, to_e18(100), to_e18(1000)); // Deposit. - let (base_deposit, quote_deposit, shares) = solver.deposit(market_id, to_e18(100), to_e18(500)); + let dep = solver.deposit(market_id, to_e18(100), to_e18(500)); // Swap. let params = SwapParams { @@ -77,36 +76,35 @@ fn test_solver_e2e_private_market() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Withdraw. - let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver - .withdraw_private(market_id, to_e18(50), to_e18(300)); + let wd = solver.withdraw_private(market_id, to_e18(50), to_e18(300)); // Run checks. - let (base_reserves, quote_reserves, _, _) = solver.get_balances(market_id); + let res = solver.get_balances(market_id); assert( - base_reserves == base_deposit - + base_deposit_init - - amount_out - - (base_withdraw - base_fees_withdraw), + res.base_amount == dep.base_amount + + dep_init.base_amount + - swap.amount_out + - wd.base_amount, 'Base reserves' ); assert( - quote_reserves == quote_deposit - + quote_deposit_init - + amount_in - - fees - - (quote_withdraw - quote_fees_withdraw), + res.quote_amount == dep.quote_amount + + dep_init.quote_amount + + swap.amount_in + - swap.fees + - wd.quote_amount, 'Quote reserves' ); - assert(base_fees_withdraw == 0, 'Base fees'); + assert(wd.base_fees == 0, 'Base fees'); assert( - approx_eq(quote_fees_withdraw, fees * to_e18(300) / (to_e18(1500) + amount_in), 1), + approx_eq(wd.quote_fees, swap.fees * to_e18(300) / (to_e18(1500) + swap.amount_in), 1), 'Quote fees' ); - assert(shares_init == 0, 'Shares init'); - assert(shares == 0, 'Shares'); + assert(dep_init.shares == 0, 'Shares init'); + assert(dep.shares == 0, 'Shares'); } #[test] @@ -125,13 +123,11 @@ fn test_solver_e2e_public_market() { solver.set_withdraw_fee(market_id, 50); // Deposit initial. - let (base_deposit_init, quote_deposit_init, shares_init) = solver - .deposit_initial(market_id, to_e18(100), to_e18(1000)); + let dep_init = solver.deposit_initial(market_id, to_e18(100), to_e18(1000)); // Deposit as LP. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_deposit, quote_deposit, shares) = solver - .deposit(market_id, to_e18(50), to_e18(600)); // Contains extra, should coerce. + let dep = solver.deposit(market_id, to_e18(50), to_e18(600)); // Contains extra, should coerce. // Swap. start_prank(CheatTarget::One(solver.contract_address), owner()); @@ -143,12 +139,11 @@ fn test_solver_e2e_public_market() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver - .withdraw_public(market_id, shares); + let wd = solver.withdraw_public(market_id, dep.shares); // Collect withdraw fees. start_prank(CheatTarget::One(solver.contract_address), owner()); @@ -158,49 +153,58 @@ fn test_solver_e2e_public_market() { .collect_withdraw_fees(solver.contract_address, quote_token.contract_address); // Run checks. - let (base_reserves, quote_reserves, base_fee_reserves, quote_fee_reserves) = solver - .get_balances(market_id); + let res = solver.get_balances(market_id); let base_deposit_exp = to_e18(50); let quote_deposit_exp = to_e18(500); - assert(base_deposit == base_deposit_exp, 'Base deposit'); - assert(quote_deposit == quote_deposit_exp, 'Quote deposit'); + assert(dep.base_amount == base_deposit_exp, 'Base deposit'); + assert(dep.quote_amount == quote_deposit_exp, 'Quote deposit'); assert( approx_eq( - base_withdraw, math::mul_div(base_deposit_exp - amount_out / 3, 995, 1000, false), 10 + wd.base_amount, + math::mul_div(base_deposit_exp - swap.amount_out / 3, 995, 1000, false), + 10 ), 'Base withdraw' ); assert( approx_eq( - quote_withdraw, math::mul_div(quote_deposit_exp + amount_in / 3, 995, 1000, false), 10 + wd.quote_amount, + math::mul_div(quote_deposit_exp + swap.amount_in / 3, 995, 1000, false), + 10 ), 'Quote withdraw' ); - assert(base_fees_withdraw == 0, 'Base fees'); + assert(wd.base_fees == 0, 'Base fees'); assert( - approx_eq(quote_fees_withdraw, math::mul_div(fees / 3, 995, 1000, false), 10), 'Quote fees' + approx_eq(wd.quote_fees, math::mul_div(swap.fees / 3, 995, 1000, false), 10), 'Quote fees' ); - assert(shares == shares_init / 2, 'Shares'); + assert(dep.shares == dep_init.shares / 2, 'Shares'); assert( - approx_eq(base_reserves, (base_deposit_init + base_deposit_exp - amount_out) * 2 / 3, 10), + approx_eq( + res.base_amount, (dep_init.base_amount + base_deposit_exp - swap.amount_out) * 2 / 3, 10 + ), 'Base reserves' ); assert( approx_eq( - quote_reserves, (quote_deposit_init + quote_deposit_exp + amount_in - fees) * 2 / 3, 10 + res.quote_amount, + (dep_init.quote_amount + quote_deposit_exp + swap.amount_in - swap.fees) * 2 / 3, + 10 ), 'Quote reserves' ); assert( approx_eq( - base_withdraw_fees, math::mul_div(base_deposit_exp - amount_out / 3, 5, 1000, false), 10 + base_withdraw_fees, + math::mul_div(base_deposit_exp - swap.amount_out / 3, 5, 1000, false), + 10 ), 'Base withdraw fees' ); assert( approx_eq( quote_withdraw_fees, - math::mul_div(quote_deposit_exp + amount_in / 3, 5, 1000, false), + math::mul_div(quote_deposit_exp + swap.amount_in / 3, 5, 1000, false), 10 ), 'Quote withdraw fees' diff --git a/packages/replicating/src/tests/solver/test_swap.cairo b/packages/replicating/src/tests/solver/test_swap.cairo index 8eae8ba..ecdb9d4 100644 --- a/packages/replicating/src/tests/solver/test_swap.cairo +++ b/packages/replicating/src/tests/solver/test_swap.cairo @@ -759,7 +759,7 @@ fn run_swap_cases(cases: Span) { // Obtain quotes and execute swaps. start_prank(CheatTarget::One(solver.contract_address), alice()); let solver_hooks = ISolverHooksDispatcher { contract_address: solver.contract_address }; - let (quote_in, quote_out, quote_fees) = solver_hooks + let quote = solver_hooks .quote( market_id, SwapParams { @@ -773,7 +773,7 @@ fn run_swap_cases(cases: Span) { ); // Execute swap. - let (amount_in, amount_out, fees) = solver + let swap = solver .swap( market_id, SwapParams { @@ -787,51 +787,57 @@ fn run_swap_cases(cases: Span) { ); // Check results. - println!("amount in: {}, amount out: {}, fees: {}", amount_in, amount_out, fees); - if !(approx_eq_pct(amount_in, swap_case.amount_in, 10) - || approx_eq(amount_in, swap_case.amount_in, 1000)) { + println!( + "amount in: {}, amount out: {}, fees: {}", + swap.amount_in, + swap.amount_out, + swap.fees + ); + if !(approx_eq_pct(swap.amount_in, swap_case.amount_in, 10) + || approx_eq(swap.amount_in, swap_case.amount_in, 1000)) { panic( array![ 'Amount in', i.into() + 1, j.into() + 1, - amount_in.low.into(), - amount_in.high.into(), + swap.amount_in.low.into(), + swap.amount_in.high.into(), swap_case.amount_in.low.into(), swap_case.amount_in.high.into() ] ); } - if !(approx_eq_pct(amount_out, swap_case.amount_out, 10) - || approx_eq(amount_out, swap_case.amount_out, 1000)) { + if !(approx_eq_pct(swap.amount_out, swap_case.amount_out, 10) + || approx_eq(swap.amount_out, swap_case.amount_out, 1000)) { panic( array![ 'Amount out', i.into() + 1, j.into() + 1, - amount_out.low.into(), - amount_out.high.into(), + swap.amount_out.low.into(), + swap.amount_out.high.into(), swap_case.amount_out.low.into(), swap_case.amount_out.high.into() ] ); } - if !(approx_eq_pct(fees, swap_case.fees, 10) || approx_eq(fees, swap_case.fees, 1000)) { + if !(approx_eq_pct(swap.fees, swap_case.fees, 10) + || approx_eq(swap.fees, swap_case.fees, 1000)) { panic( array![ 'Fees', i.into() + 1, j.into() + 1, - fees.low.into(), - fees.high.into(), + swap.fees.low.into(), + swap.fees.high.into(), swap_case.fees.low.into(), swap_case.fees.high.into() ] ); } - assert(amount_in == quote_in, 'Quote in'); - assert(amount_out == quote_out, 'Quote out'); - assert(fees == quote_fees, 'Quote fees'); + assert(swap.amount_in == quote.amount_in, 'Quote in'); + assert(swap.amount_out == quote.amount_out, 'Quote out'); + assert(swap.fees == quote.fees, 'Quote fees'); j += 1; }; diff --git a/packages/replicating/src/tests/solver/test_withdraw.cairo b/packages/replicating/src/tests/solver/test_withdraw.cairo index ad3656f..391cbc7 100644 --- a/packages/replicating/src/tests/solver/test_withdraw.cairo +++ b/packages/replicating/src/tests/solver/test_withdraw.cairo @@ -54,7 +54,7 @@ fn test_withdraw_partial_shares_from_public_vault() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (_, _, shares) = solver.deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Swap. let params = SwapParams { @@ -65,7 +65,7 @@ fn test_withdraw_partial_shares_from_public_vault() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -73,29 +73,28 @@ fn test_withdraw_partial_shares_from_public_vault() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver - .withdraw_public(market_id, shares / 2); + let wd = solver.withdraw_public(market_id, dep.shares / 2); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(base_withdraw, (base_amount * 2 + amount_in) / 4, 10), 'Base deposit'); - assert(approx_eq(quote_withdraw, (quote_amount * 2 - amount_out) / 4, 10), 'Quote deposit'); - assert(approx_eq(base_fees_withdraw, fees / 4, 10), 'Base fees withdraw'); - assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal - shares / 2, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal - shares / 2, 'LP total shares'); + assert(approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in) / 4, 10), 'Base deposit'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - - (base_withdraw - base_fees_withdraw), + approx_eq(wd.quote_amount, (quote_amount * 2 - swap.amount_out) / 4, 10), 'Quote deposit' + ); + assert(approx_eq(wd.base_fees, swap.fees / 4, 10), 'Base fees withdraw'); + assert(wd.quote_fees == 0, 'Quote fees withdraw'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal - dep.shares / 2, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal - dep.shares / 2, 'LP total shares'); + assert( + aft.market_state.base_reserves == bef.market_state.base_reserves - wd.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - - (quote_withdraw - quote_fees_withdraw), + aft.market_state.quote_reserves == bef.market_state.quote_reserves - wd.quote_amount, 'Quote reserve' ); assert(aft.bid.lower_sqrt_price == bef.bid.lower_sqrt_price, 'Bid lower sqrt price'); @@ -121,11 +120,11 @@ fn test_withdraw_remaining_shares_from_public_vault() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = to_e18(500); - let (_, _, shares_init) = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (_, _, shares) = solver.deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Swap. let params = SwapParams { @@ -136,11 +135,11 @@ fn test_withdraw_remaining_shares_from_public_vault() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Withdraw owner. start_prank(CheatTarget::One(solver.contract_address), owner()); - solver.withdraw_public(market_id, shares_init); + solver.withdraw_public(market_id, dep_init.shares); // Snapshot before. let vault_token = vault_token_opt.unwrap(); @@ -148,29 +147,28 @@ fn test_withdraw_remaining_shares_from_public_vault() { // Withdraw LP. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver - .withdraw_public(market_id, shares); + let wd = solver.withdraw_public(market_id, dep.shares); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(base_withdraw, (base_amount * 2 + amount_in) / 2, 1), 'Base deposit'); - assert(approx_eq(quote_withdraw, (quote_amount * 2 - amount_out) / 2, 1), 'Quote deposit'); - assert(approx_eq(base_fees_withdraw, fees / 2, 1), 'Base fees withdraw'); - assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); - assert(aft.vault_lp_bal == bef.vault_lp_bal - shares, 'LP shares'); - assert(aft.vault_total_bal == bef.vault_total_bal - shares, 'LP total shares'); + assert(approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in) / 2, 1), 'Base deposit'); + assert( + approx_eq(wd.quote_amount, (quote_amount * 2 - swap.amount_out) / 2, 1), 'Quote deposit' + ); + assert(approx_eq(wd.base_fees, swap.fees / 2, 1), 'Base fees withdraw'); + assert(wd.quote_fees == 0, 'Quote fees withdraw'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.vault_lp_bal == bef.vault_lp_bal - dep.shares, 'LP shares'); + assert(aft.vault_total_bal == bef.vault_total_bal - dep.shares, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - - (base_withdraw - base_fees_withdraw), + aft.market_state.base_reserves == bef.market_state.base_reserves - wd.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - - (quote_withdraw - quote_fees_withdraw), + aft.market_state.quote_reserves == bef.market_state.quote_reserves - wd.quote_amount, 'Quote reserve' ); assert(aft.bid.lower_sqrt_price == 0, 'Bid lower sqrt price'); @@ -198,7 +196,7 @@ fn test_withdraw_allowed_even_if_paused() { // Deposit. start_prank(CheatTarget::One(solver.contract_address), alice()); - let (_, _, shares) = solver.deposit(market_id, base_amount, quote_amount); + let dep = solver.deposit(market_id, base_amount, quote_amount); // Pause. start_prank(CheatTarget::One(solver.contract_address), owner()); @@ -206,7 +204,7 @@ fn test_withdraw_allowed_even_if_paused() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), alice()); - solver.withdraw_public(market_id, shares); + solver.withdraw_public(market_id, dep.shares); } #[test] @@ -242,7 +240,7 @@ fn test_withdraw_private_base_only() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, _, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = contract_address_const::<0x0>(); @@ -250,19 +248,18 @@ fn test_withdraw_private_base_only() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver - .withdraw_private(market_id, base_amount + amount_in, 0); + let wd = solver.withdraw_private(market_id, base_amount + swap.amount_in, 0); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount + amount_in, 10), 'Base withdraw'); - assert(quote_withdraw == 0, 'Quote withdraw'); - assert(approx_eq(base_fees_withdraw, fees, 10), 'Base fees withdraw'); - assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); + assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 10), 'Base withdraw'); + assert(wd.quote_amount == 0, 'Quote withdraw'); + assert(approx_eq(wd.base_fees, swap.fees, 10), 'Base fees withdraw'); + assert(wd.quote_fees == 0, 'Quote fees withdraw'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); } @@ -300,7 +297,7 @@ fn test_withdraw_private_quote_only() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (_, amount_out, _) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = contract_address_const::<0x0>(); @@ -308,19 +305,18 @@ fn test_withdraw_private_quote_only() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver - .withdraw_private(market_id, 0, quote_amount - amount_out); + let wd = solver.withdraw_private(market_id, 0, quote_amount - swap.amount_out); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(base_withdraw == 0, 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount - amount_out, 10), 'Quote withdraw'); - assert(base_fees_withdraw == 0, 'Base fees withdraw'); - assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); + assert(wd.base_amount == 0, 'Base withdraw'); + assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 10), 'Quote withdraw'); + assert(wd.base_fees == 0, 'Base fees withdraw'); + assert(wd.quote_fees == 0, 'Quote fees withdraw'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); } @@ -350,7 +346,7 @@ fn test_withdraw_private_partial_amounts() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = contract_address_const::<0x0>(); @@ -358,31 +354,29 @@ fn test_withdraw_private_partial_amounts() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver + let wd = solver .withdraw_private( - market_id, (base_amount + amount_in) / 2, (quote_amount - amount_out) / 2 + market_id, (base_amount + swap.amount_in) / 2, (quote_amount - swap.amount_out) / 2 ); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, (base_amount + amount_in) / 2, 10), 'Base withdraw'); - assert(approx_eq(quote_withdraw, (quote_amount - amount_out) / 2, 10), 'Quote withdraw'); - assert(approx_eq(base_fees_withdraw, fees / 2, 10), 'Base fees withdraw'); - assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); + assert(approx_eq(wd.base_amount, (base_amount + swap.amount_in) / 2, 10), 'Base withdraw'); + assert(approx_eq(wd.quote_amount, (quote_amount - swap.amount_out) / 2, 10), 'Quote withdraw'); + assert(approx_eq(wd.base_fees, swap.fees / 2, 10), 'Base fees withdraw'); + assert(wd.quote_fees == 0, 'Quote fees withdraw'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - - (base_withdraw - base_fees_withdraw), + aft.market_state.base_reserves == bef.market_state.base_reserves - wd.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - - (quote_withdraw - quote_fees_withdraw), + aft.market_state.quote_reserves == bef.market_state.quote_reserves - wd.quote_amount, 'Quote reserve' ); assert(aft.bid.lower_sqrt_price == bef.bid.lower_sqrt_price, 'Bid lower sqrt price'); @@ -422,36 +416,34 @@ fn test_withdraw_all_remaining_balances_from_private_vault() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Snapshot before. let vault_token = contract_address_const::<0x0>(); let bef = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver - .withdraw_private(market_id, base_amount + amount_in, quote_amount + amount_out); + let wd = solver + .withdraw_private(market_id, base_amount + swap.amount_in, quote_amount - swap.amount_out); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(base_withdraw, base_amount + amount_in, 10), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount - amount_out, 10), 'Quote withdraw'); - assert(approx_eq(base_fees_withdraw, fees, 10), 'Base fees withdraw'); - assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + base_withdraw, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + quote_withdraw, 'LP quote bal'); + assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 10), 'Base withdraw'); + assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 10), 'Quote withdraw'); + assert(approx_eq(wd.base_fees, swap.fees, 10), 'Base fees withdraw'); + assert(wd.quote_fees == 0, 'Quote fees withdraw'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); assert(aft.vault_lp_bal == 0, 'LP shares'); assert(aft.vault_total_bal == 0, 'LP total shares'); assert( - aft.market_state.base_reserves == bef.market_state.base_reserves - - (base_withdraw - base_fees_withdraw), + aft.market_state.base_reserves == bef.market_state.base_reserves - wd.base_amount, 'Base reserve' ); assert( - aft.market_state.quote_reserves == bef.market_state.quote_reserves - - (quote_withdraw - quote_fees_withdraw), + aft.market_state.quote_reserves == bef.market_state.quote_reserves - wd.quote_amount, 'Quote reserve' ); assert(aft.bid.lower_sqrt_price == 0, 'Bid lower sqrt price'); @@ -487,15 +479,14 @@ fn test_withdraw_more_than_available_correctly_caps_amount_for_private_vault() { threshold_amount: Option::None(()), deadline: Option::None(()), }; - let (amount_in, amount_out, fees) = solver.swap(market_id, params); + let swap = solver.swap(market_id, params); // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); - let (base_withdraw, quote_withdraw, base_fees_withdraw, quote_fees_withdraw) = solver - .withdraw_private(market_id, base_amount * 2, quote_amount * 2); + let wd = solver.withdraw_private(market_id, base_amount * 2, quote_amount * 2); // Run checks. - assert(approx_eq(base_withdraw, base_amount + amount_in, 10), 'Base withdraw'); - assert(approx_eq(quote_withdraw, quote_amount - amount_out, 10), 'Quote withdraw'); - assert(base_fees_withdraw == fees, 'Base fees withdraw'); - assert(quote_fees_withdraw == 0, 'Quote fees withdraw'); + assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 10), 'Base withdraw'); + assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 10), 'Quote withdraw'); + assert(wd.base_fees == swap.fees, 'Base fees withdraw'); + assert(wd.quote_fees == 0, 'Quote fees withdraw'); } From 72d04c0ed121177f8c6956c39005d3d16bd6cbb1 Mon Sep 17 00:00:00 2001 From: parketh Date: Thu, 12 Sep 2024 18:17:44 +0100 Subject: [PATCH 07/10] fix: add exception for public markets --- packages/core/src/contracts/solver.cairo | 66 ++++++++++++++---------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/packages/core/src/contracts/solver.cairo b/packages/core/src/contracts/solver.cairo index 31f9fdc..a1a8cd8 100644 --- a/packages/core/src/contracts/solver.cairo +++ b/packages/core/src/contracts/solver.cairo @@ -447,16 +447,18 @@ pub mod SolverComponent { } } - // Update fees per share. - let mut market_fps: FeesPerShare = self.fees_per_share.read(market_id); - let vault_token = ERC20ABIDispatcher { contract_address: state.vault_token }; - let total_supply = vault_token.totalSupply(); - if swap_params.is_buy { - market_fps.quote_fps += math::mul_div(fees, ONE, total_supply, false); - } else { - market_fps.base_fps += math::mul_div(fees, ONE, total_supply, false); + // Update fees per share if this is a public market. + if market_info.is_public { + let mut market_fps: FeesPerShare = self.fees_per_share.read(market_id); + let vault_token = ERC20ABIDispatcher { contract_address: state.vault_token }; + let total_supply = vault_token.totalSupply(); + if swap_params.is_buy { + market_fps.quote_fps += math::mul_div(fees, ONE, total_supply, false); + } else { + market_fps.base_fps += math::mul_div(fees, ONE, total_supply, false); + } + self.fees_per_share.write(market_id, market_fps); } - self.fees_per_share.write(market_id, market_fps); // Transfer tokens. let market_info: MarketInfo = self.market_info.read(market_id); @@ -1125,25 +1127,36 @@ pub mod SolverComponent { let user = get_caller_address(); let market_info: MarketInfo = self.market_info.read(market_id); let mut market_state: MarketState = self.market_state.read(market_id); - let vault_token = ERC20ABIDispatcher { contract_address: market_state.vault_token }; - let user_shares = vault_token.balanceOf(user); - let user_fps: FeesPerShare = self.user_fees_per_share.read((market_id, user)); - let fps: FeesPerShare = self.fees_per_share.read(market_id); - // No accrued fee balances exist. Set user fps to pool fps and return. - if user_shares == 0 - || (user_fps.base_fps == fps.base_fps && user_fps.quote_fps == fps.quote_fps) { + // Calculate accrued fee balances. + let mut base_fees = 0; + let mut quote_fees = 0; + if market_state.is_public { + let vault_token = ERC20ABIDispatcher { contract_address: market_state.vault_token }; + let user_shares = vault_token.balanceOf(user); + let user_fps: FeesPerShare = self.user_fees_per_share.read((market_id, user)); + let fps: FeesPerShare = self.fees_per_share.read(market_id); + + // Update user fps. self.user_fees_per_share.write((market_id, user), fps); - return (0, 0); - } - // Accrued fee balances exist, calculate fee balances to collect. - let base_fees = math::mul_div( - user_shares, fps.base_fps - user_fps.base_fps, ONE, false - ); - let quote_fees = math::mul_div( - user_shares, fps.quote_fps - user_fps.quote_fps, ONE, false - ); + // No accrued fee balances exist. Set user fps to pool fps and return. + if user_shares == 0 + || (user_fps.base_fps == fps.base_fps && user_fps.quote_fps == fps.quote_fps) { + return (0, 0); + } + + // Accrued fee balances exist, calculate fee balances to collect. + base_fees = math::mul_div( + user_shares, fps.base_fps - user_fps.base_fps, ONE, false + ); + quote_fees = math::mul_div( + user_shares, fps.quote_fps - user_fps.quote_fps, ONE, false + ); + } else { + base_fees = market_state.base_fees; + quote_fees = market_state.quote_fees; + } // Update fee reserves and commit state changes. market_state.base_fees -= base_fees; @@ -1181,9 +1194,6 @@ pub mod SolverComponent { quote_token.transfer(user, quote_fees - quote_withdraw_fees); } - // Set user fps to pool fps. - self.user_fees_per_share.write((market_id, user), fps); - // Return collected fees gross of withdraw fees. (base_fees, quote_fees) } From e742e29d1a0eea52d4b6d225cfde79172d8b6a18 Mon Sep 17 00:00:00 2001 From: parketh Date: Thu, 12 Sep 2024 22:40:57 +0100 Subject: [PATCH 08/10] fix tests --- packages/core/src/contracts/solver.cairo | 36 +- packages/core/src/interfaces/ISolver.cairo | 11 +- .../tests/solver/test_deposit_initial.cairo | 14 +- .../src/tests/solver/test_get_balances.cairo | 6 +- .../core/src/tests/solver/test_withdraw.cairo | 57 +- packages/core/src/types.cairo | 2 +- .../replicating/src/libraries/swap_lib.cairo | 9 - .../replicating/src/tests/helpers/utils.cairo | 6 + packages/replicating/src/tests/solver.cairo | 1 + .../tests/solver/test_deposit_initial.cairo | 6 +- .../src/tests/solver/test_e2e.cairo | 24 +- .../tests/solver/test_fees_per_share.cairo | 642 ++++++++++++++++++ .../src/tests/solver/test_withdraw.cairo | 61 +- 13 files changed, 779 insertions(+), 96 deletions(-) create mode 100644 packages/replicating/src/tests/solver/test_fees_per_share.cairo diff --git a/packages/core/src/contracts/solver.cairo b/packages/core/src/contracts/solver.cairo index a1a8cd8..2de5de6 100644 --- a/packages/core/src/contracts/solver.cairo +++ b/packages/core/src/contracts/solver.cairo @@ -265,6 +265,20 @@ pub mod SolverComponent { self.queued_owner.read() } + // Fees per share for a given market + fn fees_per_share( + self: @ComponentState, market_id: felt252 + ) -> FeesPerShare { + self.fees_per_share.read(market_id) + } + + // Fees per share for a given market and user + fn user_fees_per_share( + self: @ComponentState, market_id: felt252, user: ContractAddress + ) -> FeesPerShare { + self.user_fees_per_share.read((market_id, user)) + } + // Withdraw fee rate for a given market fn withdraw_fee_rate(self: @ComponentState, market_id: felt252) -> u16 { self.withdraw_fee_rate.read(market_id) @@ -824,13 +838,14 @@ pub mod SolverComponent { ref self: ComponentState, market_id: felt252, shares: u256 ) -> Amounts { // Fetch state. - let market_info = self.market_info.read(market_id); + let market_info: MarketInfo = self.market_info.read(market_id); let mut state: MarketState = self.market_state.read(market_id); // Run checks. assert(market_info.base_token != contract_address_const::<0x0>(), 'MarketNull'); assert(shares != 0, 'SharesZero'); assert(market_info.is_public, 'UseWithdrawPrivate'); + let vault_token = ERC20ABIDispatcher { contract_address: state.vault_token }; let caller = get_caller_address(); assert(shares <= vault_token.balanceOf(caller), 'InsuffShares'); @@ -841,6 +856,8 @@ pub mod SolverComponent { let (base_fees, quote_fees) = self._collect_fees(market_id); // Burn shares. + // Must refetch market state here after fees are collected. + state = self.market_state.read(market_id); IVaultTokenDispatcher { contract_address: state.vault_token }.burn(caller, shares); // Calculate share of reserves to withdraw. Commit state changes. @@ -876,8 +893,7 @@ pub mod SolverComponent { quote_amount: u256 ) -> Amounts { // Fetch state. - let market_info = self.market_info.read(market_id); - let mut state: MarketState = self.market_state.read(market_id); + let market_info: MarketInfo = self.market_info.read(market_id); // Run checks. assert(market_info.base_token != contract_address_const::<0x0>(), 'MarketNull'); @@ -888,6 +904,8 @@ pub mod SolverComponent { let (base_fees, quote_fees) = self._collect_fees(market_id); // Cap withdraw amount at available. Commit state changes. + // Must fetch market state here after fees are collected. + let mut state: MarketState = self.market_state.read(market_id); let base_withdraw = min(base_amount, state.base_reserves); let quote_withdraw = min(quote_amount, state.quote_reserves); @@ -1131,7 +1149,7 @@ pub mod SolverComponent { // Calculate accrued fee balances. let mut base_fees = 0; let mut quote_fees = 0; - if market_state.is_public { + if market_info.is_public { let vault_token = ERC20ABIDispatcher { contract_address: market_state.vault_token }; let user_shares = vault_token.balanceOf(user); let user_fps: FeesPerShare = self.user_fees_per_share.read((market_id, user)); @@ -1147,12 +1165,10 @@ pub mod SolverComponent { } // Accrued fee balances exist, calculate fee balances to collect. - base_fees = math::mul_div( - user_shares, fps.base_fps - user_fps.base_fps, ONE, false - ); - quote_fees = math::mul_div( - user_shares, fps.quote_fps - user_fps.quote_fps, ONE, false - ); + base_fees = + math::mul_div(user_shares, fps.base_fps - user_fps.base_fps, ONE, false); + quote_fees = + math::mul_div(user_shares, fps.quote_fps - user_fps.quote_fps, ONE, false); } else { base_fees = market_state.base_fees; quote_fees = market_state.quote_fees; diff --git a/packages/core/src/interfaces/ISolver.cairo b/packages/core/src/interfaces/ISolver.cairo index d861edf..92309c1 100644 --- a/packages/core/src/interfaces/ISolver.cairo +++ b/packages/core/src/interfaces/ISolver.cairo @@ -4,7 +4,8 @@ use starknet::class_hash::ClassHash; // Local imports. use haiko_solver_core::types::{ - MarketState, MarketInfo, PositionInfo, SwapParams, Amounts, AmountsWithShares, SwapAmounts + MarketState, MarketInfo, PositionInfo, SwapParams, Amounts, AmountsWithShares, SwapAmounts, + FeesPerShare }; #[starknet::interface] @@ -39,6 +40,14 @@ pub trait ISolver { // Queued contract owner, used for ownership transfers fn queued_owner(self: @TContractState) -> ContractAddress; + // Fees per share for a given market + fn fees_per_share(self: @TContractState, market_id: felt252) -> FeesPerShare; + + // Fees per share for a given market and user + fn user_fees_per_share( + self: @TContractState, market_id: felt252, user: ContractAddress + ) -> FeesPerShare; + // Withdraw fee rate for a given market fn withdraw_fee_rate(self: @TContractState, market_id: felt252) -> u16; diff --git a/packages/core/src/tests/solver/test_deposit_initial.cairo b/packages/core/src/tests/solver/test_deposit_initial.cairo index 82b4fe0..f08a328 100644 --- a/packages/core/src/tests/solver/test_deposit_initial.cairo +++ b/packages/core/src/tests/solver/test_deposit_initial.cairo @@ -146,7 +146,7 @@ fn test_deposit_initial_private_both_tokens() { // Run checks. assert(res.base_amount == base_amount, 'Base deposit'); assert(res.quote_amount == quote_amount, 'Quote deposit'); - assert(res.shares != 0, 'Shares'); + assert(res.shares == 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); @@ -167,15 +167,17 @@ fn test_deposit_initial_private_base_token_only() { start_prank(CheatTarget::One(solver.contract_address), owner()); let base_amount = to_e18(100); let quote_amount = 0; - let res = solver.deposit_initial(market_id, base_amount, quote_amount); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); // Snapshot after. let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, owner()); // Run checks. - assert(res.base_amount == base_amount, 'Base deposit'); - assert(res.quote_amount == quote_amount, 'Quote deposit'); - assert(res.shares != 0, 'Shares'); + assert(dep_init.base_amount == base_amount, 'Base deposit'); + assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); + assert(dep_init.shares == 0, 'Shares'); + assert(dep_init.base_fees == 0, 'Base fees'); + assert(dep_init.quote_fees == 0, 'Quote fees'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); @@ -204,7 +206,7 @@ fn test_deposit_initial_private_quote_token_only() { // Run checks. assert(res.base_amount == base_amount, 'Base deposit'); assert(res.quote_amount == quote_amount, 'Quote deposit'); - assert(res.shares != 0, 'Shares'); + assert(res.shares == 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); diff --git a/packages/core/src/tests/solver/test_get_balances.cairo b/packages/core/src/tests/solver/test_get_balances.cairo index 839aa11..468f0e4 100644 --- a/packages/core/src/tests/solver/test_get_balances.cairo +++ b/packages/core/src/tests/solver/test_get_balances.cairo @@ -13,7 +13,7 @@ use haiko_solver_core::{ // Haiko imports. use haiko_lib::math::math; use haiko_lib::helpers::params::{owner, alice, bob}; -use haiko_lib::helpers::utils::{to_e18, approx_eq}; +use haiko_lib::helpers::utils::{to_e18, approx_eq, approx_eq_pct}; // External imports. use snforge_std::{ @@ -108,7 +108,7 @@ fn test_get_user_balances() { 'Quote owner' ); assert(bal_owner.base_fees == 0, 'Base fees owner'); - assert(approx_eq(bal_owner.quote_fees, swap.fees * 2 / 3, 1), 'Quote fees owner'); + assert(approx_eq_pct(bal_owner.quote_fees, swap.fees * 2 / 3, 10), 'Quote fees owner'); assert( approx_eq(bal_alice.base_amount, base_deposit_alice - swap.amount_out / 3, 1), 'Base alice' ); @@ -119,5 +119,5 @@ fn test_get_user_balances() { 'Quote alice' ); assert(bal_alice.base_fees == 0, 'Base fees alice'); - assert(approx_eq(bal_alice.quote_fees, swap.fees / 3, 1), 'Quote fees alice'); + assert(approx_eq_pct(bal_alice.quote_fees, swap.fees / 3, 10), 'Quote fees alice'); } diff --git a/packages/core/src/tests/solver/test_withdraw.cairo b/packages/core/src/tests/solver/test_withdraw.cairo index ea5b759..2e419b8 100644 --- a/packages/core/src/tests/solver/test_withdraw.cairo +++ b/packages/core/src/tests/solver/test_withdraw.cairo @@ -67,14 +67,17 @@ fn test_withdraw_partial_shares_from_public_vault() { let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in) / 4, 10), 'Base deposit'); + assert( + approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in - swap.fees) / 4, 10), + 'Base deposit' + ); assert( approx_eq(wd.quote_amount, (quote_amount * 2 - swap.amount_out) / 4, 10), 'Quote deposit' ); - assert(approx_eq(wd.base_fees, swap.fees / 4, 1), 'Base fees'); + assert(approx_eq_pct(wd.base_fees, swap.fees / 2, 10), 'Base fees'); assert(wd.quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal - dep.shares / 2, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal - dep.shares / 2, 'LP total shares'); assert( @@ -130,13 +133,16 @@ fn test_withdraw_remaining_shares_from_public_vault() { let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in) / 2, 1), 'Base deposit'); + assert( + approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in - swap.fees) / 2, 1), + 'Base deposit' + ); assert( approx_eq(wd.quote_amount, (quote_amount * 2 - swap.amount_out) / 2, 1), 'Quote deposit' ); - assert(wd.base_fees == swap.fees / 2, 'Base fees'); + assert(approx_eq_pct(wd.base_fees, swap.fees / 2, 10), 'Base fees'); assert(wd.quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal - dep.shares, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal - dep.shares, 'LP total shares'); @@ -212,12 +218,12 @@ fn test_withdraw_private_base_only() { let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 1), 'Base withdraw'); + assert(approx_eq(wd.base_amount, base_amount + swap.amount_in - swap.fees, 1), 'Base withdraw'); assert(wd.quote_amount == 0, 'Quote withdraw'); assert(approx_eq(wd.base_fees, swap.fees, 1), 'Base fees'); assert(wd.quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); } @@ -260,10 +266,10 @@ fn test_withdraw_private_quote_only() { // Run checks. assert(wd.base_amount == 0, 'Base withdraw'); assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 1), 'Quote withdraw'); - assert(wd.base_fees == 0, 'Base fees'); + assert(approx_eq_pct(wd.base_fees, swap.fees, 10), 'Base fees'); assert(wd.quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); } @@ -309,10 +315,10 @@ fn test_withdraw_private_partial_amounts() { // Run checks. assert(approx_eq(wd.base_amount, (base_amount + swap.amount_in) / 2, 1), 'Base withdraw'); assert(approx_eq(wd.quote_amount, (quote_amount - swap.amount_out) / 2, 1), 'Quote withdraw'); - assert(approx_eq(wd.base_fees, swap.fees / 2, 1), 'Base fees'); + assert(approx_eq(wd.base_fees, swap.fees, 1), 'Base fees'); assert(wd.quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); assert( @@ -356,18 +362,20 @@ fn test_withdraw_all_remaining_balances_from_private_vault() { // Withdraw. start_prank(CheatTarget::One(solver.contract_address), owner()); let wd = solver - .withdraw_private(market_id, base_amount + swap.amount_in, quote_amount - swap.amount_out); + .withdraw_private( + market_id, base_amount + swap.amount_in - swap.fees, quote_amount - swap.amount_out + ); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 1), 'Base withdraw'); + assert(approx_eq(wd.base_amount, base_amount + swap.amount_in - swap.fees, 1), 'Base withdraw'); assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 1), 'Quote withdraw'); assert(wd.base_fees == swap.fees, 'Base fees'); assert(wd.quote_fees == 0, 'Quote fees'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == 0, 'LP shares'); assert(aft.vault_total_bal == 0, 'LP total shares'); assert( @@ -378,6 +386,13 @@ fn test_withdraw_all_remaining_balances_from_private_vault() { aft.market_state.quote_reserves == bef.market_state.quote_reserves - wd.quote_amount, 'Quote reserve' ); + assert( + aft.market_state.base_fees == bef.market_state.base_fees - wd.base_fees, 'Base fees reserve' + ); + assert( + aft.market_state.quote_fees == bef.market_state.quote_fees - wd.quote_fees, + 'Quote fees reserve' + ); } #[test] @@ -409,7 +424,7 @@ fn test_withdraw_more_than_available_correctly_caps_amount_for_private_vault() { let wd = solver.withdraw_private(market_id, base_amount * 2, quote_amount * 2); // Run checks. - assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 1), 'Base withdraw'); + assert(approx_eq(wd.base_amount, base_amount + swap.amount_in - swap.fees, 1), 'Base withdraw'); assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 1), 'Quote withdraw'); assert(wd.base_fees == swap.fees, 'Base fees'); assert(wd.quote_fees == 0, 'Quote fees'); diff --git a/packages/core/src/types.cairo b/packages/core/src/types.cairo index 43d579d..db47d0c 100644 --- a/packages/core/src/types.cairo +++ b/packages/core/src/types.cairo @@ -41,7 +41,7 @@ pub struct MarketState { // // * `base_fps` - base fees per share // * `quote_fps` - quote fees per share -#[derive(Drop, Copy, Serde)] +#[derive(Drop, Copy, Serde, Default, PartialEq)] pub struct FeesPerShare { pub base_fps: u256, pub quote_fps: u256, diff --git a/packages/replicating/src/libraries/swap_lib.cairo b/packages/replicating/src/libraries/swap_lib.cairo index 0505b2c..1ad7c0a 100644 --- a/packages/replicating/src/libraries/swap_lib.cairo +++ b/packages/replicating/src/libraries/swap_lib.cairo @@ -84,15 +84,6 @@ pub fn compute_swap_amounts( fee_rate: u16, exact_input: bool, ) -> (u256, u256, u256, u256) { - println!( - "curr_sqrt_price: {}, target_sqrt_price: {}, liquidity: {}, amount: {}, fee_rate: {}, exact_input: {}", - curr_sqrt_price, - target_sqrt_price, - liquidity, - amount, - fee_rate, - exact_input - ); // Determine whether swap is a buy or sell. let is_buy = target_sqrt_price > curr_sqrt_price; diff --git a/packages/replicating/src/tests/helpers/utils.cairo b/packages/replicating/src/tests/helpers/utils.cairo index 85f90e6..1779379 100644 --- a/packages/replicating/src/tests/helpers/utils.cairo +++ b/packages/replicating/src/tests/helpers/utils.cairo @@ -222,6 +222,12 @@ fn _before( approve(base_token, alice(), solver.contract_address, base_amount); approve(quote_token, alice(), solver.contract_address, quote_amount); } + fund(base_token, bob(), base_amount); + fund(quote_token, bob(), quote_amount); + if approve_solver { + approve(base_token, bob(), solver.contract_address, base_amount); + approve(quote_token, bob(), solver.contract_address, quote_amount); + } ( base_token, diff --git a/packages/replicating/src/tests/solver.cairo b/packages/replicating/src/tests/solver.cairo index 2c9c377..8848423 100644 --- a/packages/replicating/src/tests/solver.cairo +++ b/packages/replicating/src/tests/solver.cairo @@ -6,6 +6,7 @@ pub mod test_swap; pub mod test_withdraw; pub mod test_market_params; pub mod test_oracle; +pub mod test_fees_per_share; // Disabled - used for debugging of live vaults // pub mod debug_swap; diff --git a/packages/replicating/src/tests/solver/test_deposit_initial.cairo b/packages/replicating/src/tests/solver/test_deposit_initial.cairo index c2f3584..9d4f6e5 100644 --- a/packages/replicating/src/tests/solver/test_deposit_initial.cairo +++ b/packages/replicating/src/tests/solver/test_deposit_initial.cairo @@ -195,7 +195,7 @@ fn test_deposit_initial_private_both_tokens() { // Run checks. assert(dep_init.base_amount == base_amount, 'Base deposit'); assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); - assert(dep_init.shares != 0, 'Shares'); + assert(dep_init.shares == 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); @@ -243,7 +243,7 @@ fn test_deposit_initial_private_base_token_only() { // Run checks. assert(dep_init.base_amount == base_amount, 'Base deposit'); assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); - assert(dep_init.shares != 0, 'Shares'); + assert(dep_init.shares == 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); @@ -291,7 +291,7 @@ fn test_deposit_initial_private_quote_token_only() { // Run checks. assert(dep_init.base_amount == base_amount, 'Base deposit'); assert(dep_init.quote_amount == quote_amount, 'Quote deposit'); - assert(dep_init.shares != 0, 'Shares'); + assert(dep_init.shares == 0, 'Shares'); assert(aft.lp_base_bal == bef.lp_base_bal - base_amount, 'LP base bal'); assert(aft.lp_quote_bal == bef.lp_quote_bal - quote_amount, 'LP quote bal'); assert(aft.market_state.base_reserves == base_amount, 'Base reserve'); diff --git a/packages/replicating/src/tests/solver/test_e2e.cairo b/packages/replicating/src/tests/solver/test_e2e.cairo index 3f89440..17c7ddb 100644 --- a/packages/replicating/src/tests/solver/test_e2e.cairo +++ b/packages/replicating/src/tests/solver/test_e2e.cairo @@ -99,10 +99,7 @@ fn test_solver_e2e_private_market() { 'Quote reserves' ); assert(wd.base_fees == 0, 'Base fees'); - assert( - approx_eq(wd.quote_fees, swap.fees * to_e18(300) / (to_e18(1500) + swap.amount_in), 1), - 'Quote fees' - ); + assert(approx_eq_pct(wd.quote_fees, swap.fees, 10), 'Quote fees'); assert(dep_init.shares == 0, 'Shares init'); assert(dep.shares == 0, 'Shares'); } @@ -158,26 +155,13 @@ fn test_solver_e2e_public_market() { let quote_deposit_exp = to_e18(500); assert(dep.base_amount == base_deposit_exp, 'Base deposit'); assert(dep.quote_amount == quote_deposit_exp, 'Quote deposit'); + assert(approx_eq(wd.base_amount, base_deposit_exp - swap.amount_out / 3, 10), 'Base withdraw'); assert( - approx_eq( - wd.base_amount, - math::mul_div(base_deposit_exp - swap.amount_out / 3, 995, 1000, false), - 10 - ), - 'Base withdraw' - ); - assert( - approx_eq( - wd.quote_amount, - math::mul_div(quote_deposit_exp + swap.amount_in / 3, 995, 1000, false), - 10 - ), + approx_eq(wd.quote_amount, quote_deposit_exp + (swap.amount_in - swap.fees) / 3, 10), 'Quote withdraw' ); assert(wd.base_fees == 0, 'Base fees'); - assert( - approx_eq(wd.quote_fees, math::mul_div(swap.fees / 3, 995, 1000, false), 10), 'Quote fees' - ); + assert(approx_eq(wd.quote_fees, swap.fees / 3, 10), 'Quote fees'); assert(dep.shares == dep_init.shares / 2, 'Shares'); assert( approx_eq( diff --git a/packages/replicating/src/tests/solver/test_fees_per_share.cairo b/packages/replicating/src/tests/solver/test_fees_per_share.cairo new file mode 100644 index 0000000..998a4c8 --- /dev/null +++ b/packages/replicating/src/tests/solver/test_fees_per_share.cairo @@ -0,0 +1,642 @@ +// Core lib imports. +use starknet::contract_address_const; + +// Local imports. +use haiko_solver_core::{ + contracts::solver::SolverComponent, + interfaces::ISolver::{ISolverDispatcher, ISolverDispatcherTrait}, types::SwapParams, +}; +use haiko_solver_replicating::{ + contracts::mocks::mock_pragma_oracle::{ + IMockPragmaOracleDispatcher, IMockPragmaOracleDispatcherTrait + }, + interfaces::IReplicatingSolver::{ + IReplicatingSolverDispatcher, IReplicatingSolverDispatcherTrait + }, + types::MarketParams, + tests::{ + helpers::{ + actions::{deploy_replicating_solver, deploy_mock_pragma_oracle}, + params::default_market_params, + utils::{before, before_custom_decimals, before_skip_approve, snapshot}, + }, + }, +}; + +// Haiko imports. +use haiko_lib::helpers::params::{owner, alice, bob}; +use haiko_lib::helpers::utils::{to_e18, approx_eq, approx_eq_pct}; +use haiko_lib::helpers::actions::token::{fund, approve}; + +// External imports. +use snforge_std::{ + start_prank, stop_prank, start_warp, declare, spy_events, SpyOn, EventSpy, EventAssertions, + CheatTarget +}; +use openzeppelin::token::erc20::interface::{ERC20ABIDispatcher, ERC20ABIDispatcherTrait}; + +/////////////////////////////////// +// TESTS - Success cases (Public) +/////////////////////////////////// + +#[test] +fn test_fps_public_market_fee_per_shares_correctly_updated() { + let ( + _base_token, _quote_token, _oracle, _vault_token_class, solver, market_id, _vault_token_opt + ) = + before( + true + ); + + // Deposit initial. + // after: market fps = 0, owner fps = 0 + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_amount = to_e18(100); + let quote_amount = to_e18(500); + let dep_init = solver.deposit_initial(market_id, base_amount, quote_amount); + let mut market_fps = solver.fees_per_share(market_id); + let mut owner_fps = solver.user_fees_per_share(market_id, owner()); + assert(market_fps.base_fps == 0, 'Market base fps init'); + assert(market_fps.quote_fps == 0, 'Market quote fps init'); + assert(owner_fps.base_fps == 0, 'Owner base fps init'); + assert(owner_fps.quote_fps == 0, 'Owner quote fps init'); + + // Swap. + // after: market fps = [1], owner fps = 0 + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + market_fps = solver.fees_per_share(market_id); + owner_fps = solver.user_fees_per_share(market_id, owner()); + assert(market_fps.base_fps > owner_fps.base_fps, 'Owner fps 1'); + assert(market_fps.quote_fps == owner_fps.quote_fps, 'Owner fps 1'); + + // Deposit. + // after: market fps = [1], owner fps = 0, alice fps = [1] + start_prank(CheatTarget::One(solver.contract_address), alice()); + let dep = solver.deposit(market_id, base_amount, quote_amount); + market_fps = solver.fees_per_share(market_id); + owner_fps = solver.user_fees_per_share(market_id, owner()); + let mut alice_fps = solver.user_fees_per_share(market_id, alice()); + assert(market_fps == alice_fps, 'Market + alice fps 2'); + assert(owner_fps == Default::default(), 'Owner fps 2'); + + // Withdraw. + // after: market fps = [1], owner fps = [1], alice fps = [1] + start_prank(CheatTarget::One(solver.contract_address), owner()); + solver.withdraw_public(market_id, dep_init.shares); + market_fps = solver.fees_per_share(market_id); + owner_fps = solver.user_fees_per_share(market_id, owner()); + alice_fps = solver.user_fees_per_share(market_id, alice()); + assert(market_fps == owner_fps, 'Market fps 3'); + assert(owner_fps == alice_fps, 'Owner fps 3'); + + // Swap. + // after: market fps = [2], owner fps = [1], alice fps = [1] + let params = SwapParams { + is_buy: true, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + market_fps = solver.fees_per_share(market_id); + owner_fps = solver.user_fees_per_share(market_id, owner()); + alice_fps = solver.user_fees_per_share(market_id, alice()); + assert(market_fps.base_fps == owner_fps.base_fps, 'Market + owner base fps 4'); + assert(market_fps.quote_fps > owner_fps.quote_fps, 'Market + owner quote fps 4'); + assert(owner_fps == alice_fps, 'Owner + alice fps 4'); + + // Withdraw. + // after: market fps = [2], owner fps = [1], alice fps = [2] + start_prank(CheatTarget::One(solver.contract_address), alice()); + solver.withdraw_public(market_id, dep.shares / 2); + market_fps = solver.fees_per_share(market_id); + owner_fps = solver.user_fees_per_share(market_id, owner()); + alice_fps = solver.user_fees_per_share(market_id, alice()); + assert(market_fps == alice_fps, 'Alice fps 5'); + assert(market_fps.base_fps == owner_fps.base_fps, 'Market + owner base fps 5'); + assert(market_fps.quote_fps > owner_fps.quote_fps, 'Market + owner quote fps 5'); +} + +#[test] +fn test_fps_deposit_and_withdraw_to_public_market_with_no_fee_balance() { + let ( + _base_token, _quote_token, _oracle, _vault_token_class, solver, market_id, _vault_token_opt + ) = + before( + true + ); + + // Deposit initial. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_amount = to_e18(100); + let quote_amount = to_e18(500); + solver.deposit_initial(market_id, base_amount, quote_amount); + + // Deposit. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let dep = solver.deposit(market_id, base_amount, quote_amount); + + // Withdraw. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let wd_alice = solver.withdraw_public(market_id, dep.shares); + assert(approx_eq(wd_alice.base_amount, base_amount, 1), 'Alice base amount'); + assert(approx_eq(wd_alice.quote_amount, quote_amount, 1), 'Alice quote amount'); + + // Withdraw. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let wd_owner = solver.withdraw_public(market_id, dep.shares); + assert(approx_eq(wd_owner.base_amount, base_amount, 1), 'Owner base amount'); + assert(approx_eq(wd_owner.quote_amount, quote_amount, 1), 'Owner quote amount'); +} + +#[test] +fn test_fps_deposit_to_public_market_with_existing_fee_balance_doesnt_skim_profits() { + let ( + _base_token, _quote_token, _oracle, _vault_token_class, solver, market_id, _vault_token_opt + ) = + before( + true + ); + + // Deposit initial. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_amount = to_e18(100); + let quote_amount = to_e18(500); + solver.deposit_initial(market_id, base_amount, quote_amount); + + // Swap. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let swap = solver.swap(market_id, params); + + // Deposit. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let dep = solver.deposit(market_id, base_amount, quote_amount); + + // Get balances. + let owner = solver.get_user_balances(owner(), market_id); + let alice = solver.get_user_balances(alice(), market_id); + + // Check balances. + assert(approx_eq(owner.base_fees, swap.fees, 1), 'Owner base fees'); + assert(owner.quote_fees == 0, 'Owner quote fees'); + assert(alice.base_fees == 0, 'Alice base fees'); + assert(alice.quote_fees == 0, 'Alice quote fees'); + + // Withdraw and confirm. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let wd_alice = solver.withdraw_public(market_id, dep.shares); + assert(wd_alice.base_fees == 0, 'Alice base fees'); + assert(wd_alice.quote_fees == 0, 'Alice quote fees'); + + // Withdraw owner. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let wd_owner = solver.withdraw_public(market_id, dep.shares); + assert(approx_eq(wd_owner.base_fees, swap.fees, 1), 'Owner base fees'); + assert(wd_owner.quote_fees == 0, 'Owner quote fees'); +} + +#[test] +fn test_fps_public_market_multiple_lps_base() { + let ( + _base_token, _quote_token, _oracle, _vault_token_class, solver, market_id, _vault_token_opt + ) = + before( + true + ); + + // Disable max skew. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let mut market_params = default_market_params(); + market_params.max_skew = 0; + let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; + repl_solver.queue_market_params(market_id, market_params); + repl_solver.set_market_params(market_id); + + // Deposit initial (owner). + // ownership: owner = 100%, alice = 0%, bob = 0% + // base fees: owner = 0, alice = 0, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_amount = to_e18(500); + let quote_amount = to_e18(1000); + let dep_owner = solver.deposit_initial(market_id, base_amount, quote_amount); + + // Swap. + // ownership: owner = 100%, alice = 0%, bob = 0% + // base fees: owner = 0.05, alice = 0, bob = 0 + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + + // Deposit (alice). + // ownership: owner = 50%, alice = 50%, bob = 0% + // base fees: owner = 0.05, alice = 0, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), alice()); + let mut balances = solver.get_balances(market_id); + solver.deposit(market_id, balances.base_amount, balances.quote_amount); + + // Swap. + // ownership: owner = 50%, alice = 50%, bob = 0% + // base fees: owner = 0.175, alice = 0.125, bob = 0 + let params = SwapParams { + is_buy: false, + amount: to_e18(50), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + + // Withdraw (owner). + // ownership: owner = 0%, alice = 100%, bob = 0% + // base fees: owner = 0, alice = 0.125, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), owner()); + let wd_owner = solver.withdraw_public(market_id, dep_owner.shares); + + // Deposit (bob). + // ownership: owner = 0%, alice = 66.66%, bob = 33.33% + // base fees: owner = 0, alice = 0.125, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), bob()); + balances = solver.get_balances(market_id); + solver.deposit(market_id, balances.base_amount / 2, balances.quote_amount / 2); + + // Deposit again (Alice). + // ownership: owner = 0%, alice = 75%, bob = 25% + // base fees: owner = 0, alice = 0, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), alice()); + balances = solver.get_balances(market_id); + let dep_alice_2 = solver + .deposit(market_id, balances.base_amount / 3, balances.quote_amount / 3); + + // Swap. + // ownership: owner = 0%, alice = 75%, bob = 25% + // base fees: owner = 0, alice = 0.15, bob = 0.05 + let params = SwapParams { + is_buy: false, + amount: to_e18(40), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + + // Check withdrawn fees. + assert(approx_eq(wd_owner.base_fees, 175000000000000000, 1), 'Owner withdrawn base fees'); + assert(wd_owner.quote_fees == 0, 'Owner withdrawn quote fees'); + assert(approx_eq(dep_alice_2.base_fees, 125000000000000000, 1), 'Alice deposit 2 base fees'); + assert(dep_alice_2.quote_fees == 0, 'Alice deposit 2 quote fees'); + + // Check fee amounts. + let owner_bal = solver.get_user_balances(owner(), market_id); + let alice_bal = solver.get_user_balances(alice(), market_id); + let bob_bal = solver.get_user_balances(bob(), market_id); + assert(owner_bal.base_fees == 0, 'Owner base fees'); + assert(owner_bal.quote_fees == 0, 'Owner quote fees'); + assert(approx_eq(alice_bal.base_fees, 150000000000000000, 1), 'Alice base fees'); + assert(alice_bal.quote_fees == 0, 'Alice quote fees'); + assert(approx_eq(bob_bal.base_fees, 50000000000000000, 1), 'Bob base fees'); + assert(bob_bal.quote_fees == 0, 'Bob quote fees'); +} + +#[test] +fn test_fps_public_market_multiple_lps_quote() { + let ( + _base_token, _quote_token, _oracle, _vault_token_class, solver, market_id, _vault_token_opt + ) = + before( + true + ); + + // Disable max skew. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let mut market_params = default_market_params(); + market_params.max_skew = 0; + let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; + repl_solver.queue_market_params(market_id, market_params); + repl_solver.set_market_params(market_id); + + // Deposit initial (owner). + // ownership: owner = 100%, alice = 0%, bob = 0% + // quote fees: owner = 0, alice = 0, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_amount = to_e18(500); + let quote_amount = to_e18(1000); + let dep_owner = solver.deposit_initial(market_id, base_amount, quote_amount); + + // Swap. + // ownership: owner = 100%, alice = 0%, bob = 0% + // quote fees: owner = 0.05, alice = 0, bob = 0 + let params = SwapParams { + is_buy: true, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + + // Deposit (alice). + // ownership: owner = 50%, alice = 50%, bob = 0% + // quote fees: owner = 0.05, alice = 0, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), alice()); + let mut balances = solver.get_balances(market_id); + solver.deposit(market_id, balances.base_amount, balances.quote_amount); + + // Swap. + // ownership: owner = 50%, alice = 50%, bob = 0% + // quote fees: owner = 0.175, alice = 0.125, bob = 0 + let params = SwapParams { + is_buy: true, + amount: to_e18(50), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + + // Withdraw (owner). + // ownership: owner = 0%, alice = 100%, bob = 0% + // quote fees: owner = 0, alice = 0.125, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), owner()); + let wd_owner = solver.withdraw_public(market_id, dep_owner.shares); + + // Deposit (bob). + // ownership: owner = 0%, alice = 66.66%, bob = 33.33% + // quote fees: owner = 0, alice = 0.125, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), bob()); + balances = solver.get_balances(market_id); + solver.deposit(market_id, balances.base_amount / 2, balances.quote_amount / 2); + + // Deposit again (Alice). + // ownership: owner = 0%, alice = 75%, bob = 25% + // quote fees: owner = 0, alice = 0, bob = 0 + start_prank(CheatTarget::One(solver.contract_address), alice()); + balances = solver.get_balances(market_id); + let dep_alice_2 = solver + .deposit(market_id, balances.base_amount / 3, balances.quote_amount / 3); + + // Swap. + // ownership: owner = 0%, alice = 75%, bob = 25% + // quote fees: owner = 0, alice = 0.15, bob = 0.05 + let params = SwapParams { + is_buy: true, + amount: to_e18(40), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + + // Check withdrawn fees. + assert(wd_owner.base_fees == 0, 'Owner withdrawn base fees'); + assert(approx_eq(wd_owner.quote_fees, 175000000000000000, 1), 'Owner withdrawn quote fees'); + assert(dep_alice_2.base_fees == 0, 'Alice deposit 2 base fees'); + assert(approx_eq(dep_alice_2.quote_fees, 125000000000000000, 1), 'Alice deposit 2 quote fees'); + + // Check fee amounts. + let owner_bal = solver.get_user_balances(owner(), market_id); + let alice_bal = solver.get_user_balances(alice(), market_id); + let bob_bal = solver.get_user_balances(bob(), market_id); + assert(owner_bal.base_fees == 0, 'Owner base fees'); + assert(owner_bal.quote_fees == 0, 'Owner quote fees'); + assert(alice_bal.base_fees == 0, 'Alice base fees'); + assert(approx_eq(alice_bal.quote_fees, 150000000000000000, 1), 'Alice quote fees'); + assert(bob_bal.base_fees == 0, 'Bob quote fees'); + assert(approx_eq(bob_bal.quote_fees, 50000000000000000, 1), 'Bob quote fees'); +} + +#[test] +fn test_fps_public_market_with_withdraw_fees() { + let (base_token, quote_token, _oracle, _vault_token_class, solver, market_id, vault_token_opt) = + before( + true + ); + + // Set withdraw fees. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let withraw_fee_rate = 100; + solver.set_withdraw_fee(market_id, withraw_fee_rate); + + // Deposit initial. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_amount = to_e18(500); + let quote_amount = to_e18(1000); + solver.deposit_initial(market_id, base_amount, quote_amount); + + // Swap 1. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + + // Deposit. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let balances = solver.get_balances(market_id); + let dep = solver.deposit(market_id, balances.base_amount, balances.quote_amount); + + // Swap 2. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let swap = solver.swap(market_id, params); + + // Snapshot before. + let vault_token = vault_token_opt.unwrap(); + let bef = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); + + // Withdraw. + start_prank(CheatTarget::One(solver.contract_address), alice()); + let wd = solver.withdraw_public(market_id, dep.shares); + + // Collect withdraw fees. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_withdraw_fees = solver.collect_withdraw_fees(owner(), base_token.contract_address); + + // Snapshot after. + let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); + + // Check withdrawn fees. + assert(approx_eq(wd.base_fees, 25000000000000000, 1), 'Base fees'); + assert(wd.quote_fees == 0, 'Quote fees'); + assert( + approx_eq( + aft.lp_base_bal - bef.lp_base_bal, (dep.base_amount + swap.amount_in / 2) * 99 / 100, 1 + ), + 'LP base bal' + ); + assert( + approx_eq(base_withdraw_fees, (dep.base_amount + swap.amount_in / 2) / 100, 1), + 'Base withdraw fees' + ); +} + +#[test] +fn test_fps_swap_with_0_fps_works() { + let ( + _base_token, _quote_token, _oracle, _vault_token_class, solver, market_id, _vault_token_opt + ) = + before( + true + ); + + // Deposit initial. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_amount = to_e18(500); + let quote_amount = to_e18(1000); + solver.deposit_initial(market_id, base_amount, quote_amount); + + // Snapshot fps before. + let market_fps_bef = solver.fees_per_share(market_id); + let user_fps_bef = solver.user_fees_per_share(market_id, owner()); + + // Swap. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let params = SwapParams { + is_buy: false, + amount: 10, + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + + // Snapshot fps after. + let market_fps_aft = solver.fees_per_share(market_id); + let user_fps_aft = solver.user_fees_per_share(market_id, owner()); + + // Check fps. + assert(market_fps_bef.base_fps == user_fps_bef.base_fps, 'Market fps before'); + assert(market_fps_bef.quote_fps == user_fps_bef.quote_fps, 'Market fps before'); + assert(market_fps_aft.base_fps == user_fps_aft.base_fps, 'Market fps after'); + assert(market_fps_aft.quote_fps == user_fps_aft.quote_fps, 'Market fps after'); +} + +#[test] +fn test_fps_mismatched_decimals_extreme_fps_values() { + let ( + _base_token, _quote_token, oracle, _vault_token_class, solver, market_id, _vault_token_opt + ) = + before_custom_decimals( + true, 18, 6 + ); + + // Disable max skew. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let mut market_params = default_market_params(); + market_params.max_skew = 0; + let repl_solver = IReplicatingSolverDispatcher { contract_address: solver.contract_address }; + repl_solver.queue_market_params(market_id, market_params); + repl_solver.set_market_params(market_id); + + // Set oracle price. + start_warp(CheatTarget::One(oracle.contract_address), 1000); + oracle.set_data_with_USD_hop('ETH', 'USDC', 100000, 8, 999, 5); + + // Deposit initial. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_amount = to_e18(1); + let quote_amount = to_e18(1); + solver.deposit_initial(market_id, base_amount, quote_amount); + + // Swap. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let params = SwapParams { + is_buy: true, + amount: 100000, + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + solver.swap(market_id, params); + + // Check fps and shares. + let balances = solver.get_user_balances(owner(), market_id); + assert(balances.quote_fees > 0 && balances.quote_fees < 500, 'Quote fees'); +} + +/////////////////////////////////// +// TESTS - Success cases (Private) +/////////////////////////////////// + +#[test] +fn test_fps_private_market_withdraws_full_balance() { + let ( + _base_token, _quote_token, _oracle, _vault_token_class, solver, market_id, _vault_token_opt + ) = + before( + false + ); + + // Deposit initial. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let base_amount = to_e18(500); + let quote_amount = to_e18(1000); + solver.deposit_initial(market_id, base_amount, quote_amount); + let mut market_fps = solver.fees_per_share(market_id); + let mut user_fps = solver.user_fees_per_share(market_id, owner()); + assert(market_fps.base_fps == 0 && market_fps.quote_fps == 0, 'Market fps init'); + assert(user_fps.base_fps == 0 && user_fps.quote_fps == 0, 'User fps init'); + + // Swap. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let params = SwapParams { + is_buy: false, + amount: to_e18(10), + exact_input: true, + threshold_sqrt_price: Option::None(()), + threshold_amount: Option::None(()), + deadline: Option::None(()), + }; + let swap = solver.swap(market_id, params); + market_fps = solver.fees_per_share(market_id); + user_fps = solver.user_fees_per_share(market_id, owner()); + assert(market_fps.base_fps == 0 && market_fps.quote_fps == 0, 'Market fps after swap'); + assert(user_fps.base_fps == 0 && user_fps.quote_fps == 0, 'User fps after swap'); + + // Withdraw. + start_prank(CheatTarget::One(solver.contract_address), owner()); + let balances = solver.get_balances(market_id); + let wd = solver.withdraw_private(market_id, balances.base_amount, balances.quote_amount); + market_fps = solver.fees_per_share(market_id); + user_fps = solver.user_fees_per_share(market_id, owner()); + assert(market_fps.base_fps == 0 && market_fps.quote_fps == 0, 'Market fps after withdraw'); + assert(user_fps.base_fps == 0 && user_fps.quote_fps == 0, 'User fps after withdraw'); + + // Check withdraw. + assert(approx_eq(wd.base_fees, swap.fees, 1), 'Base fees'); + assert(wd.quote_fees == 0, 'Quote fees'); +} diff --git a/packages/replicating/src/tests/solver/test_withdraw.cairo b/packages/replicating/src/tests/solver/test_withdraw.cairo index 391cbc7..7446822 100644 --- a/packages/replicating/src/tests/solver/test_withdraw.cairo +++ b/packages/replicating/src/tests/solver/test_withdraw.cairo @@ -79,14 +79,17 @@ fn test_withdraw_partial_shares_from_public_vault() { let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in) / 4, 10), 'Base deposit'); + assert( + approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in - swap.fees) / 4, 10), + 'Base deposit' + ); assert( approx_eq(wd.quote_amount, (quote_amount * 2 - swap.amount_out) / 4, 10), 'Quote deposit' ); - assert(approx_eq(wd.base_fees, swap.fees / 4, 10), 'Base fees withdraw'); + assert(approx_eq(wd.base_fees, swap.fees / 2, 10), 'Base fees withdraw'); assert(wd.quote_fees == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal - dep.shares / 2, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal - dep.shares / 2, 'LP total shares'); assert( @@ -153,14 +156,17 @@ fn test_withdraw_remaining_shares_from_public_vault() { let aft = snapshot(solver, market_id, base_token, quote_token, vault_token, alice()); // Run checks. - assert(approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in) / 2, 1), 'Base deposit'); + assert( + approx_eq(wd.base_amount, (base_amount * 2 + swap.amount_in - swap.fees) / 2, 1), + 'Base deposit' + ); assert( approx_eq(wd.quote_amount, (quote_amount * 2 - swap.amount_out) / 2, 1), 'Quote deposit' ); assert(approx_eq(wd.base_fees, swap.fees / 2, 1), 'Base fees withdraw'); assert(wd.quote_fees == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal - dep.shares, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal - dep.shares, 'LP total shares'); assert( @@ -254,12 +260,14 @@ fn test_withdraw_private_base_only() { let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 10), 'Base withdraw'); + assert( + approx_eq(wd.base_amount, base_amount + swap.amount_in - swap.fees, 10), 'Base withdraw' + ); assert(wd.quote_amount == 0, 'Quote withdraw'); assert(approx_eq(wd.base_fees, swap.fees, 10), 'Base fees withdraw'); assert(wd.quote_fees == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); } @@ -313,10 +321,10 @@ fn test_withdraw_private_quote_only() { // Run checks. assert(wd.base_amount == 0, 'Base withdraw'); assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 10), 'Quote withdraw'); - assert(wd.base_fees == 0, 'Base fees withdraw'); + assert(approx_eq(wd.base_fees, swap.fees, 10), 'Base fees withdraw'); assert(wd.quote_fees == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); } @@ -356,19 +364,24 @@ fn test_withdraw_private_partial_amounts() { start_prank(CheatTarget::One(solver.contract_address), owner()); let wd = solver .withdraw_private( - market_id, (base_amount + swap.amount_in) / 2, (quote_amount - swap.amount_out) / 2 + market_id, + (base_amount + swap.amount_in - swap.fees) / 2, + (quote_amount - swap.amount_out) / 2 ); // Snapshot after. let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(wd.base_amount, (base_amount + swap.amount_in) / 2, 10), 'Base withdraw'); + assert( + approx_eq(wd.base_amount, (base_amount + swap.amount_in - swap.fees) / 2, 10), + 'Base withdraw' + ); assert(approx_eq(wd.quote_amount, (quote_amount - swap.amount_out) / 2, 10), 'Quote withdraw'); - assert(approx_eq(wd.base_fees, swap.fees / 2, 10), 'Base fees withdraw'); + assert(approx_eq(wd.base_fees, swap.fees, 10), 'Base fees withdraw'); assert(wd.quote_fees == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == bef.vault_lp_bal, 'LP shares'); assert(aft.vault_total_bal == bef.vault_total_bal, 'LP total shares'); assert( @@ -430,12 +443,14 @@ fn test_withdraw_all_remaining_balances_from_private_vault() { let aft = snapshot(solver, market_id, _base_token, _quote_token, vault_token, owner()); // Run checks. - assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 10), 'Base withdraw'); + assert( + approx_eq(wd.base_amount, base_amount + swap.amount_in - swap.fees, 10), 'Base withdraw' + ); assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 10), 'Quote withdraw'); assert(approx_eq(wd.base_fees, swap.fees, 10), 'Base fees withdraw'); assert(wd.quote_fees == 0, 'Quote fees withdraw'); - assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount, 'LP base bal'); - assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount, 'LP quote bal'); + assert(aft.lp_base_bal == bef.lp_base_bal + wd.base_amount + wd.base_fees, 'LP base bal'); + assert(aft.lp_quote_bal == bef.lp_quote_bal + wd.quote_amount + wd.quote_fees, 'LP quote bal'); assert(aft.vault_lp_bal == 0, 'LP shares'); assert(aft.vault_total_bal == 0, 'LP total shares'); assert( @@ -485,7 +500,9 @@ fn test_withdraw_more_than_available_correctly_caps_amount_for_private_vault() { let wd = solver.withdraw_private(market_id, base_amount * 2, quote_amount * 2); // Run checks. - assert(approx_eq(wd.base_amount, base_amount + swap.amount_in, 10), 'Base withdraw'); + assert( + approx_eq(wd.base_amount, base_amount + swap.amount_in - swap.fees, 10), 'Base withdraw' + ); assert(approx_eq(wd.quote_amount, quote_amount - swap.amount_out, 10), 'Quote withdraw'); assert(wd.base_fees == swap.fees, 'Base fees withdraw'); assert(wd.quote_fees == 0, 'Quote fees withdraw'); From c839a7a1e0df1208032ae9e8e86857694fdce49e Mon Sep 17 00:00:00 2001 From: parketh Date: Fri, 13 Sep 2024 14:25:37 +0100 Subject: [PATCH 09/10] redeploy contracts --- scripts/sepolia.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/sepolia.sh b/scripts/sepolia.sh index 2b65784..9170ca9 100644 --- a/scripts/sepolia.sh +++ b/scripts/sepolia.sh @@ -31,8 +31,8 @@ starkli deploy --rpc $STARKNET_RPC $REPLICATING_SOLVER_CLASS $OWNER $ORACLE $VAU ############################# # 12 September 2024 -export REPLICATING_SOLVER=0x03423c755f3639b78be2705278bb067e59608ca479ac21d604e15c47e1ebf8be -export REPLICATING_SOLVER_CLASS=0x07236ed1addff06e12721bd5e152bd3eeeecdefb4702539147d7d72701227ad5 +export REPLICATING_SOLVER=0x0674de91977103c1902f35007263d0f5fdaa90aaf0959493c2721e23df108a4c +export REPLICATING_SOLVER_CLASS=0x030ba3f1857330dfe032ae7e43f51d4b206756ec4757fa3ac14e79ff79a98a44 # 22 August 2024 export REPLICATING_SOLVER_CLASS=0x0589858bd41fc0c922ff5c656f3373f7072fb8a69fcd02c8cdad0dce6666d4fb From 252a932538cfde3a232dad9e4bca34bee8fc35a7 Mon Sep 17 00:00:00 2001 From: parketh Date: Fri, 13 Sep 2024 16:01:54 +0100 Subject: [PATCH 10/10] chore: deploy new mainnet contracts --- scripts/mainnet.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/mainnet.sh b/scripts/mainnet.sh index b6c5528..d305062 100644 --- a/scripts/mainnet.sh +++ b/scripts/mainnet.sh @@ -30,6 +30,10 @@ starkli deploy --rpc $STARKNET_RPC $REPLICATING_SOLVER_CLASS $OWNER $ORACLE $VAU # Deployments ############################# +# 13 September 2024 +export REPLICATING_SOLVER=0x073cc79b07a02fe5dcd714903d62f9f3081e15aeb34e3725f44e495ecd88a5a1 +export REPLICATING_SOLVER_CLASS=0x030ba3f1857330dfe032ae7e43f51d4b206756ec4757fa3ac14e79ff79a98a44 + # 28 August 2024 export REPLICATING_SOLVER=0x07f2975ef3d288a031a842bdb50253d6255344356f9f4a02e54fbc147b007a13 export REPLICATING_SOLVER_CLASS=0x0589858bd41fc0c922ff5c656f3373f7072fb8a69fcd02c8cdad0dce6666d4fb