Skip to content

Commit

Permalink
Merge pull request #62 from SundaeSwap-finance/pi/self-destination
Browse files Browse the repository at this point in the history
Add a special 'Self' destination variant
  • Loading branch information
Quantumplation authored Mar 6, 2024
2 parents 3f95924 + e8bdada commit 618f457
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 77 deletions.
39 changes: 30 additions & 9 deletions lib/calculation/deposit.ak
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use aiken/math
use aiken/transaction.{NoDatum, Output}
use aiken/transaction.{NoDatum, Output, InlineDatum}
use aiken/transaction/credential.{Address, VerificationKeyCredential}
use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name}
use aiken/transaction/value.{ada_policy_id, ada_asset_name}
use calculation/shared.{PoolState} as calc_shared
use sundae/multisig
use shared.{SingletonValue}
use types/order.{Destination, OrderDatum}
use types/order.{Destination, Fixed, Self, OrderDatum}

/// Calculate the result of depositing some amount of tokens into the pool
///
Expand All @@ -14,13 +14,14 @@ use types/order.{Destination, OrderDatum}
///
pub fn do_deposit(
pool_state: PoolState,
input_value: Value,
input_utxo: Output,
assets: (SingletonValue, SingletonValue),
destination: Destination,
actual_protocol_fee: Int,
output: Output,
) -> PoolState {
let (asset_a, asset_b) = assets
let Output { value: input_value, .. } = input_utxo

// Policy ID and token name of the assets must match the pool, otherwise someone
// could load the pool with junk tokens and freeze the pool.
Expand Down Expand Up @@ -107,10 +108,24 @@ pub fn do_deposit(
)

// Make sure we're paying the result to the correct destination (both the address and the datum),
// with the correct amount
expect output.address == destination.address
expect output.datum == destination.datum
// with the correct amount; In the special case where Datum is "Self" (for example for a repeating strategy)
// use the input datum for validation
expect output.value == out_value
expect when destination is {
Fixed { address, datum } -> {
and {
output.address == address,
output.datum == datum
}
}
Self -> {
let Output { address: input_address, datum: input_datum, .. } = input_utxo
and {
output.address == input_address,
output.datum == input_datum
}
}
}

// And construct the final pool state
PoolState {
Expand Down Expand Up @@ -158,7 +173,7 @@ test deposit_test() {
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
max_protocol_fee: 2_500_000,
destination: Destination {
destination: Fixed {
address: addr,
datum: NoDatum,
},
Expand All @@ -174,7 +189,13 @@ test deposit_test() {
datum: NoDatum,
reference_script: None,
}
let final_pool_state = do_deposit(pool_state, input_value, assets, order.destination, 2_500_000, output)
let input = Output {
address: addr,
value: input_value,
datum: InlineDatum(order),
reference_script: None,
}
let final_pool_state = do_deposit(pool_state, input, assets, order.destination, 2_500_000, output)
expect final_pool_state.quantity_a.3rd == 1_010_000_000
expect final_pool_state.quantity_b.3rd == 1_010_000_000
True
Expand Down
29 changes: 23 additions & 6 deletions lib/calculation/donation.ak
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use aiken/transaction.{Output}
use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name}
use aiken/transaction/value.{ada_policy_id, ada_asset_name}
use calculation/shared.{PoolState} as calc_shared
use shared.{SingletonValue}
use types/order.{Destination}
use types/order.{Destination, Fixed, Self}

/// A donation describes an amount of assets to deposit into the pool, receiving nothing in return (except for the extra change on the UTXO).
/// Because every LP token holder has an entitlement to a percentage of the assets in the pool, the donation is distributed to all LP token holders
Expand All @@ -15,7 +15,7 @@ pub fn do_donation(
/// The pool state as a result of processing all orders before this one
pool_state: PoolState,
/// The total quantity of tokens on this particular input
input_value: Value,
input_utxo: Output,
/// The amounts of each asset being donated
assets: (SingletonValue, SingletonValue),
/// The destination (address + datum) that any *change* should be sent
Expand All @@ -27,6 +27,7 @@ pub fn do_donation(
/// If there is no change leftover, this output is ignored and used for the next order
output: Output,
) -> (PoolState, Bool) {
let Output { value: input_value, .. } = input_utxo
let ((asset_a_policy_id, asset_a_asset_name, asset_a_qty), (asset_b_policy_id, asset_b_asset_name, asset_b_qty)) = assets
// Make sure we're actually donating the pool assets; this is to prevent setting
// poolIdent to None, and then filling the pool UTXO with garbage tokens and eventually locking it
Expand All @@ -46,11 +47,27 @@ pub fn do_donation(
// is so that we can do a few expects without having to return a value right away
expect or {
!has_remainder,
// Make sure we're paying the result to the correct destination (both the address and the datum),
// with the correct amount; In the special case where Datum is "Self" (for example for a repeating strategy)
// use the input datum for validation
and {
output.address == destination.address,
output.datum == destination.datum,
output.value == remainder,
},
when destination is {
Fixed { address, datum } -> {
and {
output.address == address,
output.datum == datum
}
}
Self -> {
let Output { address: input_address, datum: input_datum, .. } = input_utxo
and {
output.address == input_address,
output.datum == input_datum
}
}
},
}
}

// Compute the new pool state, which is exactly the old pool state, plus the
Expand Down
20 changes: 12 additions & 8 deletions lib/calculation/process.ak
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use calculation/withdrawal
use calculation/strategy
use shared.{Ident, datum_of, pool_lp_name, is_script}
use sundae/multisig
use types/order.{Order, Destination, OrderDatum, SignedStrategyExecution}
use types/order.{Order, Destination, Fixed, OrderDatum, SignedStrategyExecution}
use types/pool.{PoolDatum}

/// Construct the initial pool state for processing a set of orders
Expand Down Expand Up @@ -119,6 +119,8 @@ pub fn process_order(
tx_valid_range: ValidityRange,
// The transaction withdrawals, so we can check strategy executions
withdrawals: Dict<StakeCredential, Int>,
// The input being processed
input: Output,
// The value attached to the input, so we can ensure that any surplus tokens are paid out to the destination along with the results of the order; this lets transactions chain
value: Value,
// The details of the order to execute, such as whether it's a swap, the limit, etc.
Expand Down Expand Up @@ -155,6 +157,7 @@ pub fn process_order(
output_reference,
tx_valid_range,
withdrawals,
input,
value,
details,
max_protocol_fee,
Expand All @@ -177,7 +180,7 @@ pub fn process_order(
let next =
swap.do_swap(
initial,
value,
input,
destination,
fees_per_10_thousand,
fee,
Expand All @@ -194,7 +197,7 @@ pub fn process_order(
let fee = amortized_base_fee + simple_fee
expect max_protocol_fee >= fee
// Calculate and validate the result of a deposit
let next = deposit.do_deposit(initial, value, assets, destination, fee, output)
let next = deposit.do_deposit(initial, input, assets, destination, fee, output)
(next, rest_outputs)
}
order.Withdrawal(amount) -> {
Expand All @@ -205,7 +208,7 @@ pub fn process_order(
expect max_protocol_fee >= fee
// Calculate and validate the result of a withdrawal
let next =
withdrawal.do_withdrawal(initial, value, amount, destination, fee, output)
withdrawal.do_withdrawal(initial, input, amount, destination, fee, output)
(next, rest_outputs)
}
// NOTE: we decided not to implement zap, for time constraints, and because a zap can be easily implemented as a chained order, as it is in V1
Expand All @@ -219,7 +222,7 @@ pub fn process_order(
expect max_protocol_fee >= fee
// Calculate and validate the result of a donation
let (next, used_output) =
donation.do_donation(initial, value, assets, destination, fee, output)
donation.do_donation(initial, input, assets, destination, fee, output)
// If a donation has no change (ex it has exactly the donation + the fee) then we don't have an output dedicated to the order
// so we can skip over it
// TODO: can we just return used_output here instead of passing around the lists of outputs?
Expand All @@ -239,7 +242,7 @@ pub fn process_orders(
this_pool_ident: Ident,
// The transaction valid range, if we end up processing a strategy
tx_valid_range: ValidityRange,
// THe withdrawals attached to the transaction, for validating strategies
// The withdrawals attached to the transaction, for validating strategies
withdrawals: Dict<StakeCredential, Int>,
// The datums in the witness set, in case we need to lookup a non-inline datum
datums: Dict<Hash<Blake2b_256, Data>, Data>,
Expand Down Expand Up @@ -326,6 +329,7 @@ pub fn process_orders(
output_reference,
tx_valid_range,
withdrawals,
order,
order.value,
details,
max_protocol_fee,
Expand Down Expand Up @@ -383,7 +387,7 @@ test process_orders_test() {
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
max_protocol_fee: 2_500_000,
destination: Destination {
destination: Fixed {
address: addr,
datum: NoDatum,
},
Expand Down Expand Up @@ -464,7 +468,7 @@ test process_30_shuffled_orders_test() {
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
max_protocol_fee: 2_500_000,
destination: Destination {
destination: Fixed {
address: addr,
datum: NoDatum,
},
Expand Down
47 changes: 36 additions & 11 deletions lib/calculation/swap.ak
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use aiken/transaction.{NoDatum, Output}
use aiken/transaction.{NoDatum, InlineDatum, Output}
use aiken/transaction/credential.{Address, VerificationKeyCredential}
use aiken/transaction/value.{
AssetName, PolicyId, Value, ada_asset_name, ada_policy_id,
}
use calculation/shared.{PoolState} as calc_shared
use shared.{SingletonValue}
use sundae/multisig
use types/order.{Destination, OrderDatum}
use types/order.{Destination, Fixed, Self, OrderDatum}

/// Compute the amount of token a swap yields; returns the amount of token received, and the remainder to be sent to the user
///
Expand Down Expand Up @@ -59,8 +59,8 @@ pub fn swap_takes(
/// Calculate the new pool state after performing a swap, and validate that the output is correct according to the order
pub fn do_swap(
pool_state: PoolState,
/// The value coming in from the UTXO with the order
input_value: Value,
/// The full UTXO for this swap
input_utxo: Output,
/// Where the results of the swap need to be paid
destination: Destination,
/// The liquidity provider fee to charge
Expand All @@ -73,6 +73,7 @@ pub fn do_swap(
/// The output to compare against
output: Output,
) -> PoolState {
let Output { value: input_value, .. } = input_utxo
// Destructure the pool state
// TODO: it'd make the code nightmarish, but things would be way more efficient to just always pass these values destructured as parameters...
let (offer_policy_id, offer_asset_name, offer_amt) = offer
Expand All @@ -84,6 +85,25 @@ pub fn do_swap(
let (a_policy_id, a_asset_name, a_amt) = quantity_a
let (b_policy_id, b_asset_name, b_amt) = quantity_b

// Make sure we're paying the result to the correct destination (both the address and the datum),
// with the correct amount; In the special case where Datum is "Self" (for example for a repeating strategy)
// use the input datum for validation
expect when destination is {
Fixed { address, datum } -> {
and {
output.address == address,
output.datum == datum
}
}
Self -> {
let Output { address: input_address, datum: input_datum, .. } = input_utxo
and {
output.address == input_address,
output.datum == input_datum
}
}
}

// There are two symmetric cases, depending on whether you're giving order A or order B
// If there's a clever way to write this as one branch it might be clearer to read
if offer_policy_id == a_policy_id && offer_asset_name == a_asset_name {
Expand All @@ -106,10 +126,9 @@ pub fn do_swap(
input_value,
)

// Check that the output has the correct destination and value
expect output.address == destination.address
expect output.datum == destination.datum
// Make sure the correct value (including change) carries through to the output
expect output.value == out_value

// And check that the min_received (the lowest amount of tokens the user is willing to receive, aka a limit price)
// is satisfied
expect takes >= min_received.3rd
Expand Down Expand Up @@ -137,9 +156,9 @@ pub fn do_swap(
input_value,
)

expect output.address == destination.address
expect output.datum == destination.datum
// Make sure the correct value (including change) carries through to the output
expect output.value == out_value

// Check that mintakes is satisfied
expect takes >= min_received.3rd
PoolState {
Expand Down Expand Up @@ -179,7 +198,7 @@ test swap_mintakes_too_high() fail {
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
max_protocol_fee: 2_500_000,
destination: Destination {
destination: Fixed {
address: addr,
datum: NoDatum,
},
Expand All @@ -193,7 +212,13 @@ test swap_mintakes_too_high() fail {
datum: NoDatum,
reference_script: None,
}
let final_pool_state = do_swap(pool_state, input_value, order.destination, 5, 2_500_000, swap_offer, swap_min_received, output)
let input = Output {
address: addr,
value: input_value,
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)
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
Expand Down
Loading

0 comments on commit 618f457

Please sign in to comment.