Skip to content

Commit

Permalink
Merge pull request #49 from SundaeSwap-finance/pi/script-strategies
Browse files Browse the repository at this point in the history
Add script-enforced strategies
  • Loading branch information
Quantumplation authored Mar 6, 2024
2 parents d5890b1 + 0f58c80 commit 3f95924
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 21 deletions.
14 changes: 11 additions & 3 deletions lib/calculation/process.ak
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use aiken/dict.{Dict}
use aiken/hash.{Blake2b_256, Hash}
use aiken/interval.{IntervalBound, NegativeInfinity, Finite}
use aiken/transaction.{InlineDatum, NoDatum, Input, Output, OutputReference, TransactionId, ValidityRange}
use aiken/transaction/credential.{Address, ScriptCredential, VerificationKeyCredential}
use aiken/transaction/credential.{Address, ScriptCredential, StakeCredential, VerificationKeyCredential}
use aiken/transaction/value.{Value, PolicyId}
use aiken/time.{PosixTime}
use calculation/deposit
Expand Down Expand Up @@ -117,6 +117,8 @@ pub fn process_order(
output_reference: OutputReference,
// The validity range of the transaction, used to ensure the signed execution is within the correct time window
tx_valid_range: ValidityRange,
// The transaction withdrawals, so we can check strategy executions
withdrawals: Dict<StakeCredential, Int>,
// 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 All @@ -143,6 +145,7 @@ pub fn process_order(
let details = strategy.get_strategy(
output_reference,
tx_valid_range,
withdrawals,
signer,
execution,
)
Expand All @@ -151,6 +154,7 @@ pub fn process_order(
None, // No need to pass a signed execution through again; it doesn't make sense to have multiple nested strategies
output_reference,
tx_valid_range,
withdrawals,
value,
details,
max_protocol_fee,
Expand Down Expand Up @@ -235,6 +239,8 @@ 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
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>,
// The initial / current pool state, passed recursively as we process each order
Expand Down Expand Up @@ -319,6 +325,7 @@ pub fn process_orders(
sse,
output_reference,
tx_valid_range,
withdrawals,
order.value,
details,
max_protocol_fee,
Expand All @@ -334,6 +341,7 @@ pub fn process_orders(
process_orders(
this_pool_ident,
tx_valid_range,
withdrawals,
datums,
next_state,
rest, // This advances to the next element from input_order
Expand Down Expand Up @@ -425,7 +433,7 @@ test process_orders_test() {
let inputs = [input]
let outputs = [output]

let (final_pool_state, simple, strategies) = process_orders(#"", valid_range, 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, 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 @@ -538,7 +546,7 @@ test process_30_shuffled_orders_test() {
]
let outputs = [output]

let (final_pool_state, simple, strategies) = process_orders(#"", valid_range, 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, 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
29 changes: 20 additions & 9 deletions lib/calculation/strategy.ak
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use aiken/cbor
use aiken/dict.{Dict}
use aiken/interval.{Interval, Finite}
use aiken/transaction.{OutputReference, ValidityRange}
use aiken/transaction/credential.{verify_signature, VerificationKey}
use types/order.{StrategyExecution, SignedStrategyExecution, Order}
use aiken/transaction/credential.{verify_signature, Inline, ScriptCredential, StakeCredential}
use types/order.{StrategyExecution, StrategyAuthorization, Signature, Script, SignedStrategyExecution, Order}

/// Check that the outer interval completely contains the inner interval
fn contains_interval(outer: Interval<Int>, inner: Interval<Int>) -> Bool {
Expand Down Expand Up @@ -61,21 +62,31 @@ fn contains_interval(outer: Interval<Int>, inner: Interval<Int>) -> Bool {
pub fn get_strategy(
order_tx_ref: OutputReference,
tx_valid_range: ValidityRange,
signer: VerificationKey,
withdrawals: Dict<StakeCredential, Int>,
auth: StrategyAuthorization,
strategy: SignedStrategyExecution,
) -> Order {
let SignedStrategyExecution { strategy, signature } = strategy
let StrategyExecution { tx_ref, validity_range, details } = strategy
let StrategyExecution { tx_ref, validity_range, details, .. } = strategy
// We check that the order_tx_ref from the strategy matches the order we're actually processing,
// to prevent replay / cross-play attacks.
expect order_tx_ref == tx_ref
// Make sure the transaction validity range (all the valid points in time the tx could exist)
// is entirely contained in the *strategy* valid range (all the points where the strategy is valid)
// This is so the user can impose timing restrictions (for example, maybe they only want their order to execute 20 minutes from now)
expect contains_interval(validity_range, tx_valid_range)
// And finally, use cbor.serialise and check that the signature is valid
// TODO: is this at risk if cbor.serialise changes? is there a way for us to get the raw bytes of the data?
let strategy_bytes = cbor.serialise(details)
expect verify_signature(signer, strategy_bytes, signature)
details
when auth is {
Signature { signer } -> {
// And finally, use cbor.serialise and check that the signature is valid
// TODO: is this at risk if cbor.serialise changes? is there a way for us to get the raw bytes of the data?
let strategy_bytes = cbor.serialise(details)
expect Some(signature) = signature
expect verify_signature(signer, strategy_bytes, signature)
details
}
Script { script } -> {
expect dict.has_key(withdrawals, Inline(ScriptCredential(script)))
details
}
}
}
8 changes: 4 additions & 4 deletions lib/tests/examples/ex_order.ak
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use aiken/cbor
use types/order.{Destination, OrderDatum, Swap, Deposit, Withdrawal, Strategy, StrategyExecution, SignedStrategyExecution, Scoop, Cancel}
use types/order.{Destination, OrderDatum, Swap, Deposit, Withdrawal, Strategy, StrategyExecution, SignedStrategyExecution, Signature, Scoop, Cancel}
use aiken/transaction.{NoDatum, OutputReference, TransactionId}
use aiken/interval
use aiken/transaction/credential.{VerificationKey, verify_signature}
use sundae/multisig
use tests/examples/ex_shared.{print_example, wallet_address}

Expand Down Expand Up @@ -93,7 +92,7 @@ test example_withdrawal() {
fn mk_strategy() {
let addr = wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513")
let dest = Destination { address: addr, datum: NoDatum }
let strategy = Strategy(#"d441227553a0f1a965fee7d60a0f724b368dd1bddbc208730fccebcf")
let strategy = Strategy(Signature(#"d441227553a0f1a965fee7d60a0f724b368dd1bddbc208730fccebcf"))
OrderDatum {
pool_ident: Some(#"fc2c4a6ae8048b0b5affc169dfd496a7ace7d08288c476d9d7a5804e"),
owner: multisig.Signature(
Expand All @@ -117,6 +116,7 @@ fn mk_strategy_execution() {
tx_ref: tx_ref,
validity_range: valid_range,
details: mk_swap().details,
extensions: Void,
}
}

Expand All @@ -128,7 +128,7 @@ fn mk_signed_strategy_execution() {
let sig = #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513"
SignedStrategyExecution {
strategy: mk_strategy_execution(),
signature: sig,
signature: Some(sig),
}
}

Expand Down
18 changes: 15 additions & 3 deletions lib/types/order.ak
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use aiken/transaction.{Datum, OutputReference, ValidityRange}
use aiken/transaction/credential.{Address, VerificationKey, Signature}
use aiken/transaction/credential.{Address, VerificationKey, Signature, Script}
use shared.{Ident, SingletonValue}
use sundae/multisig.{MultisigScript}
use aiken/hash.{Hash, Blake2b_224}

/// An order to execute within the SundaeSwap ecosystem
pub type OrderDatum {
Expand Down Expand Up @@ -38,13 +39,21 @@ pub type Destination {
datum: Datum,
}

/// There are two ways to delegate authorization of a strategy::
/// - by a signing key
/// - or by some script in the withdrawals of the transaction
pub type StrategyAuthorization {
Signature { signer: VerificationKey }
Script { script: Hash<Blake2b_224, Script> }
}

/// The specific order details for executing an order
pub type Order {
/// The details of the order aren't yet determined, but will be communicated out of band to the scoopers
/// This can be thought of as "late-binding" the order details, so long as the strategy is signed by `signer`
/// For example, you could use this to implement trailing stop loss, where the order to sell a token is only triggered
/// after a specific threshold is met.
Strategy { signer: VerificationKey }
Strategy { auth: StrategyAuthorization }
/// A swap from one token to another, offering some subset of the UTXO, in return for at least some quantity of another token
Swap { offer: SingletonValue, min_received: SingletonValue }
/// A deposit, offering at most two quantities from the UTXO to provide liquidity to the pool, resulting in LP tokens paid to the output to track an ownership percentage of the pool
Expand Down Expand Up @@ -83,12 +92,15 @@ pub type StrategyExecution {
/// These are the late-bound details that the delegated authority chose to execute
/// likely based on some arbitrary off-chain trigger condition
details: Order,
/// This extension data allows arbitrary other data to be attached that the pool
/// doesn't care about, but that the order script might
extensions: Data,
}

/// A specific strategy execution, plus a signature of that data
pub type SignedStrategyExecution {
/// The details about how to execute the strategy for a given order
strategy: StrategyExecution,
/// An ed25519 signature of the serialized `strategy`
signature: Signature,
signature: Option<Signature>,
}
3 changes: 2 additions & 1 deletion validators/pool.ak
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use aiken/bytearray
use aiken/cbor
use aiken/dict
use aiken/hash
use aiken/interval
Expand Down Expand Up @@ -62,6 +61,7 @@ validator(settings_policy_id: PolicyId) {
datums,
extra_signatories,
validity_range,
withdrawals,
..
} = transaction

Expand Down Expand Up @@ -136,6 +136,7 @@ validator(settings_policy_id: PolicyId) {
process_orders(
actual_identifier, // The pool identifier, so we can check that each order is for this pool
validity_range, // The validity range of the transaction, so we can check strategies haven't expired
withdrawals, // Include the withdrawals, in case a strategy has some kind of attached script condition
datums, // The datums, so we can look up the datum of each order (which may be inline, but may also be in the datums dict)
initial_state, // The initial pool state, such as the reserves and circulating LP
input_order, // The input ordering specified by the scooper
Expand Down
2 changes: 1 addition & 1 deletion validators/tests/settings.ak
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ use aiken/transaction/value
use sundae/multisig
use types/settings.{SettingsDatum, SettingsAdminUpdate, settings_nft_name}
use tx_util/builder.{
add_asset_to_tx_output,
build_txn_context,
add_tx_input,
add_tx_output,
new_tx_output,
new_tx_input,
with_asset_of_tx_input,
add_asset_to_tx_output,
mint_assets
}
use tests/examples/ex_settings.{example_settings_admin}
Expand Down

0 comments on commit 3f95924

Please sign in to comment.