Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactors pool fees slightly #71

Merged
merged 2 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions aiken.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ source = "github"

[[requirements]]
name = "SundaeSwap-finance/aicone"
version = "ae0852d40cc6332437492102451cf331a3c10b0d"
version = "faca3e33f1cc7183e2f3801ee56b705883c6832e"
source = "github"

[[requirements]]
Expand All @@ -24,7 +24,7 @@ source = "github"

[[packages]]
name = "SundaeSwap-finance/aicone"
version = "ae0852d40cc6332437492102451cf331a3c10b0d"
version = "faca3e33f1cc7183e2f3801ee56b705883c6832e"
requirements = []
source = "github"

Expand Down
2 changes: 1 addition & 1 deletion aiken.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ source = "github"

[[dependencies]]
name = "SundaeSwap-finance/aicone"
version = "ae0852d40cc6332437492102451cf331a3c10b0d"
version = "faca3e33f1cc7183e2f3801ee56b705883c6832e"
source = "github"

[[dependencies]]
Expand Down
82 changes: 50 additions & 32 deletions lib/calculation/process.ak
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,49 @@ use sundae/multisig
use types/order.{Order, Destination, Fixed, OrderDatum, SignedStrategyExecution}
use types/pool.{PoolDatum}

pub fn calculate_fees(
fees: (Int, Int),
market_open: PosixTime,
fee_finalized: PosixTime,
valid_from: PosixTime,
) {
Quantumplation marked this conversation as resolved.
Show resolved Hide resolved
// So, if the transaction is guaranteed to run after fee_finalized, charge the final fee rate
if valid_from > fee_finalized {
fees.2nd
} else {
// Otherwise, calculate the duration of this decay period
let duration = fee_finalized - market_open
if duration == 0 {
// If it's zero (market_open == fee_finalized) then just use the final fee rate
fees.2nd
} else {
// Otherwise, calculate how much has elapsed since the market open
// then the full range of the fee rate
// then, starting from the opening fee rate, linearly increase (or decrease) the fees by that range
let elapsed = valid_from - market_open
let range = fees.2nd - fees.1st
fees.1st + (elapsed * range / duration)
}
}
}

/// Construct the initial pool state for processing a set of orders
pub fn pool_input_to_state(
pool_token_policy: PolicyId,
datum: PoolDatum,
input: Output,
valid_from: IntervalBound<PosixTime>,
) -> (PoolState, Int, Int) {
) -> (PoolState, Int, Int, Int) {
let PoolDatum {
assets,
protocol_fees,
identifier,
circulating_lp,
fees_per_10_thousand,
bid_fees_per_10_thousand,
ask_fees_per_10_thousand,
market_open,
fee_finalized,
..
} = datum
let (asset_a, asset_b) = assets
let (asset_a_policy_id, asset_a_name) = asset_a
Expand All @@ -61,25 +89,9 @@ pub fn pool_input_to_state(
}
// Calculate the fees per 10k rate to use for this whole scoop
// We let the creator of the pool specify a fee rate that decays (or increases) between market_open and fee_finalized
let fees =
// So, if the transaction is guaranteed to run after fee_finalized, charge the final fee rate
if valid_from > fee_finalized {
fees_per_10_thousand.2nd
} else {
// Otherwise, calculate the duration of this decay period
let duration = fee_finalized - market_open
if duration == 0 {
// If it's zero (market_open == fee_finalized) then just use the final fee rate
fees_per_10_thousand.2nd
} else {
// Otherwise, calculate how much has elapsed since the market open
// then the full range of the fee rate
// then, starting from the opening fee rate, linearly increase (or decrease) the fees by that range
let elapsed = valid_from - market_open
let range = fees_per_10_thousand.2nd - fees_per_10_thousand.1st
fees_per_10_thousand.1st + (elapsed * range / duration)
}
}
let bid_fees = calculate_fees(bid_fees_per_10_thousand, market_open, fee_finalized, valid_from)
let ask_fees = calculate_fees(ask_fees_per_10_thousand, market_open, fee_finalized, valid_from)

// Then construct the pool state. We include the assets here, instead of just the reserves, so we can check the values of each order
// TODO: we could potentially save quite a bit by not passing around this object, and passing around a lot of parameters instead...
(PoolState {
Expand All @@ -94,7 +106,7 @@ pub fn pool_input_to_state(
value.quantity_of(input.value, asset_b_policy_id, asset_b_name),
),
quantity_lp: (pool_token_policy, pool_lp_name(identifier), circulating_lp),
}, fees, protocol_fees)
}, bid_fees, ask_fees, protocol_fees)
}

