Skip to content

Commit

Permalink
Add a special 'Self' destination variant
Browse files Browse the repository at this point in the history
We want to allow recurring automated strategies that can execute in
multiple stages.

The idea behind this was to allow setting the destination back to the
order script with a "strategy" order details.

However, this overlooks the fact that this would require setting a
destination datum hash. This means that our order executions would have
a finite depth.

So, this commit adds a new variant to the Destination, which enforces
that the results of the swap are paid back to the *same address, with
the same datum*.

Thus, we could construct the order datum:
```
OrderDatum {
  pool_ident: ...,
  owner: ...,
  max_protocol_fee: ...,
  destination: Self,
  details: Strategy { ... },
  extension: ...
}
```

and the output order datum will be... the same thing.

Some notes:
 - We make sure it's the *second* variant, which should mean that it
   doesn't change the offchain code
 - This doesn't make a whole lot of sense for any other variant; should
   we only allow it for strategies? Unless the auditors come up with a
   problem, I don't see a reason to restrict it.
  • Loading branch information
Quantumplation committed Feb 11, 2024
1 parent e6f282f commit 7a92750
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 49 deletions.
38 changes: 29 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 { address: input_address, datum: input_datum, 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,23 @@ 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 -> {
and {
output.address == input_address,
output.datum == input_datum
}
}
}

