Skip to content

Commit 7a92750

Browse files
Add a special 'Self' destination variant
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.
1 parent e6f282f commit 7a92750

File tree

7 files changed

+143
-49
lines changed

7 files changed

+143
-49
lines changed

lib/calculation/deposit.ak

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use aiken/math
2-
use aiken/transaction.{NoDatum, Output}
2+
use aiken/transaction.{NoDatum, Output, InlineDatum}
33
use aiken/transaction/credential.{Address, VerificationKeyCredential}
4-
use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name}
4+
use aiken/transaction/value.{ada_policy_id, ada_asset_name}
55
use calculation/shared.{PoolState} as calc_shared
66
use sundae/multisig
77
use shared.{SingletonValue}
8-
use types/order.{Destination, OrderDatum}
8+
use types/order.{Destination, Fixed, Self, OrderDatum}
99

1010
/// Calculate the result of depositing some amount of tokens into the pool
1111
///
@@ -14,13 +14,14 @@ use types/order.{Destination, OrderDatum}
1414
///
1515
pub fn do_deposit(
1616
pool_state: PoolState,
17-
input_value: Value,
17+
input_utxo: Output,
1818
assets: (SingletonValue, SingletonValue),
1919
destination: Destination,
2020
actual_protocol_fee: Int,
2121
output: Output,
2222
) -> PoolState {
2323
let (asset_a, asset_b) = assets
24+
let Output { address: input_address, datum: input_datum, value: input_value, .. } = input_utxo
2425

2526
// Policy ID and token name of the assets must match the pool, otherwise someone
2627
// could load the pool with junk tokens and freeze the pool.
@@ -107,10 +108,23 @@ pub fn do_deposit(
107108
)
108109

109110
// Make sure we're paying the result to the correct destination (both the address and the datum),
110-
// with the correct amount
111-
expect output.address == destination.address
112-
expect output.datum == destination.datum
111+
// with the correct amount; In the special case where Datum is "Self" (for example for a repeating strategy)
112+
// use the input datum for validation
113113
expect output.value == out_value
114+
expect when destination is {
115+
Fixed { address, datum } -> {
116+
and {
117+
output.address == address,
118+
output.datum == datum
119+
}
120+
}
121+
Self -> {
122+
and {
123+
output.address == input_address,
124+
output.datum == input_datum
125+
}
126+
}
127+
}
114128

115129
// And construct the final pool state
116130
PoolState {
@@ -158,7 +172,7 @@ test deposit_test() {
158172
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
159173
),
160174
max_protocol_fee: 2_500_000,
161-
destination: Destination {
175+
destination: Fixed {
162176
address: addr,
163177
datum: NoDatum,
164178
},
@@ -174,7 +188,13 @@ test deposit_test() {
174188
datum: NoDatum,
175189
reference_script: None,
176190
}
177-
let final_pool_state = do_deposit(pool_state, input_value, assets, order.destination, 2_500_000, output)
191+
let input = Output {
192+
address: addr,
193+
value: input_value,
194+
datum: InlineDatum(order),
195+
reference_script: None,
196+
}
197+
let final_pool_state = do_deposit(pool_state, input, assets, order.destination, 2_500_000, output)
178198
expect final_pool_state.quantity_a.3rd == 1_010_000_000
179199
expect final_pool_state.quantity_b.3rd == 1_010_000_000
180200
True

lib/calculation/donation.ak

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use aiken/transaction.{NoDatum, Output}
1+
use aiken/transaction.{NoDatum, InlineDatum, Output}
22
use aiken/transaction/credential.{Address, VerificationKeyCredential}
33
use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name}
44
use calculation/shared.{PoolState} as calc_shared
55
use shared.{SingletonValue}
66
use sundae/multisig
7-
use types/order.{Destination, OrderDatum}
7+
use types/order.{Destination, Fixed, Self, OrderDatum}
88

99
/// A donation describes an amount of assets to deposit into the pool, receiving nothing in return (except for the extra change on the UTXO).
1010
/// 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(
1717
/// The pool state as a result of processing all orders before this one
1818
pool_state: PoolState,
1919
/// The total quantity of tokens on this particular input
20-
input_value: Value,
20+
input_utxo: Output,
2121
/// The amounts of each asset being donated
2222
assets: (SingletonValue, SingletonValue),
2323
/// The destination (address + datum) that any *change* should be sent
@@ -29,6 +29,7 @@ pub fn do_donation(
2929
/// If there is no change leftover, this output is ignored and used for the next order
3030
output: Output,
3131
) -> (PoolState, Bool) {
32+
let Output { value: input_value, address: input_address, datum: input_datum, .. } = input_utxo
3233
// Make sure we're actually donating the pool assets; this is to prevent setting
3334
// poolIdent to None, and then filling the pool UTXO with garbage tokens and eventually locking it
3435
expect assets.1st.1st == pool_state.quantity_a.1st
@@ -48,10 +49,22 @@ pub fn do_donation(
4849
// is so that we can do a few expects without having to return a value right away
4950
expect or {
5051
!has_remainder,
51-
and {
52-
output.address == destination.address,
53-
output.datum == destination.datum,
54-
output.value == remainder,
52+
// Make sure we're paying the result to the correct destination (both the address and the datum),
53+
// with the correct amount; In the special case where Datum is "Self" (for example for a repeating strategy)
54+
// use the input datum for validation
55+
when destination is {
56+
Fixed { address, datum } -> {
57+
and {
58+
output.address == address,
59+
output.datum == datum
60+
}
61+
}
62+
Self -> {
63+
and {
64+
output.address == input_address,
65+
output.datum == input_datum
66+
}
67+
}
5568
},
5669
}
5770

@@ -109,7 +122,7 @@ test donation() {
109122
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
110123
),
111124
max_protocol_fee: 2_500_000,
112-
destination: Destination { address: addr, datum: NoDatum },
125+
destination: Fixed { address: addr, datum: NoDatum },
113126
details: order.Donation {
114127
assets: assets,
115128
},
@@ -123,8 +136,14 @@ test donation() {
123136
datum: NoDatum,
124137
reference_script: None,
125138
}
139+
let input = Output {
140+
address: addr,
141+
value: input_value,
142+
datum: InlineDatum(order),
143+
reference_script: None,
144+
}
126145
let (final_pool_state, has_remainder) =
127-
do_donation(pool_state, input_value, assets, order.destination, 2_500_000, output)
146+
do_donation(pool_state, input, assets, order.destination, 2_500_000, output)
128147
expect !has_remainder
129148
expect final_pool_state.quantity_a.3rd == 1_001_000_000
130149
expect final_pool_state.quantity_b.3rd == 1_001_000_000

lib/calculation/swap.ak

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
use aiken/transaction.{NoDatum, Output}
1+
use aiken/transaction.{NoDatum, InlineDatum, Output}
22
use aiken/transaction/credential.{Address, VerificationKeyCredential}
33
use aiken/transaction/value.{
44
AssetName, PolicyId, Value, ada_asset_name, ada_policy_id,
55
}
66
use calculation/shared.{PoolState} as calc_shared
77
use shared.{SingletonValue}
88
use sundae/multisig
9-
use types/order.{Destination, OrderDatum}
9+
use types/order.{Destination, Fixed, Self, OrderDatum}
1010

1111
/// Compute the amount of token a swap yields; returns the amount of token received, and the remainder to be sent to the user
1212
///
@@ -60,7 +60,7 @@ pub fn swap_takes(
6060
pub fn do_swap(
6161
pool_state: PoolState,
6262
/// The value coming in from the UTXO with the order
63-
input_value: Value,
63+
input_utxo: Output,
6464
/// Where the results of the swap need to be paid
6565
destination: Destination,
6666
/// The liquidity provider fee to charge
@@ -73,6 +73,7 @@ pub fn do_swap(
7373
/// The output to compare against
7474
output: Output,
7575
) -> PoolState {
76+
let Output { address: input_address, datum: input_datum, value: input_value, .. } = input_utxo
7677
// Destructure the pool state
7778
// TODO: it'd make the code nightmarish, but things would be way more efficient to just always pass these values destructured as parameters...
7879
let (offer_policy_id, offer_asset_name, offer_amt) = offer
@@ -84,6 +85,24 @@ pub fn do_swap(
8485
let (a_policy_id, a_asset_name, a_amt) = quantity_a
8586
let (b_policy_id, b_asset_name, b_amt) = quantity_b
8687

88+
// Make sure we're paying the result to the correct destination (both the address and the datum),
89+
// with the correct amount; In the special case where Datum is "Self" (for example for a repeating strategy)
90+
// use the input datum for validation
91+
expect when destination is {
92+
Fixed { address, datum } -> {
93+
and {
94+
output.address == address,
95+
output.datum == datum
96+
}
97+
}
98+
Self -> {
99+
and {
100+
output.address == input_address,
101+
output.datum == input_datum
102+
}
103+
}
104+
}
105+
87106
// There are two symmetric cases, depending on whether you're giving order A or order B
88107
// If there's a clever way to write this as one branch it might be clearer to read
89108
if offer_policy_id == a_policy_id && offer_asset_name == a_asset_name {
@@ -106,10 +125,9 @@ pub fn do_swap(
106125
input_value,
107126
)
108127

109-
// Check that the output has the correct destination and value
110-
expect output.address == destination.address
111-
expect output.datum == destination.datum
128+
// Make sure the correct value (including change) carries through to the output
112129
expect output.value == out_value
130+
113131
// And check that the min_received (the lowest amount of tokens the user is willing to receive, aka a limit price)
114132
// is satisfied
115133
expect takes >= min_received.3rd
@@ -137,9 +155,9 @@ pub fn do_swap(
137155
input_value,
138156
)
139157

140-
expect output.address == destination.address
141-
expect output.datum == destination.datum
158+
// Make sure the correct value (including change) carries through to the output
142159
expect output.value == out_value
160+
143161
// Check that mintakes is satisfied
144162
expect takes >= min_received.3rd
145163
PoolState {
@@ -179,7 +197,7 @@ test swap_mintakes_too_high() fail {
179197
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
180198
),
181199
max_protocol_fee: 2_500_000,
182-
destination: Destination {
200+
destination: Fixed {
183201
address: addr,
184202
datum: NoDatum,
185203
},
@@ -193,7 +211,13 @@ test swap_mintakes_too_high() fail {
193211
datum: NoDatum,
194212
reference_script: None,
195213
}
196-
let final_pool_state = do_swap(pool_state, input_value, order.destination, 5, 2_500_000, swap_offer, swap_min_received, output)
214+
let input = Output {
215+
address: addr,
216+
value: input_value,
217+
datum: InlineDatum(order),
218+
reference_script: None,
219+
}
220+
let final_pool_state = do_swap(pool_state, input, order.destination, 5, 2_500_000, swap_offer, swap_min_received, output)
197221
expect final_pool_state.quantity_a.3rd == 1_000_000_000 + 10_000_000
198222
expect final_pool_state.quantity_b.3rd == 1_000_000_000 - 9_896_088
199223
True

lib/calculation/withdrawal.ak

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
use aiken/option
22
use aiken/math
3-
use aiken/transaction.{NoDatum, Output}
3+
use aiken/transaction.{NoDatum, InlineDatum, Output}
44
use aiken/transaction/credential.{Address, VerificationKeyCredential}
55
use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name}
66
use calculation/shared.{PoolState} as calc_shared
77
use sundae/multisig
88
use shared.{SingletonValue}
9-
use types/order.{Destination, OrderDatum}
9+
use types/order.{Destination, Fixed, Self, OrderDatum}
1010

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

2526
// Make sure we're withdrawing from the right pool;
2627
// 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(
6162
withdrawn_b,
6263
)
6364

64-
// Check that the result is paid to the destination with the full value
65-
// Like on other order types, the datum here lets us compose with other protocols
66-
// or do chained orders within Sundae
67-
expect output.address == destination.address
68-
expect output.datum == destination.datum
65+
66+
// Make sure we're paying the result to the correct destination (both the address and the datum),
67+
// with the correct amount; In the special case where Datum is "Self" (for example for a repeating strategy)
68+
// use the input datum for validation
6969
expect output.value == remainder
70+
expect when destination is {
71+
Fixed { address, datum } -> {
72+
and {
73+
output.address == address,
74+
output.datum == datum
75+
}
76+
}
77+
Self -> {
78+
and {
79+
output.address == input_address,
80+
output.datum == input_datum
81+
}
82+
}
83+
}
7084

7185
// Return the final pool state
7286
PoolState {
@@ -153,7 +167,7 @@ fn withdrawal_test(options: WithdrawalTestOptions) {
153167
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
154168
),
155169
max_protocol_fee: 2_500_000,
156-
destination: Destination {
170+
destination: Fixed {
157171
address: addr,
158172
datum: NoDatum,
159173
},
@@ -172,7 +186,13 @@ fn withdrawal_test(options: WithdrawalTestOptions) {
172186
datum: NoDatum,
173187
reference_script: None,
174188
}
175-
let final_pool_state = do_withdrawal(pool_state, input_value, amount, order.destination, 2_500_000, output)
189+
let input = Output {
190+
address: addr,
191+
value: input_value,
192+
datum: InlineDatum(order),
193+
reference_script: None,
194+
}
195+
let final_pool_state = do_withdrawal(pool_state, input, amount, order.destination, 2_500_000, output)
176196
expect final_pool_state.quantity_a.3rd == 1_000_000_000
177197
expect final_pool_state.quantity_b.3rd == 1_000_000_000
178198
True

lib/tests/aiken/deposit.ak

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use sundae/multisig
22
use aiken/math
3-
use aiken/transaction.{NoDatum, Output}
3+
use aiken/transaction.{NoDatum, InlineDatum, Output}
44
use aiken/transaction/value
55
use aiken/transaction/credential.{Address, VerificationKeyCredential}
66
use calculation/shared.{PoolState}
77
use calculation/deposit.{do_deposit}
8-
use types/order.{OrderDatum, Destination}
8+
use types/order.{OrderDatum, Destination, Fixed}
99

1010
test deposit_test() {
1111
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
4848
#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513",
4949
),
5050
max_protocol_fee: 2_500_000,
51-
destination: Destination {
51+
destination: Fixed {
5252
address: addr,
5353
datum: NoDatum,
5454
},
@@ -63,9 +63,15 @@ fn deposit_test_schema(qa: Int, qb: Int, has_a: Int, has_b: Int, gives_a: Int, g
6363
datum: NoDatum,
6464
reference_script: None,
6565
}
66+
let input = Output {
67+
address: addr,
68+
value: input_value,
69+
datum: InlineDatum(order),
70+
reference_script: None,
71+
}
6672
let PoolState{..} = do_deposit(
6773
pool_state,
68-
input_value,
74+
input,
6975
assets,
7076
order.destination,
7177
2_500_000,

0 commit comments

Comments
 (0)