/// If the order is restricted to a specific pool, then make sure pool_ident matches that; otherwise just return true
Expand Down Expand Up @@ -129,8 +141,9 @@ pub fn process_order(
max_protocol_fee: Int,
// The destination where the result of the order must be sent; useful for chaining transactions, as it lets you specify a datum for the output
destination: Destination,
// The liquidity provider fee to charge, expressed as parts per 10,000. also called basis points
fees_per_10_thousand: Int,
// The liquidity provider fee to charge for bid (A -> B) and ask (B -> A) orders, expressed as parts per 10,000. also called basis points
bid_fees_per_10_thousand: Int,
ask_fees_per_10_thousand: Int,
// The base fee, divided among all the participants in the scoop
amortized_base_fee: Int,
// The amount to charge for simple vs strategy orders, taken from the settings
Expand Down Expand Up @@ -162,7 +175,8 @@ pub fn process_order(
details,
max_protocol_fee,
destination,
fees_per_10_thousand,
bid_fees_per_10_thousand,
ask_fees_per_10_thousand,
amortized_base_fee,
// We pass strategy_fee here, instead of simple_fee,
// because even though we're using the same logic, we're charging a different fee for strategies
Expand All @@ -182,7 +196,8 @@ pub fn process_order(
initial,
input,
destination,
fees_per_10_thousand,
bid_fees_per_10_thousand,
ask_fees_per_10_thousand,
fee,
offer,
min_received,
Expand Down Expand Up @@ -250,8 +265,9 @@ pub fn process_orders(
initial: PoolState,
// The list of remaining indices into the inputs, specifying which orders to process
input_order: List<(Int, Option<SignedStrategyExecution>, Int)>,
// The liquidity provider fee, expressed as parts per 10,000 (also known as 'basis points'); this allows us to save some math over storing the full ratio like in V1
fees_per_10_thousand: Int,
// The liquidity provider fee, for bid (swapping asset A to asset B) and ask (asset B to asset A) orders, expressed as parts per 10,000
bid_fees_per_10_thousand: Int,
ask_fees_per_10_thousand: Int,
// The protocol base fee, split across each order
amortized_base_fee: Int,
// The simple and strategy fees from the settings datum
Expand Down Expand Up @@ -334,7 +350,8 @@ pub fn process_orders(
details,
max_protocol_fee,
destination,
fees_per_10_thousand,
bid_fees_per_10_thousand,
ask_fees_per_10_thousand,
amortized_base_fee,
simple_fee,
strategy_fee,
Expand All @@ -349,7 +366,8 @@ pub fn process_orders(
datums,
next_state,
rest, // This advances to the next element from input_order
fees_per_10_thousand,
bid_fees_per_10_thousand,
ask_fees_per_10_thousand,
amortized_base_fee,
simple_fee,
strategy_fee,
Expand Down Expand Up @@ -437,7 +455,7 @@ test process_orders_test() {
let inputs = [input]
let outputs = [output]

let (final_pool_state, simple, strategies) = process_orders(#"", valid_range, dict.new(), datums, pool_state, input_order, 5, 2_500_000, 0, 0, 0, inputs, inputs, outputs, 0, 0, 0)
let (final_pool_state, simple, strategies) = process_orders(#"", valid_range, dict.new(), datums, pool_state, input_order, 5, 5, 2_500_000, 0, 0, 0, inputs, inputs, outputs, 0, 0, 0)

expect final_pool_state.quantity_a.3rd == 1_001_000_000
expect final_pool_state.quantity_b.3rd == 1_001_000_000
Expand Down Expand Up @@ -550,7 +568,7 @@ test process_30_shuffled_orders_test() {
]
let outputs = [output]

let (final_pool_state, simple, strategies) = process_orders(#"", valid_range, dict.new(), datums, pool_state, input_order, 5, 2_500_000, 0, 0, 0, inputs, inputs, outputs, 0, 0, 0)
let (final_pool_state, simple, strategies) = process_orders(#"", valid_range, dict.new(), datums, pool_state, input_order, 5, 5, 2_500_000, 0, 0, 0, inputs, inputs, outputs, 0, 0, 0)

final_pool_state.quantity_a.3rd == 1_030_000_000 &&
final_pool_state.quantity_b.3rd == 1_030_000_000 &&
Expand Down
10 changes: 6 additions & 4 deletions lib/calculation/swap.ak
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub fn swap_takes(
// The quantity of the token that should be given to the user that the pool already has
pool_take: Int,
// The percentage fee collected on behalf of the liquidity providers, represented as the numerator of a fraction over 10,000 (commonly referred to as basis points)
// already adjusted for bid or ask
fees_per_10_thousand: Int,
// The ADA fee to deduct as part of the protocol fee
actual_protocol_fee: Int,
Expand Down Expand Up @@ -61,8 +62,9 @@ pub fn do_swap(
input_utxo: Output,
/// Where the results of the swap need to be paid
destination: Destination,
/// The liquidity provider fee to charge
fees_per_10_thousand: Int,
/// The liquidity provider fee to charge for bid (A -> B) or ask (B -> A) orders
bid_fees_per_10_thousand: Int,
ask_fees_per_10_thousand: Int,
actual_protocol_fee: Int,
/// The amount of value from the UTXO that is "on offer" for the trade
offer: SingletonValue,
Expand Down Expand Up @@ -118,7 +120,7 @@ pub fn do_swap(
b_asset_name,
a_amt,
b_amt,
fees_per_10_thousand,
bid_fees_per_10_thousand,
actual_protocol_fee,
offer_amt,
input_value,
Expand Down Expand Up @@ -148,7 +150,7 @@ pub fn do_swap(
a_asset_name,
b_amt,
a_amt,
fees_per_10_thousand,
ask_fees_per_10_thousand,
actual_protocol_fee,
offer_amt,
input_value,
Expand Down
55 changes: 54 additions & 1 deletion lib/tests/aiken/swap.ak
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,61 @@ test swap_mintakes_too_high() fail {
datum: InlineDatum(order),
reference_script: None,
}
let final_pool_state = do_swap(pool_state, input, order.destination, 5, 2_500_000, swap_offer, swap_min_received, output)
let final_pool_state = do_swap(pool_state, input, order.destination, 5, 5, 2_500_000, swap_offer, swap_min_received, output)
expect final_pool_state.quantity_a.3rd == 1_000_000_000 + 10_000_000
expect final_pool_state.quantity_b.3rd == 1_000_000_000 - 9_896_088
True
}

test swap_different_bid_ask() {
let addr =
Address(
VerificationKeyCredential(
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
None,
)
let ada = (#"", #"")
let rberry = (#"01010101010101010101010101010101010101010101010101010101", "RBERRY")
let lp = (#"99999999999999999999999999999999999999999999999999999999", "LP")
let pool_state = PoolState {
quantity_a: (#"", #"", 1_000_000_000),
quantity_b: (rberry.1st, rberry.2nd, 1_000_000_000),
quantity_lp: (lp.1st, lp.2nd, 1_000_000_000),
}
let input_value =
value.from_lovelace(4_500_000) |> value.add(rberry.1st, rberry.2nd, 10_000_000)
let swap_offer = (rberry.1st, rberry.2nd, 10_000_000)
let swap_min_received = (ada.1st, ada.2nd, 0)
let order = OrderDatum {
pool_ident: None,
owner: multisig.Signature(
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
max_protocol_fee: 2_500_000,
destination: Fixed {
address: addr,
datum: NoDatum,
},
details: order.Swap { offer: swap_offer, min_received: swap_min_received, },
extension: Void,
}
let output = Output {
address: addr,
value: value.from_lovelace(11_802_950),
datum: NoDatum,
reference_script: None,
}
let input = Output {
address: addr,
value: input_value,
datum: InlineDatum(order),
reference_script: None,
}
trace @"Test"
let final_pool_state = do_swap(pool_state, input, order.destination, 5, 100, 2_500_000, swap_offer, swap_min_received, output)
// If we charged 5%, from the bid fee, it would be `- 9_896_088`, but because we charged a higher fee, the user got less in return
expect final_pool_state.quantity_a.3rd == 1_000_000_000 - 9_802_950
expect final_pool_state.quantity_b.3rd == 1_000_000_000 + 10_000_000
True
}
4 changes: 3 additions & 1 deletion lib/tests/examples/ex_pool.ak
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ fn mk_pool_datum() -> PoolDatum {
),
),
circulating_lp: 20229488080013,
fees_per_10_thousand: (2000, 500),
bid_fees_per_10_thousand: (2000, 500),
ask_fees_per_10_thousand: (2000, 500),
fee_manager: None,
market_open: 100,
fee_finalized: 1000,
protocol_fees: 10000000,
Expand Down
9 changes: 7 additions & 2 deletions lib/types/pool.ak
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use aiken/time.{PosixTime}
use shared.{AssetClass, Ident}
use types/order.{SignedStrategyExecution}
use sundae/multisig

/// The current state of a AMM liquidity pool at a UTXO.
pub type PoolDatum {
Expand All @@ -15,12 +16,15 @@ pub type PoolDatum {
/// - circulating_lp is always equal to the number of LP tokens that have been minted and are in circulation
/// - A users LP tokens (or burned LP tokens), as a percentage of the circulating LP tokens, represent the percentage of assets they just deposited or withdrew.
circulating_lp: Int,
/// The basis points to charge on each trade
/// The basis points to charge on each trade for bid (A -> B) and ask (B -> A) orders
/// For example, a 1% fee would be represented as 100 (out of 10,000), and a 0.3% fee would be represented as 30
/// The two values represent the fees as of `market_open` and as of `fee_finalized`, respectively, with a linear
/// decay from one to the other.
/// The transaction uses the valid_from field to charge the largest fee the transaction *could* be obligated to pay
fees_per_10_thousand: (Int, Int),
bid_fees_per_10_thousand: (Int, Int),
ask_fees_per_10_thousand: (Int, Int),
// An optional multisig condition under which the protocol fees can be updated
fee_manager: Option<multisig.MultisigScript>,
/// The UNIX millisecond timestamp at which trading against the pool should be allowed
/// TODO: deposits and arguably withdrawals should be processed before the market open
market_open: PosixTime,
Expand Down Expand Up @@ -64,6 +68,7 @@ pub type PoolRedeemer {
/// and it is safe to do so because that output must be to the treasury address from the settings datum
treasury_output: Int,
}
UpdatePoolFees
}

/// We use the pool mint script for two different purposes
Expand Down
1 change: 1 addition & 0 deletions validators/order.ak
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ validator(stake_script_hash: Hash<Blake2b_224, Script>) {
datum.owner,
ctx.transaction.extra_signatories,
ctx.transaction.validity_range,
ctx.transaction.withdrawals,
)
}
Scoop -> {
Expand Down
Loading
Loading