// And construct the final pool state
PoolState {
Expand Down Expand Up @@ -158,7 +172,7 @@ test deposit_test() {
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
max_protocol_fee: 2_500_000,
destination: Destination {
destination: Fixed {
address: addr,
datum: NoDatum,
},
Expand All @@ -174,7 +188,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
37 changes: 28 additions & 9 deletions lib/calculation/donation.ak
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use aiken/transaction.{NoDatum, Output}
use aiken/transaction.{NoDatum, InlineDatum, Output}
use aiken/transaction/credential.{Address, VerificationKeyCredential}
use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name}
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}

/// 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 @@ -17,7 +17,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 @@ -29,6 +29,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, address: input_address, datum: input_datum, .. } = input_utxo
// 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
expect assets.1st.1st == pool_state.quantity_a.1st
Expand All @@ -48,10 +49,22 @@ 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,
and {
output.address == destination.address,
output.datum == destination.datum,
output.value == 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
when destination is {
Fixed { address, datum } -> {
and {
output.address == address,
output.datum == datum
}
}
Self -> {
and {
output.address == input_address,
output.datum == input_datum
}
}
},
}

Expand Down Expand Up @@ -109,7 +122,7 @@ test donation() {
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
max_protocol_fee: 2_500_000,
destination: Destination { address: addr, datum: NoDatum },
destination: Fixed { address: addr, datum: NoDatum },
details: order.Donation {
assets: assets,
},
Expand All @@ -123,8 +136,14 @@ test donation() {
datum: NoDatum,
reference_script: None,
}
let input = Output {
address: addr,
value: input_value,
datum: InlineDatum(order),
reference_script: None,
}
let (final_pool_state, has_remainder) =
do_donation(pool_state, input_value, assets, order.destination, 2_500_000, output)
do_donation(pool_state, input, assets, order.destination, 2_500_000, output)
expect !has_remainder
expect final_pool_state.quantity_a.3rd == 1_001_000_000
expect final_pool_state.quantity_b.3rd == 1_001_000_000
Expand Down
44 changes: 34 additions & 10 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 @@ -60,7 +60,7 @@ pub fn swap_takes(
pub fn do_swap(
pool_state: PoolState,
/// The value coming in from the UTXO with the order
input_value: Value,
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 { address: input_address, datum: input_datum, 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,24 @@ 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 -> {
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 +125,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 +155,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 +197,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 +211,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
40 changes: 30 additions & 10 deletions lib/calculation/withdrawal.ak
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
use aiken/option
use aiken/math
use aiken/transaction.{NoDatum, Output}
use aiken/transaction.{NoDatum, InlineDatum, Output}
use aiken/transaction/credential.{Address, VerificationKeyCredential}
use aiken/transaction/value.{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}

/// Execute a withdrawal order
///
/// This burns LP tokens, and pays out a portion of the pool in proportion to the percentage
/// of the circulating supply of LP tokens that was burned.
pub fn do_withdrawal(
pool_state: PoolState, // The interim pool state against which we should calculate the withdrawal
input_value: Value, // The incoming value; may have additional assets on it, for example if executing as part of a larger chain
input_utxo: Output, // The incoming UTXO, useful for returning surplus and handling Self destinations
amount: SingletonValue, // The amount to actually withdraw
destination: Destination, // The destination that the LP tokens (and change) should be sent to
actual_protocol_fee: Int, // The protocol fee to deduct
output: Output, // The output to compare the order execution against, to ensure the right quantity was paid out
) -> PoolState {
let (lp_policy, lp_asset_name, amount) = amount
let Output { address: input_address, datum: input_datum, value: input_value, .. } = input_utxo

// Make sure we're withdrawing from the right pool;
// if we were to provide LP tokens for a different pool, you could withdraw funds that weren't yours
Expand Down Expand Up @@ -61,12 +62,25 @@ pub fn do_withdrawal(
withdrawn_b,
)

// Check that the result is paid to the destination with the full value
// Like on other order types, the datum here lets us compose with other protocols
// or do chained orders within Sundae
expect output.address == destination.address
expect output.datum == destination.datum

// 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 output.value == remainder
expect when destination is {
Fixed { address, datum } -> {
and {
output.address == address,
output.datum == datum
}
}
Self -> {
and {
output.address == input_address,
output.datum == input_datum
}
}
}

// Return the final pool state
PoolState {
Expand Down Expand Up @@ -153,7 +167,7 @@ fn withdrawal_test(options: WithdrawalTestOptions) {
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
max_protocol_fee: 2_500_000,
destination: Destination {
destination: Fixed {
address: addr,
datum: NoDatum,
},
Expand All @@ -172,7 +186,13 @@ fn withdrawal_test(options: WithdrawalTestOptions) {
datum: NoDatum,
reference_script: None,
}
let final_pool_state = do_withdrawal(pool_state, input_value, amount, 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_withdrawal(pool_state, input, amount, order.destination, 2_500_000, output)
expect final_pool_state.quantity_a.3rd == 1_000_000_000
expect final_pool_state.quantity_b.3rd == 1_000_000_000
True
Expand Down
14 changes: 10 additions & 4 deletions lib/tests/aiken/deposit.ak
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use sundae/multisig
use aiken/math
use aiken/transaction.{NoDatum, Output}
use aiken/transaction.{NoDatum, InlineDatum, Output}
use aiken/transaction/value
use aiken/transaction/credential.{Address, VerificationKeyCredential}
use calculation/shared.{PoolState}
use calculation/deposit.{do_deposit}
use types/order.{OrderDatum, Destination}
use types/order.{OrderDatum, Destination, Fixed}

test deposit_test() {
let lp = (#"99999999999999999999999999999999999999999999999999999999", "LP")
Expand Down Expand Up @@ -48,7 +48,7 @@ fn deposit_test_schema(qa: Int, qb: Int, has_a: Int, has_b: Int, gives_a: Int, g
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
),
max_protocol_fee: 2_500_000,
destination: Destination {
destination: Fixed {
address: addr,
datum: NoDatum,
},
Expand All @@ -63,9 +63,15 @@ fn deposit_test_schema(qa: Int, qb: Int, has_a: Int, has_b: Int, gives_a: Int, g
datum: NoDatum,
reference_script: None,
}
let input = Output {
address: addr,
value: input_value,
datum: InlineDatum(order),
reference_script: None,
}
let PoolState{..} = do_deposit(
pool_state,
input_value,
input,
assets,
order.destination,
2_500_000,
Expand Down
Loading

0 comments on commit 7a92750

Please sign in to comment.