diff --git a/lib/calculation/deposit.ak b/lib/calculation/deposit.ak index c429191..3de1c82 100644 --- a/lib/calculation/deposit.ak +++ b/lib/calculation/deposit.ak @@ -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 /// @@ -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. @@ -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 { @@ -158,7 +172,7 @@ test deposit_test() { #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", ), max_protocol_fee: 2_500_000, - destination: Destination { + destination: Fixed { address: addr, datum: NoDatum, }, @@ -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 diff --git a/lib/calculation/donation.ak b/lib/calculation/donation.ak index a1d7b4e..357ba2c 100644 --- a/lib/calculation/donation.ak +++ b/lib/calculation/donation.ak @@ -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 @@ -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 @@ -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 @@ -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 + } + } }, } @@ -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, }, @@ -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 diff --git a/lib/calculation/swap.ak b/lib/calculation/swap.ak index ed6fdde..f912649 100644 --- a/lib/calculation/swap.ak +++ b/lib/calculation/swap.ak @@ -1,4 +1,4 @@ -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, @@ -6,7 +6,7 @@ use aiken/transaction/value.{ 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 /// @@ -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 @@ -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 @@ -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 { @@ -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 @@ -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 { @@ -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, }, @@ -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 diff --git a/lib/calculation/withdrawal.ak b/lib/calculation/withdrawal.ak index 8a9cc4d..a50e204 100644 --- a/lib/calculation/withdrawal.ak +++ b/lib/calculation/withdrawal.ak @@ -1,12 +1,12 @@ 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 /// @@ -14,13 +14,14 @@ use types/order.{Destination, OrderDatum} /// 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 @@ -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 { @@ -153,7 +167,7 @@ fn withdrawal_test(options: WithdrawalTestOptions) { #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", ), max_protocol_fee: 2_500_000, - destination: Destination { + destination: Fixed { address: addr, datum: NoDatum, }, @@ -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 diff --git a/lib/tests/aiken/deposit.ak b/lib/tests/aiken/deposit.ak index 830e797..3285e3d 100644 --- a/lib/tests/aiken/deposit.ak +++ b/lib/tests/aiken/deposit.ak @@ -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") @@ -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, }, @@ -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, diff --git a/lib/tests/examples/ex_order.ak b/lib/tests/examples/ex_order.ak index d3526e8..562d8b2 100644 --- a/lib/tests/examples/ex_order.ak +++ b/lib/tests/examples/ex_order.ak @@ -1,12 +1,12 @@ use aiken/cbor -use types/order.{Destination, OrderDatum, Swap, Deposit, Withdrawal, Scoop, Cancel} +use types/order.{Destination, Fixed, OrderDatum, Swap, Deposit, Withdrawal, Scoop, Cancel} use aiken/transaction.{NoDatum} use sundae/multisig use tests/examples/ex_shared.{print_example, wallet_address} fn mk_swap() -> OrderDatum { let addr = wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513") - let dest = Destination { address: addr, datum: NoDatum } + let dest = Fixed { address: addr, datum: NoDatum } let swap = Swap( (#"", #"", 10000000), diff --git a/lib/types/order.ak b/lib/types/order.ak index b2b9d3e..c4ed945 100644 --- a/lib/types/order.ak +++ b/lib/types/order.ak @@ -32,11 +32,16 @@ pub type OrderDatum { /// The destination where funds are to be sent after the order pub type Destination { - /// The address to attach to the output (such as the users wallet, or another script) - address: Address, - /// The datum to attach to the output; Could be none in typical cases, but could also be used to compose with other protocols, - /// such as paying the results of a swap back to a treasury contract - datum: Datum, + Fixed { + /// The address to attach to the output (such as the users wallet, or another script) + address: Address, + /// The datum to attach to the output; Could be none in typical cases, but could also be used to compose with other protocols, + /// such as paying the results of a swap back to a treasury contract + datum: Datum, + } + /// The result gets paid back to the exact same address and datum it came from; useful for strategies + /// with unbounded legs + Self } /// There are two ways to delegate authorization of a strategy::