From 3c0130fb47e654704ac90034cb1447b9f316e176 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Mon, 8 Jan 2024 09:02:05 -0500 Subject: [PATCH 01/13] Initial draft of the Oracle feature --- lib/calculation/oracle.ak | 24 ++++++++++ lib/calculation/process.ak | 8 ++++ lib/shared.ak | 4 ++ lib/types/oracle.ak | 21 +++++++++ lib/types/order.ak | 2 + validators/oracle.ak | 94 ++++++++++++++++++++++++++++++++++++++ validators/pool.ak | 1 + 7 files changed, 154 insertions(+) create mode 100644 lib/calculation/oracle.ak create mode 100644 lib/types/oracle.ak create mode 100644 validators/oracle.ak diff --git a/lib/calculation/oracle.ak b/lib/calculation/oracle.ak new file mode 100644 index 0000000..8f30eee --- /dev/null +++ b/lib/calculation/oracle.ak @@ -0,0 +1,24 @@ +use aiken/transaction.{Output} +use aiken/transaction/credential.{Address, VerificationKeyCredential} +use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name} +use types/order.{OrderDatum} + +pub fn check_oracle( + input_value: Value, + order: OrderDatum, + actual_protocol_fee: Int, + output: Output, +) -> Bool { + // Make sure all of the funds from the input make it into the oracle output + // Note that the accuracy of the oracle is handled by the mint policy, since it has easier + // access to the final state of the order + let remainder = value.add(input_value, ada_policy_id, ada_asset_name, -actual_protocol_fee) + + // TODO + True +} + +test oracle() { + // TODO + True +} diff --git a/lib/calculation/process.ak b/lib/calculation/process.ak index 4c455b6..8e39d65 100644 --- a/lib/calculation/process.ak +++ b/lib/calculation/process.ak @@ -9,6 +9,7 @@ use aiken/transaction/value.{Value, PolicyId} use aiken/time.{PosixTime} use calculation/deposit use calculation/donation +use calculation/oracle use calculation/shared.{ PoolState, check_and_set_unique, unsafe_fast_index_skip_with_tail, } as calc_shared @@ -178,6 +179,13 @@ pub fn process_order( (next, outputs, fee) } } + order.Oracle -> { + // Make sure the scooper can only take up to the max fee the user has agreed to + let fee = amortized_base_fee + simple_fee + expect max_protocol_fee >= fee + expect oracle.check_oracle(value, datum, fee, output) + (initial, rest_outputs, fee) + } } } diff --git a/lib/shared.ak b/lib/shared.ak index e8f21dd..301928e 100644 --- a/lib/shared.ak +++ b/lib/shared.ak @@ -149,3 +149,7 @@ pub fn pool_nft_name(pool_ident: Ident) { pub fn pool_lp_name(pool_ident: Ident) { bytearray.concat(#"0014df10", pool_ident) } + +pub fn oracle_sft_name() { + "oracle" +} diff --git a/lib/types/oracle.ak b/lib/types/oracle.ak new file mode 100644 index 0000000..f0f3477 --- /dev/null +++ b/lib/types/oracle.ak @@ -0,0 +1,21 @@ +use sundae/multisig +use aiken/transaction.{ValidityRange} +use shared.{SingletonValue, Ident} + +pub type OracleDatum { + // The owner who is allowed to reclaim this datum at the end + owner: multisig.MultisigScript, + // The valid range for the scoop transaction that produced this Datum, which gives a confidence interval for when this price was valid + valid_range: ValidityRange, + // The pool identifier that this datum was produced for + pool_ident: Ident, + // The reserve *after* the scoop in question + reserve_a: SingletonValue, + reserve_b: SingletonValue, + circulating_lp: SingletonValue, +} + +pub type OracleRedeemer { + Mint(Ident) + Burn +} \ No newline at end of file diff --git a/lib/types/order.ak b/lib/types/order.ak index 74329e2..3ca8aff 100644 --- a/lib/types/order.ak +++ b/lib/types/order.ak @@ -42,6 +42,8 @@ pub type Order { // ZapOut { assets: (Int, Int) } // Donate some value to the pool Donation { assets: (SingletonValue, SingletonValue) } + // Create an oracle UTXO, for other protocols to read the price at a given time + Oracle } // In order to redeem an order, you can either diff --git a/validators/oracle.ak b/validators/oracle.ak new file mode 100644 index 0000000..bd4d88d --- /dev/null +++ b/validators/oracle.ak @@ -0,0 +1,94 @@ +use aiken/list +use aiken/transaction/value +use aiken/hash.{Blake2b_224, Hash} +use aiken/transaction.{ScriptContext, InlineDatum} +use aiken/transaction/credential.{Script, ScriptCredential} +use sundae/multisig +use shared +use types/pool.{PoolDatum} +use types/oracle.{OracleRedeemer, OracleDatum, Mint, Burn} + +// The oracle script holds an oracle token, and a snapshot of the pool price at the *end* of some scoop. +// This allows other protocols to build integrations that read the pool price (for some confidence interval) without worrying about contention +// +// It's important to use the price at the *end* of the scoop, or at the beginning, rather than just using the price +// "at the time" the order was processed. If we expose the pool price mid-stream, then it is easy to sandwich the order between two others. +// By using the snapshot at the end of the order, such an attacker exposes themselves to arbitrage opportunities which makes such an attack riskier. +validator(pool_script_hash: Hash) { + // In order to spend the oracle script, two things must be true: + // - it must be signed by the "owner" + // - there must be no oracle tokens on the outputs + // This allows reclaiming funds that were accidentally locked at the script address, + // while also enforcing that the oracle token is burned + fn spend(datum: Data, _r: Data, ctx: ScriptContext) -> Bool { + let own_input = shared.spent_output(ctx) + expect ScriptCredential(own_script_hash) = own_input.address.payment_credential + expect datum: OracleDatum = datum + and { + multisig.satisfied( + datum.owner, + ctx.transaction.extra_signatories, + ctx.transaction.validity_range, + ), + list.all( + ctx.transaction.outputs, + fn(output) { + value.quantity_of(output.value, own_script_hash, shared.oracle_sft_name()) == 0 + }, + ) + } + } + // In order to mint an orcale token, two things must be true: + // - each oracle token on the outputs must be paid with a quantity of 1 to the oracle script + // - the datum for each must have the correct timing and pricing information + // Burning an oracle token is always allowed + fn mint(redeemer: OracleRedeemer, ctx: ScriptContext) { + when redeemer is { + Mint(pool_ident) -> { + expect transaction.Mint(own_policy_id) = ctx.purpose + let pool_lp_name = shared.pool_lp_name(pool_ident) + let pool_nft_name = shared.pool_nft_name(pool_ident) + + expect Some(pool_output) = list.head(ctx.transaction.outputs) + expect pool_output.address.payment_credential == ScriptCredential(pool_script_hash) + expect value.quantity_of(pool_output.value, own_policy_id, pool_nft_name) == 1 + expect InlineDatum(pool_datum) = pool_output.datum + expect pool_datum: PoolDatum = pool_datum + let PoolDatum { + assets: (asset_a, asset_b), + circulating_lp, + .. + } = pool_datum + + let reserve_a = (asset_a.1st, asset_a.2nd, value.quantity_of(pool_output.value, asset_a.1st, asset_a.2nd)) + let reserve_b = (asset_b.1st, asset_b.2nd, value.quantity_of(pool_output.value, asset_b.1st, asset_b.2nd)) + let circulating_lp = (pool_script_hash, pool_lp_name, circulating_lp) + + let oracle_name = shared.oracle_sft_name() + + list.all( + ctx.transaction.outputs, + fn(output) { + let qty = value.quantity_of(output.value, own_policy_id, oracle_name) + when qty is { + 0 -> True + 1 -> { + expect output.address.payment_credential == ScriptCredential(own_policy_id) + expect Some(oracle_datum) = shared.datum_of(ctx.transaction.datums, output) + expect oracle_datum: OracleDatum = oracle_datum + expect oracle_datum.valid_range == ctx.transaction.validity_range + expect oracle_datum.pool_ident == pool_ident + expect reserve_a == oracle_datum.reserve_a + expect reserve_b == oracle_datum.reserve_b + expect circulating_lp == oracle_datum.circulating_lp + True + } + _ -> False + } + } + ) + } + Burn -> True + } + } +} diff --git a/validators/pool.ak b/validators/pool.ak index 27a0566..dfca2d2 100644 --- a/validators/pool.ak +++ b/validators/pool.ak @@ -121,6 +121,7 @@ validator(settings_policy_id: PolicyId) { expect ScriptCredential(pool_script_hash) = pool_input.address.payment_credential // Find the pool output + // TODO: this doesn't let us change the stake address expect Some(pool_output) = list.head(outputs) expect pool_output.address == pool_input.address expect InlineDatum(output_datum) = pool_output.datum From b86ee284609962ab2b6682885b57e0eb393e48cc Mon Sep 17 00:00:00 2001 From: rrruko Date: Fri, 8 Mar 2024 13:59:41 -0800 Subject: [PATCH 02/13] implement check_oracle and tests --- lib/calculation/oracle.ak | 86 +++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/lib/calculation/oracle.ak b/lib/calculation/oracle.ak index 8f30eee..7a12f6b 100644 --- a/lib/calculation/oracle.ak +++ b/lib/calculation/oracle.ak @@ -1,7 +1,9 @@ -use aiken/transaction.{Output} -use aiken/transaction/credential.{Address, VerificationKeyCredential} +use aiken/transaction.{NoDatum, InlineDatum, Output} +use aiken/transaction/credential.{Address, ScriptCredential} use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name} -use types/order.{OrderDatum} +use sundae/multisig +use types/order.{Destination, OrderDatum} +use types/oracle.{OracleDatum} pub fn check_oracle( input_value: Value, @@ -14,11 +16,81 @@ pub fn check_oracle( // access to the final state of the order let remainder = value.add(input_value, ada_policy_id, ada_asset_name, -actual_protocol_fee) - // TODO - True + and { + output.address == order.destination.address, + output.datum == order.destination.datum, + output.value == remainder + } } test oracle() { - // TODO - True + let addr = + Address( + ScriptCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ) + let ada = (#"", #"") + let rberry = (#"01010101010101010101010101010101010101010101010101010101", "RBERRY") + let lp = (#"99999999999999999999999999999999999999999999999999999999", "LP") + let input_value = + value.from_lovelace(4_500_000) + let order = OrderDatum { + pool_ident: None, + owner: multisig.Signature( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + max_protocol_fee: 2_500_000, + destination: Destination { + address: addr, + datum: NoDatum, + }, + details: order.Oracle, + extension: Void, + } + let output = Output { + address: addr, + value: value.from_lovelace(2_000_000), + datum: NoDatum, + reference_script: None, + } + let ok = check_oracle(input_value, order, 2_500_000, output) + ok +} + +test oracle_bad_datum() { + let addr = + Address( + ScriptCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ) + let ada = (#"", #"") + let rberry = (#"01010101010101010101010101010101010101010101010101010101", "RBERRY") + let lp = (#"99999999999999999999999999999999999999999999999999999999", "LP") + let input_value = + value.from_lovelace(4_500_000) + let order = OrderDatum { + pool_ident: None, + owner: multisig.Signature( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + max_protocol_fee: 2_500_000, + destination: Destination { + address: addr, + datum: NoDatum, + }, + details: order.Oracle, + extension: Void, + } + let output = Output { + address: addr, + value: value.from_lovelace(2_000_000), + datum: InlineDatum(Void), + reference_script: None, + } + let ok = check_oracle(input_value, order, 2_500_000, output) + !ok } From 7355a6752b3111b5fc7d50c13d1e23254b159b8e Mon Sep 17 00:00:00 2001 From: rrruko Date: Fri, 8 Mar 2024 14:21:11 -0800 Subject: [PATCH 03/13] add basic test for oracle validator --- validators/oracle.ak | 78 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/validators/oracle.ak b/validators/oracle.ak index bd4d88d..d562dd5 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -1,12 +1,15 @@ +use aiken/dict use aiken/list -use aiken/transaction/value use aiken/hash.{Blake2b_224, Hash} -use aiken/transaction.{ScriptContext, InlineDatum} +use aiken/interval +use aiken/transaction.{Transaction, Output, ScriptContext, InlineDatum} use aiken/transaction/credential.{Script, ScriptCredential} +use aiken/transaction/value use sundae/multisig use shared use types/pool.{PoolDatum} use types/oracle.{OracleRedeemer, OracleDatum, Mint, Burn} +use tests/examples/ex_shared.{wallet_address, script_address, mk_output_reference, mk_tx_hash} // The oracle script holds an oracle token, and a snapshot of the pool price at the *end* of some scoop. // This allows other protocols to build integrations that read the pool price (for some confidence interval) without worrying about contention @@ -92,3 +95,74 @@ validator(pool_script_hash: Hash) { } } } + +test oracle() { + let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000" + let pool_script_hash = #"00000000000000000000000000000000000000000000000000000000" + let pool_address = script_address(pool_script_hash) + let rberry_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" + let rberry_token_name = #"524245525259" + let user_address = + wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513") + let pool_id = #"00" + let pool_lp_name = shared.pool_lp_name(pool_id) + let pool_nft_name = shared.pool_nft_name(pool_id) + // This looks like a fresh pool but pretend that we're scooping + let pool_output = Output { + address: pool_address, + value: value.from_lovelace(1_000_000_000) + |> value.add(rberry_policy_id, rberry_token_name, 1_000_000_000) + |> value.add(pool_script_hash, pool_nft_name, 1), + datum: InlineDatum(PoolDatum { + identifier: pool_id, + assets: ((#"", #""), (rberry_policy_id, rberry_token_name)), + circulating_lp: 1_000_000_000, + fees_per_10_thousand: (5, 5), + market_open: 0, + fee_finalized: 0, + protocol_fees: 2_000_000, + }), + reference_script: None, + } + let oracleMintRedeemer = Mint(pool_id) + let oracle_name = shared.oracle_sft_name() + let oracle_output = Output { + address: script_address(oracle_policy_id), + value: value.from_lovelace(1_000_000) + |> value.add(oracle_policy_id, oracle_name, 1), + datum: InlineDatum(OracleDatum { + owner: + multisig.Signature( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + valid_range: interval.between(1, 2), + pool_ident: pool_id, + reserve_a: ("", "", 1_000_000_000), + reserve_b: (rberry_policy_id, rberry_token_name, 1_000_000_000), + circulating_lp: (pool_script_hash, pool_lp_name, 1_000_000_000), + }), + reference_script: None, + } + let ctx = ScriptContext { + transaction: Transaction { + inputs: [], + outputs: [pool_output, oracle_output], + reference_inputs: [], + fee: value.from_lovelace(1_000_000), + mint: value.to_minted_value( + value.from_lovelace(0) + |> value.add(oracle_policy_id, oracle_name, 1) + ), + certificates: [], + withdrawals: dict.new(), + validity_range: interval.between(1, 2), + extra_signatories: [], + redeemers: dict.new(), + datums: dict.new(), + id: mk_tx_hash(1), + }, + purpose: transaction.Mint(oracle_policy_id), + } + let result = mint(oracle_policy_id, oracleMintRedeemer, ctx) + result +} From 23b6d2cf5386a21e57705d09dcf2eb8faeafea7c Mon Sep 17 00:00:00 2001 From: rrruko Date: Fri, 8 Mar 2024 14:29:09 -0800 Subject: [PATCH 04/13] more oracle minting policy tests --- validators/oracle.ak | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/validators/oracle.ak b/validators/oracle.ak index d562dd5..330f29f 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -96,7 +96,42 @@ validator(pool_script_hash: Hash) { } } -test oracle() { +test oracle_basic() { + mint_oracle(identity, identity) +} + +// Minting policy enforces that the reserves are correct +!test oracle_wrong_datum() { + mint_oracle( + fn(old_datum) { + OracleDatum { + ..old_datum, + reserve_a: ("", "", 1_000_000_000_000_000_000), + } + }, + identity, + ) +} + +// If we mint a token with the wrong name, we can choose whatever datum we want +test oracle_fake_token() { + mint_oracle( + fn(old_datum) { + OracleDatum { + ..old_datum, + reserve_a: ("", "", 1_000_000_000_000_000_000), + } + }, + fn(_) { + "fake" + }, + ) +} + +fn mint_oracle( + modify_oracle_datum: fn(OracleDatum) -> OracleDatum, + modify_oracle_name: fn(ByteArray) -> ByteArray, +) { let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000" let pool_script_hash = #"00000000000000000000000000000000000000000000000000000000" let pool_address = script_address(pool_script_hash) @@ -125,12 +160,12 @@ test oracle() { reference_script: None, } let oracleMintRedeemer = Mint(pool_id) - let oracle_name = shared.oracle_sft_name() + let oracle_name = modify_oracle_name(shared.oracle_sft_name()) let oracle_output = Output { address: script_address(oracle_policy_id), value: value.from_lovelace(1_000_000) |> value.add(oracle_policy_id, oracle_name, 1), - datum: InlineDatum(OracleDatum { + datum: InlineDatum(modify_oracle_datum(OracleDatum { owner: multisig.Signature( #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", @@ -140,7 +175,7 @@ test oracle() { reserve_a: ("", "", 1_000_000_000), reserve_b: (rberry_policy_id, rberry_token_name, 1_000_000_000), circulating_lp: (pool_script_hash, pool_lp_name, 1_000_000_000), - }), + })), reference_script: None, } let ctx = ScriptContext { From ae03c4c42422ef5711822d81b9289271d2a1c299 Mon Sep 17 00:00:00 2001 From: rrruko Date: Fri, 8 Mar 2024 15:16:03 -0800 Subject: [PATCH 05/13] add burn redeemer test --- validators/oracle.ak | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/validators/oracle.ak b/validators/oracle.ak index 330f29f..4bbb9f3 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -97,9 +97,25 @@ validator(pool_script_hash: Hash) { } test oracle_basic() { - mint_oracle(identity, identity) + mint_oracle( + identity, + identity, + identity, + ) +} + +test oracle_burn_mint() { + mint_oracle( + identity, + identity, + fn(old_redeemer) { + Burn + } + ) } + + // Minting policy enforces that the reserves are correct !test oracle_wrong_datum() { mint_oracle( @@ -110,6 +126,7 @@ test oracle_basic() { } }, identity, + identity, ) } @@ -125,12 +142,14 @@ test oracle_fake_token() { fn(_) { "fake" }, + identity, ) } fn mint_oracle( modify_oracle_datum: fn(OracleDatum) -> OracleDatum, modify_oracle_name: fn(ByteArray) -> ByteArray, + modify_redeemer: fn(OracleRedeemer) -> OracleRedeemer, ) { let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000" let pool_script_hash = #"00000000000000000000000000000000000000000000000000000000" @@ -159,7 +178,7 @@ fn mint_oracle( }), reference_script: None, } - let oracleMintRedeemer = Mint(pool_id) + let oracleMintRedeemer = modify_redeemer(Mint(pool_id)) let oracle_name = modify_oracle_name(shared.oracle_sft_name()) let oracle_output = Output { address: script_address(oracle_policy_id), From 3826ea87a7dd9338f8e6ff6d1252361c66854dd0 Mon Sep 17 00:00:00 2001 From: rrruko Date: Fri, 8 Mar 2024 15:18:08 -0800 Subject: [PATCH 06/13] resolve warnings --- lib/calculation/oracle.ak | 7 ------- validators/oracle.ak | 6 ++---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/calculation/oracle.ak b/lib/calculation/oracle.ak index 7a12f6b..0137d14 100644 --- a/lib/calculation/oracle.ak +++ b/lib/calculation/oracle.ak @@ -3,7 +3,6 @@ use aiken/transaction/credential.{Address, ScriptCredential} use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name} use sundae/multisig use types/order.{Destination, OrderDatum} -use types/oracle.{OracleDatum} pub fn check_oracle( input_value: Value, @@ -31,9 +30,6 @@ test oracle() { ), None, ) - let ada = (#"", #"") - let rberry = (#"01010101010101010101010101010101010101010101010101010101", "RBERRY") - let lp = (#"99999999999999999999999999999999999999999999999999999999", "LP") let input_value = value.from_lovelace(4_500_000) let order = OrderDatum { @@ -67,9 +63,6 @@ test oracle_bad_datum() { ), None, ) - let ada = (#"", #"") - let rberry = (#"01010101010101010101010101010101010101010101010101010101", "RBERRY") - let lp = (#"99999999999999999999999999999999999999999999999999999999", "LP") let input_value = value.from_lovelace(4_500_000) let order = OrderDatum { diff --git a/validators/oracle.ak b/validators/oracle.ak index 4bbb9f3..6910a51 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -9,7 +9,7 @@ use sundae/multisig use shared use types/pool.{PoolDatum} use types/oracle.{OracleRedeemer, OracleDatum, Mint, Burn} -use tests/examples/ex_shared.{wallet_address, script_address, mk_output_reference, mk_tx_hash} +use tests/examples/ex_shared.{script_address, mk_tx_hash} // The oracle script holds an oracle token, and a snapshot of the pool price at the *end* of some scoop. // This allows other protocols to build integrations that read the pool price (for some confidence interval) without worrying about contention @@ -108,7 +108,7 @@ test oracle_burn_mint() { mint_oracle( identity, identity, - fn(old_redeemer) { + fn(_) { Burn } ) @@ -156,8 +156,6 @@ fn mint_oracle( let pool_address = script_address(pool_script_hash) let rberry_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" let rberry_token_name = #"524245525259" - let user_address = - wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513") let pool_id = #"00" let pool_lp_name = shared.pool_lp_name(pool_id) let pool_nft_name = shared.pool_nft_name(pool_id) From a1bf116118b26ef570d198f17c5bb063f59df2cf Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Sat, 9 Mar 2024 14:46:39 -0500 Subject: [PATCH 07/13] Move tests to tests subfolder --- lib/calculation/oracle.ak | 68 +----------------------------------- lib/tests/aiken/oracle.ak | 73 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 67 deletions(-) create mode 100644 lib/tests/aiken/oracle.ak diff --git a/lib/calculation/oracle.ak b/lib/calculation/oracle.ak index 0137d14..2fa081e 100644 --- a/lib/calculation/oracle.ak +++ b/lib/calculation/oracle.ak @@ -20,70 +20,4 @@ pub fn check_oracle( output.datum == order.destination.datum, output.value == remainder } -} - -test oracle() { - let addr = - Address( - ScriptCredential( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - None, - ) - let input_value = - value.from_lovelace(4_500_000) - let order = OrderDatum { - pool_ident: None, - owner: multisig.Signature( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - max_protocol_fee: 2_500_000, - destination: Destination { - address: addr, - datum: NoDatum, - }, - details: order.Oracle, - extension: Void, - } - let output = Output { - address: addr, - value: value.from_lovelace(2_000_000), - datum: NoDatum, - reference_script: None, - } - let ok = check_oracle(input_value, order, 2_500_000, output) - ok -} - -test oracle_bad_datum() { - let addr = - Address( - ScriptCredential( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - None, - ) - let input_value = - value.from_lovelace(4_500_000) - let order = OrderDatum { - pool_ident: None, - owner: multisig.Signature( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - max_protocol_fee: 2_500_000, - destination: Destination { - address: addr, - datum: NoDatum, - }, - details: order.Oracle, - extension: Void, - } - let output = Output { - address: addr, - value: value.from_lovelace(2_000_000), - datum: InlineDatum(Void), - reference_script: None, - } - let ok = check_oracle(input_value, order, 2_500_000, output) - !ok -} +} \ No newline at end of file diff --git a/lib/tests/aiken/oracle.ak b/lib/tests/aiken/oracle.ak new file mode 100644 index 0000000..b22cd44 --- /dev/null +++ b/lib/tests/aiken/oracle.ak @@ -0,0 +1,73 @@ + +use aiken/transaction.{NoDatum, InlineDatum, Output} +use aiken/transaction/credential.{Address, ScriptCredential} +use aiken/transaction/value +use calculation/oracle.{check_oracle} +use types/order.{OrderDatum, Destination} +use sundae/multisig + +test oracle() { + let addr = + Address( + ScriptCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ) + let input_value = + value.from_lovelace(4_500_000) + let order = OrderDatum { + pool_ident: None, + owner: multisig.Signature( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + max_protocol_fee: 2_500_000, + destination: Destination { + address: addr, + datum: NoDatum, + }, + details: order.Oracle, + extension: Void, + } + let output = Output { + address: addr, + value: value.from_lovelace(2_000_000), + datum: NoDatum, + reference_script: None, + } + let ok = check_oracle(input_value, order, 2_500_000, output) + ok +} + +test oracle_bad_datum() { + let addr = + Address( + ScriptCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ) + let input_value = + value.from_lovelace(4_500_000) + let order = OrderDatum { + pool_ident: None, + owner: multisig.Signature( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + max_protocol_fee: 2_500_000, + destination: Destination { + address: addr, + datum: NoDatum, + }, + details: order.Oracle, + extension: Void, + } + let output = Output { + address: addr, + value: value.from_lovelace(2_000_000), + datum: InlineDatum(Void), + reference_script: None, + } + let ok = check_oracle(input_value, order, 2_500_000, output) + !ok +} From 6cf557ad8b7edc6f23e2cb0a475a5201c4fb3d77 Mon Sep 17 00:00:00 2001 From: rrruko Date: Mon, 11 Mar 2024 16:02:29 -0700 Subject: [PATCH 08/13] generalize oracle order to 'record' order --- lib/calculation/oracle.ak | 30 ------- lib/calculation/process.ak | 8 +- lib/calculation/record.ak | 32 +++++++ lib/tests/aiken/oracle.ak | 64 -------------- lib/tests/aiken/record.ak | 165 +++++++++++++++++++++++++++++++++++++ lib/types/oracle.ak | 4 +- lib/types/order.ak | 5 +- validators/oracle.ak | 71 ++++++++++++---- 8 files changed, 259 insertions(+), 120 deletions(-) delete mode 100644 lib/calculation/oracle.ak create mode 100644 lib/calculation/record.ak delete mode 100644 lib/tests/aiken/oracle.ak create mode 100644 lib/tests/aiken/record.ak diff --git a/lib/calculation/oracle.ak b/lib/calculation/oracle.ak deleted file mode 100644 index 68261f3..0000000 --- a/lib/calculation/oracle.ak +++ /dev/null @@ -1,30 +0,0 @@ -use aiken/transaction.{Output} -use aiken/transaction/value.{ada_policy_id, ada_asset_name} -use types/order.{Destination, Fixed, Self} - -pub fn check_oracle( - input: Output, - destination: Destination, - actual_protocol_fee: Int, - output: Output, -) -> Bool { - // Make sure all of the funds from the input make it into the oracle output - // Note that the accuracy of the oracle is handled by the mint policy, since it has easier - // access to the final state of the order - // TODO: this needs the token minted - let remainder = value.add(input.value, ada_policy_id, ada_asset_name, -actual_protocol_fee) - - and { - output.value == remainder, - when destination is { - Fixed(address, datum) -> and { - output.address == address, - output.datum == datum, - } - Self -> and { - output.address == input.address, - output.datum == input.datum, - } - } - } -} \ No newline at end of file diff --git a/lib/calculation/process.ak b/lib/calculation/process.ak index 1899d94..7575405 100644 --- a/lib/calculation/process.ak +++ b/lib/calculation/process.ak @@ -10,7 +10,7 @@ use aiken/transaction/value.{Value, PolicyId} use aiken/time.{PosixTime} use calculation/deposit use calculation/donation -use calculation/oracle +use calculation/record use calculation/shared.{ PoolState, check_and_set_unique, unsafe_fast_index_skip_with_tail, } as calc_shared @@ -233,13 +233,13 @@ pub fn process_order( (next, outputs) } } - order.Oracle -> { + order.Record(policy) -> { // Make sure the scooper can only take up to the max fee the user has agreed to expect [output, ..rest_outputs] = outputs let fee = amortized_base_fee + simple_fee expect max_protocol_fee >= fee - expect oracle.check_oracle(input, destination, fee, output) - (initial, rest_outputs, fee) + expect record.check_record(input, destination, fee, output, policy) + (initial, rest_outputs) } } } diff --git a/lib/calculation/record.ak b/lib/calculation/record.ak new file mode 100644 index 0000000..6e7268b --- /dev/null +++ b/lib/calculation/record.ak @@ -0,0 +1,32 @@ +use aiken/transaction.{Output} +use aiken/transaction/value.{ada_policy_id, ada_asset_name, PolicyId, AssetName} +use types/order.{Destination, Fixed, Self} + +pub fn check_record( + input: Output, + destination: Destination, + actual_protocol_fee: Int, + output: Output, + policy: (PolicyId, AssetName), +) -> Bool { + // Make sure all of the funds from the input make it into the oracle output + // Note that the accuracy of the oracle is handled by the mint policy, since it has easier + // access to the final state of the order + let remainder = input.value + |> value.add(ada_policy_id, ada_asset_name, -actual_protocol_fee) + |> value.add(policy.1st, policy.2nd, 1) + + and { + output.value == remainder, + when destination is { + // The datum of the destination can be used by the oracle minting policy + // but we don't check it here + // E.g. for a standard sundae oracle, the datum encodes the owner, and + // then the oracle minting policy checks that the scooper minted an oracle + // with the correct owner + Fixed(address, _) -> output.address == address + // It doesn't make sense for an oracle to chain into an oracle + Self -> False + } + } +} diff --git a/lib/tests/aiken/oracle.ak b/lib/tests/aiken/oracle.ak deleted file mode 100644 index bcd7ebb..0000000 --- a/lib/tests/aiken/oracle.ak +++ /dev/null @@ -1,64 +0,0 @@ - -use aiken/transaction.{NoDatum, InlineDatum, Output} -use aiken/transaction/credential.{Address, ScriptCredential} -use aiken/transaction/value -use calculation/oracle.{check_oracle} -use types/order.{Fixed} - -test oracle() { - let addr = - Address( - ScriptCredential( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - None, - ) - let input_value = - value.from_lovelace(4_500_000) - let destination = Fixed { - address: addr, - datum: NoDatum, - } - let input = Output { - address: addr, - value: input_value, - datum: InlineDatum(Void), - reference_script: None, - } - let output = Output { - address: addr, - value: value.from_lovelace(2_000_000), - datum: NoDatum, - reference_script: None, - } - check_oracle(input, destination, 2_500_000, output) -} - -test oracle_bad_datum() fail { - let addr = - Address( - ScriptCredential( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - None, - ) - let input_value = - value.from_lovelace(4_500_000) - let destination = Fixed { - address: addr, - datum: NoDatum, - } - let input = Output { - address: addr, - value: input_value, - datum: InlineDatum(Void), - reference_script: None, - } - let output = Output { - address: addr, - value: value.from_lovelace(2_000_000), - datum: InlineDatum(Void), - reference_script: None, - } - check_oracle(input, destination, 2_500_000, output) -} diff --git a/lib/tests/aiken/record.ak b/lib/tests/aiken/record.ak new file mode 100644 index 0000000..f6b964e --- /dev/null +++ b/lib/tests/aiken/record.ak @@ -0,0 +1,165 @@ +use aiken/interval +use aiken/transaction.{NoDatum, InlineDatum, Output} +use aiken/transaction/credential.{Address, VerificationKeyCredential, ScriptCredential} +use aiken/transaction/value +use calculation/record.{check_record} +use shared +use sundae/multisig +use types/oracle.{OracleDatum} as types_oracle +use types/order.{Fixed} + +test record() { + let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000" + let oracle_name = shared.oracle_sft_name() + let addr = + Address( + ScriptCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ) + let input_value = + value.from_lovelace(4_500_000) + let destination = Fixed { + address: addr, + datum: NoDatum, + } + let input = Output { + address: addr, + value: input_value, + datum: InlineDatum(Void), + reference_script: None, + } + let output = Output { + address: addr, + value: value.from_lovelace(2_000_000) + |> value.add(oracle_policy_id, oracle_name, 1), + datum: InlineDatum(OracleDatum { + owner: multisig.AnyOf([]), + valid_range: interval.between(0, 1), + pool_ident: #"00", + reserve_a: (#"00", #"00", 1_000_000), + reserve_b: (#"00", #"00", 1_000_000), + circulating_lp: (#"00", #"00", 1_000_000), + }), + reference_script: None, + } + check_record(input, destination, 2_500_000, output, (oracle_policy_id, oracle_name)) +} + +test record_must_pay_to_destination() fail { + let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000" + let oracle_name = shared.oracle_sft_name() + let addr = + Address( + ScriptCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ) + let input_value = + value.from_lovelace(4_500_000) + let destination = Fixed { + address: addr, + datum: NoDatum, + } + let input = Output { + address: addr, + value: input_value, + datum: InlineDatum(Void), + reference_script: None, + } + let output = Output { + address: Address(VerificationKeyCredential(#"00"), None), + value: value.from_lovelace(2_000_000) + |> value.add(oracle_policy_id, oracle_name, 1), + datum: InlineDatum(OracleDatum { + owner: multisig.AnyOf([]), + valid_range: interval.between(0, 1), + pool_ident: #"00", + reserve_a: (#"00", #"00", 1_000_000), + reserve_b: (#"00", #"00", 1_000_000), + circulating_lp: (#"00", #"00", 1_000_000), + }), + reference_script: None, + } + check_record(input, destination, 2_500_000, output, (oracle_policy_id, oracle_name)) +} + +test record_must_have_token() fail { + let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000" + let oracle_name = shared.oracle_sft_name() + let addr = + Address( + ScriptCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ) + let input_value = + value.from_lovelace(4_500_000) + let destination = Fixed { + address: addr, + datum: NoDatum, + } + let input = Output { + address: addr, + value: input_value, + datum: InlineDatum(Void), + reference_script: None, + } + let output = Output { + address: addr, + value: value.from_lovelace(2_000_000), + datum: InlineDatum(OracleDatum { + owner: multisig.AnyOf([]), + valid_range: interval.between(0, 1), + pool_ident: #"00", + reserve_a: (#"00", #"00", 1_000_000), + reserve_b: (#"00", #"00", 1_000_000), + circulating_lp: (#"00", #"00", 1_000_000), + }), + reference_script: None, + } + check_record(input, destination, 2_500_000, output, (oracle_policy_id, oracle_name)) +} + +test record_bad_datum() fail { + let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000" + let oracle_name = shared.oracle_sft_name() + let addr = + Address( + ScriptCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ) + let input_value = + value.from_lovelace(4_500_000) + |> value.add(oracle_policy_id, oracle_name, 1) + let destination = Fixed { + address: addr, + datum: NoDatum, + } + let input = Output { + address: addr, + value: input_value, + datum: InlineDatum(Void), + reference_script: None, + } + let output = Output { + address: addr, + value: value.from_lovelace(2_000_000) + |> value.add(oracle_policy_id, oracle_name, 1), + datum: InlineDatum(OracleDatum { + owner: multisig.AnyOf([]), + valid_range: interval.between(0, 1), + pool_ident: #"00", + reserve_a: (#"00", #"00", 1_000_000), + reserve_b: (#"00", #"00", 1_000_000), + circulating_lp: (#"00", #"00", 1_000_000), + }), + reference_script: None, + } + check_record(input, destination, 2_500_000, output, (oracle_policy_id, oracle_name)) +} diff --git a/lib/types/oracle.ak b/lib/types/oracle.ak index f0f3477..c88db35 100644 --- a/lib/types/oracle.ak +++ b/lib/types/oracle.ak @@ -16,6 +16,6 @@ pub type OracleDatum { } pub type OracleRedeemer { - Mint(Ident) + Mint(Ident, Int) Burn -} \ No newline at end of file +} diff --git a/lib/types/order.ak b/lib/types/order.ak index 72734ff..7c0a7b0 100644 --- a/lib/types/order.ak +++ b/lib/types/order.ak @@ -1,5 +1,6 @@ use aiken/transaction.{Datum, OutputReference, ValidityRange} use aiken/transaction/credential.{Address, VerificationKey, Signature, Script} +use aiken/transaction/value.{PolicyId, AssetName} use shared.{Ident, SingletonValue} use sundae/multisig.{MultisigScript} use aiken/hash.{Hash, Blake2b_224} @@ -76,7 +77,7 @@ pub type Order { /// protocols create interesting options: for example, an incentive program for their liquidity providers, etc. Donation { assets: (SingletonValue, SingletonValue) } // Create an oracle UTXO, for other protocols to read the price at a given time - Oracle + Record { policy: (PolicyId, AssetName) } } /// An order can be spent either to Scoop (execute) it, or to cancel it @@ -110,4 +111,4 @@ pub type SignedStrategyExecution { strategy: StrategyExecution, /// An ed25519 signature of the serialized `strategy` signature: Option, -} \ No newline at end of file +} diff --git a/validators/oracle.ak b/validators/oracle.ak index 6910a51..5b2dad9 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -2,11 +2,12 @@ use aiken/dict use aiken/list use aiken/hash.{Blake2b_224, Hash} use aiken/interval -use aiken/transaction.{Transaction, Output, ScriptContext, InlineDatum} -use aiken/transaction/credential.{Script, ScriptCredential} +use aiken/transaction.{Transaction, TransactionId, Input, OutputReference, Output, ScriptContext, InlineDatum} +use aiken/transaction/credential.{Address, Script, ScriptCredential} use aiken/transaction/value use sundae/multisig use shared +use types/order.{OrderDatum, Fixed} use types/pool.{PoolDatum} use types/oracle.{OracleRedeemer, OracleDatum, Mint, Burn} use tests/examples/ex_shared.{script_address, mk_tx_hash} @@ -16,7 +17,7 @@ use tests/examples/ex_shared.{script_address, mk_tx_hash} // // It's important to use the price at the *end* of the scoop, or at the beginning, rather than just using the price // "at the time" the order was processed. If we expose the pool price mid-stream, then it is easy to sandwich the order between two others. -// By using the snapshot at the end of the order, such an attacker exposes themselves to arbitrage opportunities which makes such an attack riskier. +// By using the snapshot at the end of the order, such an attacker exposes themselves to arbitrage opportunities which makes such an attack riskier. validator(pool_script_hash: Hash) { // In order to spend the oracle script, two things must be true: // - it must be signed by the "owner" @@ -47,7 +48,7 @@ validator(pool_script_hash: Hash) { // Burning an oracle token is always allowed fn mint(redeemer: OracleRedeemer, ctx: ScriptContext) { when redeemer is { - Mint(pool_ident) -> { + Mint(pool_ident, order_index) -> { expect transaction.Mint(own_policy_id) = ctx.purpose let pool_lp_name = shared.pool_lp_name(pool_ident) let pool_nft_name = shared.pool_nft_name(pool_ident) @@ -63,6 +64,12 @@ validator(pool_script_hash: Hash) { .. } = pool_datum + expect Some(oracle_order) = list.at(ctx.transaction.inputs, order_index) + expect Some(oracle_order_datum) = shared.datum_of(ctx.transaction.datums, oracle_order.output) + expect oracle_order_datum: OrderDatum = oracle_order_datum + expect owner = oracle_order_datum.extension + expect owner: multisig.MultisigScript = owner + let reserve_a = (asset_a.1st, asset_a.2nd, value.quantity_of(pool_output.value, asset_a.1st, asset_a.2nd)) let reserve_b = (asset_b.1st, asset_b.2nd, value.quantity_of(pool_output.value, asset_b.1st, asset_b.2nd)) let circulating_lp = (pool_script_hash, pool_lp_name, circulating_lp) @@ -81,6 +88,7 @@ validator(pool_script_hash: Hash) { expect oracle_datum: OracleDatum = oracle_datum expect oracle_datum.valid_range == ctx.transaction.validity_range expect oracle_datum.pool_ident == pool_ident + expect oracle_datum.owner == owner expect reserve_a == oracle_datum.reserve_a expect reserve_b == oracle_datum.reserve_b expect circulating_lp == oracle_datum.circulating_lp @@ -117,7 +125,7 @@ test oracle_burn_mint() { // Minting policy enforces that the reserves are correct -!test oracle_wrong_datum() { +test oracle_wrong_datum() fail { mint_oracle( fn(old_datum) { OracleDatum { @@ -152,6 +160,14 @@ fn mint_oracle( modify_redeemer: fn(OracleRedeemer) -> OracleRedeemer, ) { let oracle_policy_id = #"00000000000000000000000000000000000000000000000000000000" + let oracle_address = Address { + payment_credential: ScriptCredential(oracle_policy_id), + stake_credential: None, + } + let order_address = Address { + payment_credential: ScriptCredential(#"1234"), + stake_credential: None, + } let pool_script_hash = #"00000000000000000000000000000000000000000000000000000000" let pool_address = script_address(pool_script_hash) let rberry_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" @@ -176,28 +192,47 @@ fn mint_oracle( }), reference_script: None, } - let oracleMintRedeemer = modify_redeemer(Mint(pool_id)) + let my_multisig = multisig.Signature(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513") let oracle_name = modify_oracle_name(shared.oracle_sft_name()) + let oracle_order_input = Input { + output_reference: OutputReference { + transaction_id: TransactionId { hash: #"00" }, + output_index: 0, + }, + output: Output { + address: order_address, + value: value.from_lovelace(1_000_000), + datum: InlineDatum(OrderDatum { + pool_ident: None, + owner: multisig.AnyOf([]), + max_protocol_fee: 1_000_000, + destination: Fixed(oracle_address, InlineDatum(my_multisig)), + details: order.Record((oracle_policy_id, oracle_name)), + extension: my_multisig, + }), + reference_script: None, + }, + } + let oracleMintRedeemer = modify_redeemer(Mint(pool_id, 0)) let oracle_output = Output { address: script_address(oracle_policy_id), value: value.from_lovelace(1_000_000) |> value.add(oracle_policy_id, oracle_name, 1), - datum: InlineDatum(modify_oracle_datum(OracleDatum { - owner: - multisig.Signature( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - valid_range: interval.between(1, 2), - pool_ident: pool_id, - reserve_a: ("", "", 1_000_000_000), - reserve_b: (rberry_policy_id, rberry_token_name, 1_000_000_000), - circulating_lp: (pool_script_hash, pool_lp_name, 1_000_000_000), - })), + datum: InlineDatum(modify_oracle_datum( + OracleDatum { + owner: my_multisig, + valid_range: interval.between(1, 2), + pool_ident: pool_id, + reserve_a: ("", "", 1_000_000_000), + reserve_b: (rberry_policy_id, rberry_token_name, 1_000_000_000), + circulating_lp: (pool_script_hash, pool_lp_name, 1_000_000_000), + }, + )), reference_script: None, } let ctx = ScriptContext { transaction: Transaction { - inputs: [], + inputs: [oracle_order_input], outputs: [pool_output, oracle_output], reference_inputs: [], fee: value.from_lovelace(1_000_000), From 8091dd6f35d1f0b24d2ce689dd04de0960ae91e6 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Fri, 15 Mar 2024 19:50:56 -0400 Subject: [PATCH 09/13] Update aicone to support withdrawals scripts --- aiken.lock | 4 ++-- aiken.toml | 2 +- validators/oracle.ak | 1 + validators/order.ak | 1 + validators/pool.ak | 1 + validators/pool_stake.ak | 1 + validators/settings.ak | 2 ++ 7 files changed, 9 insertions(+), 3 deletions(-) diff --git a/aiken.lock b/aiken.lock index 1482efe..9c94672 100644 --- a/aiken.lock +++ b/aiken.lock @@ -8,7 +8,7 @@ source = "github" [[requirements]] name = "SundaeSwap-finance/aicone" -version = "ae0852d40cc6332437492102451cf331a3c10b0d" +version = "faca3e33f1cc7183e2f3801ee56b705883c6832e" source = "github" [[requirements]] @@ -24,7 +24,7 @@ source = "github" [[packages]] name = "SundaeSwap-finance/aicone" -version = "ae0852d40cc6332437492102451cf331a3c10b0d" +version = "faca3e33f1cc7183e2f3801ee56b705883c6832e" requirements = [] source = "github" diff --git a/aiken.toml b/aiken.toml index bb019cb..59afb2a 100644 --- a/aiken.toml +++ b/aiken.toml @@ -15,7 +15,7 @@ source = "github" [[dependencies]] name = "SundaeSwap-finance/aicone" -version = "ae0852d40cc6332437492102451cf331a3c10b0d" +version = "faca3e33f1cc7183e2f3801ee56b705883c6832e" source = "github" [[dependencies]] diff --git a/validators/oracle.ak b/validators/oracle.ak index 5b2dad9..8cb9f0b 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -33,6 +33,7 @@ validator(pool_script_hash: Hash) { datum.owner, ctx.transaction.extra_signatories, ctx.transaction.validity_range, + ctx.transaction.withdrawals, ), list.all( ctx.transaction.outputs, diff --git a/validators/order.ak b/validators/order.ak index 2a4daee..16eabfc 100644 --- a/validators/order.ak +++ b/validators/order.ak @@ -37,6 +37,7 @@ validator(stake_script_hash: Hash) { datum.owner, ctx.transaction.extra_signatories, ctx.transaction.validity_range, + ctx.transaction.withdrawals, ) } Scoop -> { diff --git a/validators/pool.ak b/validators/pool.ak index 3307db3..74524aa 100644 --- a/validators/pool.ak +++ b/validators/pool.ak @@ -264,6 +264,7 @@ validator(settings_policy_id: PolicyId) { settings_datum.treasury_admin, extra_signatories, validity_range, + withdrawals, ) // Asking the DAO to approve every single cost individually would be a small cognitive DDOS on the community diff --git a/validators/pool_stake.ak b/validators/pool_stake.ak index 370a89f..62b2848 100644 --- a/validators/pool_stake.ak +++ b/validators/pool_stake.ak @@ -39,6 +39,7 @@ validator(settings_policy_id: PolicyId, _instance: Int) { settings_datum.treasury_admin, extra_signatories, validity_range, + ctx.transaction.withdrawals, ) when purpose is { diff --git a/validators/settings.ak b/validators/settings.ak index be57a6c..eb73e36 100644 --- a/validators/settings.ak +++ b/validators/settings.ak @@ -52,6 +52,7 @@ validator(protocol_boot_utxo: OutputReference) { input_datum.settings_admin, ctx.transaction.extra_signatories, ctx.transaction.validity_range, + ctx.transaction.withdrawals, ) // Settings admin can change any datum fields except for these @@ -79,6 +80,7 @@ validator(protocol_boot_utxo: OutputReference) { input_datum.treasury_admin, ctx.transaction.extra_signatories, ctx.transaction.validity_range, + ctx.transaction.withdrawals, ) // Treasury admin can change any datum fields except for these From 314eb9221b727823a2a4ffd4df9c676e5a03a019 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Mon, 18 Mar 2024 23:13:03 -0400 Subject: [PATCH 10/13] Add some comments --- validators/oracle.ak | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/validators/oracle.ak b/validators/oracle.ak index 8cb9f0b..ccfb520 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -15,6 +15,9 @@ use tests/examples/ex_shared.{script_address, mk_tx_hash} // The oracle script holds an oracle token, and a snapshot of the pool price at the *end* of some scoop. // This allows other protocols to build integrations that read the pool price (for some confidence interval) without worrying about contention // +// In particular, this is an agnostic format; because the `record` order type just specifies the policy ID that needs to be minted, +// you could write other versions of the "Oracle" script that supported other oracle formats, like Charli3 or OrcFax. +// // It's important to use the price at the *end* of the scoop, or at the beginning, rather than just using the price // "at the time" the order was processed. If we expose the pool price mid-stream, then it is easy to sandwich the order between two others. // By using the snapshot at the end of the order, such an attacker exposes themselves to arbitrage opportunities which makes such an attack riskier. @@ -50,13 +53,19 @@ validator(pool_script_hash: Hash) { fn mint(redeemer: OracleRedeemer, ctx: ScriptContext) { when redeemer is { Mint(pool_ident, order_index) -> { + // First, find our policy ID expect transaction.Mint(own_policy_id) = ctx.purpose + + // Calculate the expected pool token names, so we can look for the pool name, and record the LP tokens let pool_lp_name = shared.pool_lp_name(pool_ident) let pool_nft_name = shared.pool_nft_name(pool_ident) + // Find the pool output, i.e. the one with the pool NFT, so we can record the prices expect Some(pool_output) = list.head(ctx.transaction.outputs) expect pool_output.address.payment_credential == ScriptCredential(pool_script_hash) expect value.quantity_of(pool_output.value, own_policy_id, pool_nft_name) == 1 + + // Then unpack the pool datum expect InlineDatum(pool_datum) = pool_output.datum expect pool_datum: PoolDatum = pool_datum let PoolDatum { @@ -65,6 +74,7 @@ validator(pool_script_hash: Hash) { .. } = pool_datum + // expect Some(oracle_order) = list.at(ctx.transaction.inputs, order_index) expect Some(oracle_order_datum) = shared.datum_of(ctx.transaction.datums, oracle_order.output) expect oracle_order_datum: OrderDatum = oracle_order_datum From 260aa6f3a2a06af98ef4b5b31b225a352755fd12 Mon Sep 17 00:00:00 2001 From: rrruko Date: Tue, 19 Mar 2024 06:01:15 -0700 Subject: [PATCH 11/13] support multiple oracle mints per scoop --- lib/types/oracle.ak | 2 +- validators/oracle.ak | 55 ++++++++++++++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/lib/types/oracle.ak b/lib/types/oracle.ak index c88db35..bbecf1f 100644 --- a/lib/types/oracle.ak +++ b/lib/types/oracle.ak @@ -16,6 +16,6 @@ pub type OracleDatum { } pub type OracleRedeemer { - Mint(Ident, Int) + Mint(Ident, List) Burn } diff --git a/validators/oracle.ak b/validators/oracle.ak index ccfb520..2f82ab3 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -52,8 +52,7 @@ validator(pool_script_hash: Hash) { // Burning an oracle token is always allowed fn mint(redeemer: OracleRedeemer, ctx: ScriptContext) { when redeemer is { - Mint(pool_ident, order_index) -> { - // First, find our policy ID + Mint(pool_ident, order_indices) -> { expect transaction.Mint(own_policy_id) = ctx.purpose // Calculate the expected pool token names, so we can look for the pool name, and record the LP tokens @@ -74,26 +73,31 @@ validator(pool_script_hash: Hash) { .. } = pool_datum - // - expect Some(oracle_order) = list.at(ctx.transaction.inputs, order_index) - expect Some(oracle_order_datum) = shared.datum_of(ctx.transaction.datums, oracle_order.output) - expect oracle_order_datum: OrderDatum = oracle_order_datum - expect owner = oracle_order_datum.extension - expect owner: multisig.MultisigScript = owner - let reserve_a = (asset_a.1st, asset_a.2nd, value.quantity_of(pool_output.value, asset_a.1st, asset_a.2nd)) let reserve_b = (asset_b.1st, asset_b.2nd, value.quantity_of(pool_output.value, asset_b.1st, asset_b.2nd)) let circulating_lp = (pool_script_hash, pool_lp_name, circulating_lp) let oracle_name = shared.oracle_sft_name() - list.all( + // For each output that produces an oracle, there should be an oracle + // order in the inputs given by the nth item of the order_indices list + // in the redeemer + let (_, no_duplicate_minted_oracles) = list.foldl( ctx.transaction.outputs, - fn(output) { + (0, True), + fn(output, state) { + let (oracle_minted_index, no_duplicates) = state let qty = value.quantity_of(output.value, own_policy_id, oracle_name) when qty is { - 0 -> True + 0 -> (oracle_minted_index, no_duplicates) 1 -> { + expect Some(this_order_index) = list.at(order_indices, oracle_minted_index) + expect Some(oracle_order) = list.at(ctx.transaction.inputs, this_order_index) + expect Some(oracle_order_datum) = shared.datum_of(ctx.transaction.datums, oracle_order.output) + expect oracle_order_datum: OrderDatum = oracle_order_datum + expect owner = oracle_order_datum.extension + expect owner: multisig.MultisigScript = owner + expect output.address.payment_credential == ScriptCredential(own_policy_id) expect Some(oracle_datum) = shared.datum_of(ctx.transaction.datums, output) expect oracle_datum: OracleDatum = oracle_datum @@ -103,12 +107,13 @@ validator(pool_script_hash: Hash) { expect reserve_a == oracle_datum.reserve_a expect reserve_b == oracle_datum.reserve_b expect circulating_lp == oracle_datum.circulating_lp - True + (oracle_minted_index + 1, no_duplicates) } - _ -> False + _ -> (0, False) } } ) + no_duplicate_minted_oracles } Burn -> True } @@ -133,7 +138,27 @@ test oracle_burn_mint() { ) } +test oracle_redeemer_indices_can_have_extras() { + let pool_id = #"00" + mint_oracle( + identity, + identity, + fn(_) { + Mint(pool_id, [0, 999, -1]) + } + ) +} +test oracle_redeemer_indices_must_be_valid() fail { + let pool_id = #"00" + mint_oracle( + identity, + identity, + fn(_) { + Mint(pool_id, [1]) + } + ) +} // Minting policy enforces that the reserves are correct test oracle_wrong_datum() fail { @@ -224,7 +249,7 @@ fn mint_oracle( reference_script: None, }, } - let oracleMintRedeemer = modify_redeemer(Mint(pool_id, 0)) + let oracleMintRedeemer = modify_redeemer(Mint(pool_id, [0])) let oracle_output = Output { address: script_address(oracle_policy_id), value: value.from_lovelace(1_000_000) From 079171f8feaf5d38977f36110b8399ec30cde3d2 Mon Sep 17 00:00:00 2001 From: rrruko Date: Tue, 19 Mar 2024 06:10:16 -0700 Subject: [PATCH 12/13] test txes minting multiple oracles at once --- validators/oracle.ak | 61 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/validators/oracle.ak b/validators/oracle.ak index 2f82ab3..6e550e5 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -144,18 +144,18 @@ test oracle_redeemer_indices_can_have_extras() { identity, identity, fn(_) { - Mint(pool_id, [0, 999, -1]) + Mint(pool_id, [0, 1, 999, -1]) } ) } -test oracle_redeemer_indices_must_be_valid() fail { +test oracle_redeemer_indices_must_match_up() fail { let pool_id = #"00" mint_oracle( identity, identity, fn(_) { - Mint(pool_id, [1]) + Mint(pool_id, [1, 0]) } ) } @@ -228,9 +228,10 @@ fn mint_oracle( }), reference_script: None, } - let my_multisig = multisig.Signature(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513") + let user_1_multisig = multisig.Signature(#"01") + let user_2_multisig = multisig.Signature(#"02") let oracle_name = modify_oracle_name(shared.oracle_sft_name()) - let oracle_order_input = Input { + let oracle_order_input_1 = Input { output_reference: OutputReference { transaction_id: TransactionId { hash: #"00" }, output_index: 0, @@ -242,21 +243,57 @@ fn mint_oracle( pool_ident: None, owner: multisig.AnyOf([]), max_protocol_fee: 1_000_000, - destination: Fixed(oracle_address, InlineDatum(my_multisig)), + destination: Fixed(oracle_address, InlineDatum(multisig.AnyOf([]))), details: order.Record((oracle_policy_id, oracle_name)), - extension: my_multisig, + extension: user_1_multisig, }), reference_script: None, }, } - let oracleMintRedeemer = modify_redeemer(Mint(pool_id, [0])) - let oracle_output = Output { + let oracle_order_input_2 = Input { + output_reference: OutputReference { + transaction_id: TransactionId { hash: #"00" }, + output_index: 1, + }, + output: Output { + address: order_address, + value: value.from_lovelace(1_000_000), + datum: InlineDatum(OrderDatum { + pool_ident: None, + owner: multisig.AnyOf([]), + max_protocol_fee: 1_000_000, + destination: Fixed(oracle_address, InlineDatum(multisig.AnyOf([]))), + details: order.Record((oracle_policy_id, oracle_name)), + extension: user_2_multisig, + }), + reference_script: None, + }, + } + + let oracleMintRedeemer = modify_redeemer(Mint(pool_id, [0, 1])) + let oracle_output_1 = Output { + address: script_address(oracle_policy_id), + value: value.from_lovelace(1_000_000) + |> value.add(oracle_policy_id, oracle_name, 1), + datum: InlineDatum(modify_oracle_datum( + OracleDatum { + owner: user_1_multisig, + valid_range: interval.between(1, 2), + pool_ident: pool_id, + reserve_a: ("", "", 1_000_000_000), + reserve_b: (rberry_policy_id, rberry_token_name, 1_000_000_000), + circulating_lp: (pool_script_hash, pool_lp_name, 1_000_000_000), + }, + )), + reference_script: None, + } + let oracle_output_2 = Output { address: script_address(oracle_policy_id), value: value.from_lovelace(1_000_000) |> value.add(oracle_policy_id, oracle_name, 1), datum: InlineDatum(modify_oracle_datum( OracleDatum { - owner: my_multisig, + owner: user_2_multisig, valid_range: interval.between(1, 2), pool_ident: pool_id, reserve_a: ("", "", 1_000_000_000), @@ -268,8 +305,8 @@ fn mint_oracle( } let ctx = ScriptContext { transaction: Transaction { - inputs: [oracle_order_input], - outputs: [pool_output, oracle_output], + inputs: [oracle_order_input_1, oracle_order_input_2], + outputs: [pool_output, oracle_output_1, oracle_output_2], reference_inputs: [], fee: value.from_lovelace(1_000_000), mint: value.to_minted_value( From f0158f8bdb6cb779581acdc18f9c67b139cea3ec Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Sun, 31 Mar 2024 03:08:39 -0400 Subject: [PATCH 13/13] Add some documentation, clearer name --- lib/calculation/record.ak | 10 ++++++++-- lib/types/order.ak | 3 ++- validators/oracle.ak | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/calculation/record.ak b/lib/calculation/record.ak index 6e7268b..32a6550 100644 --- a/lib/calculation/record.ak +++ b/lib/calculation/record.ak @@ -7,14 +7,20 @@ pub fn check_record( destination: Destination, actual_protocol_fee: Int, output: Output, - policy: (PolicyId, AssetName), + asset_id: (PolicyId, AssetName), ) -> Bool { // Make sure all of the funds from the input make it into the oracle output // Note that the accuracy of the oracle is handled by the mint policy, since it has easier // access to the final state of the order + + // Theoretically, this could be satisfied if someone specified an asset_id / policy_id that + // came from another input, rather than one that had to be minted here; but that would be on them + // and maybe there's a good reason for someone to want that behavior. + // We know that the `oracle` token we provide is secure against this, because it must be minted into + // its own spending policy, and it must be burned when spent. let remainder = input.value |> value.add(ada_policy_id, ada_asset_name, -actual_protocol_fee) - |> value.add(policy.1st, policy.2nd, 1) + |> value.add(asset_id.1st, asset_id.2nd, 1) and { output.value == remainder, diff --git a/lib/types/order.ak b/lib/types/order.ak index 7c0a7b0..79e3404 100644 --- a/lib/types/order.ak +++ b/lib/types/order.ak @@ -76,7 +76,8 @@ pub type Order { /// Donate some value to the pool; Likely not useful for an end user, but lets other /// protocols create interesting options: for example, an incentive program for their liquidity providers, etc. Donation { assets: (SingletonValue, SingletonValue) } - // Create an oracle UTXO, for other protocols to read the price at a given time + // Record, like the verb; let some script witness a scoop, and enforce some property about the output + // Used, for example, to record the state of the pool for an on-chain oracle, etc. Record { policy: (PolicyId, AssetName) } } diff --git a/validators/oracle.ak b/validators/oracle.ak index 6e550e5..c3164ce 100644 --- a/validators/oracle.ak +++ b/validators/oracle.ak @@ -26,7 +26,8 @@ validator(pool_script_hash: Hash) { // - it must be signed by the "owner" // - there must be no oracle tokens on the outputs // This allows reclaiming funds that were accidentally locked at the script address, - // while also enforcing that the oracle token is burned + // while also enforcing that the oracle token is burned; this is important, as people + // will be relying on the oracle token to authenticate the actual values fn spend(datum: Data, _r: Data, ctx: ScriptContext) -> Bool { let own_input = shared.spent_output(ctx) expect ScriptCredential(own_script_hash) = own_input.address.payment_credential