diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc04b6d..030110d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,33 @@ jobs: - uses: aiken-lang/setup-aiken@v0.1.0 with: - version: v1.0.20-alpha + version: v1.0.24-alpha - - run: aiken check + - run: | + # Run the tests + set -o pipefail + RESULT=0 + aiken check 2>&1 | tee aiken.log || RESULT=$? + if [ $RESULT -ne 0 ]; then + { + echo 'FAILING_TESTS<> "$GITHUB_ENV" + cat $GITHUB_ENV + exit $RESULT + fi + - if: failure() + run: | + echo "$FAILING_TESTS" - run: aiken build + - uses: actions/github-script@v6 + if: failure() && env.FAILING_TESTS != '' + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `Tests failed:\n\n\`\`\`\n${{ env.FAILING_TESTS }}\n\`\`\`` + }) \ No newline at end of file diff --git a/aiken.lock b/aiken.lock index c86c914..1482efe 100644 --- a/aiken.lock +++ b/aiken.lock @@ -11,6 +11,11 @@ name = "SundaeSwap-finance/aicone" version = "ae0852d40cc6332437492102451cf331a3c10b0d" source = "github" +[[requirements]] +name = "aiken-extra/tx_util" +version = "1.170.202312" +source = "github" + [[packages]] name = "aiken-lang/stdlib" version = "97cd61345bcc8925c521b30d0f354859eb0148cd" @@ -23,4 +28,10 @@ version = "ae0852d40cc6332437492102451cf331a3c10b0d" requirements = [] source = "github" +[[packages]] +name = "aiken-extra/tx_util" +version = "1.170.202312" +requirements = [] +source = "github" + [etags] diff --git a/aiken.toml b/aiken.toml index 8a78860..bb019cb 100644 --- a/aiken.toml +++ b/aiken.toml @@ -17,3 +17,8 @@ source = "github" name = "SundaeSwap-finance/aicone" version = "ae0852d40cc6332437492102451cf331a3c10b0d" source = "github" + +[[dependencies]] +name = "aiken-extra/tx_util" +version = "1.170.202312" +source = "github" diff --git a/lib/calculation/donation.ak b/lib/calculation/donation.ak index a1d7b4e..1072f81 100644 --- a/lib/calculation/donation.ak +++ b/lib/calculation/donation.ak @@ -1,10 +1,8 @@ -use aiken/transaction.{NoDatum, Output} -use aiken/transaction/credential.{Address, VerificationKeyCredential} +use aiken/transaction.{Output} 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} /// 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 @@ -29,19 +27,19 @@ 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 ((asset_a_policy_id, asset_a_asset_name, asset_a_qty), (asset_b_policy_id, asset_b_asset_name, asset_b_qty)) = assets // 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 - expect assets.1st.2nd == pool_state.quantity_a.2nd - expect assets.2nd.1st == pool_state.quantity_b.1st - expect assets.2nd.2nd == pool_state.quantity_b.2nd + expect asset_a_policy_id == pool_state.quantity_a.1st + expect asset_a_asset_name == pool_state.quantity_a.2nd + expect asset_b_policy_id == pool_state.quantity_b.1st + expect asset_b_asset_name == pool_state.quantity_b.2nd // Compute however much of the UTXO value is *left over* after deducting the donation amount from it; If nonzero, this will need to be returned to the user let remainder = - shared.to_value(assets.1st) - |> value.merge(shared.to_value(assets.2nd)) - |> value.add(ada_policy_id, ada_asset_name, actual_protocol_fee) - |> value.negate - |> value.merge(input_value) + input_value + |> value.add(ada_policy_id, ada_asset_name, -actual_protocol_fee) + |> value.add(asset_a_policy_id, asset_a_asset_name, -asset_a_qty) + |> value.add(asset_b_policy_id, asset_b_asset_name, -asset_b_qty) let has_remainder = remainder != value.zero() // If we have a remainder, then we need to check the details of the output; this awkward structure @@ -76,57 +74,3 @@ pub fn do_donation( has_remainder, ) } - -test donation() { - let addr = - Address( - VerificationKeyCredential( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - None, - ) - let ada = (#"", #"") - let rberry = - (#"01010101010101010101010101010101010101010101010101010101", "RBERRY") - let lp = (#"99999999999999999999999999999999999999999999999999999999", "LP") - let pool_state = - PoolState { - quantity_a: (#"", #"", 1_000_000_000), - quantity_b: (rberry.1st, rberry.2nd, 1_000_000_000), - quantity_lp: (lp.1st, lp.2nd, 1_000_000_000), - } - let input_value = - value.from_lovelace(3_500_000) - |> value.add(rberry.1st, rberry.2nd, 1_000_000) - let assets = ( - (ada.1st, ada.2nd, 1_000_000), - (rberry.1st, rberry.2nd, 1_000_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.Donation { - assets: assets, - }, - extension: Void, - } - // There's no remainder so do_donation totally ignores this Output record - let output = - Output { - address: addr, - value: value.from_lovelace(999_999_999_999_999_999), - datum: NoDatum, - reference_script: None, - } - let (final_pool_state, has_remainder) = - do_donation(pool_state, input_value, 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 - True -} diff --git a/lib/shared.ak b/lib/shared.ak index 82d6bb9..95f8225 100644 --- a/lib/shared.ak +++ b/lib/shared.ak @@ -10,6 +10,10 @@ use aiken/transaction.{ use aiken/transaction/credential.{Credential, ScriptCredential} use aiken/transaction/value.{AssetName, PolicyId} +use tests/examples/ex_shared.{ + script_address, wallet_address, mk_output_reference +} + /// An alias type for the pool identifier, to make it slightly easier to reason about pub type Ident = ByteArray @@ -112,20 +116,33 @@ pub fn is_script(credential: Credential) -> Bool { /// without knowing the exact script address they will use /// This is quite subtle, so see the note above pub fn count_orders(tx_inputs: List) -> Int { - // Can be done as a simple fold - list.foldl( - tx_inputs, - // Note: by starting at -exact_non_order_script_inputs, it's the same as if we subtracted this at the end, - // but saves one mathematical operation. - -exact_non_order_script_inputs, - fn(input, total) { + when tx_inputs is { + // Note: by using -exact_non_order_script_inputs for the base case, + // it's equivalent to subtracting at the end + [] -> -exact_non_order_script_inputs + [input, ..rest] -> // The reason this is important is twofold: // - We count them in the first place to ensure that we process every order by the end of the transaction // - and we compare the script this way so that if we implement a new order script, with additional logic, // and the correct datum format, it can be processed the pool just fine - total + if is_script(input.output.address.payment_credential) { 1 } else { 0 } - }, - ) + when input.output.address.payment_credential is { + ScriptCredential(_) -> count_orders(rest) + 1 + _ -> count_orders(rest) + } + } +} + +// Taken from unmerged PR: https://github.com/aiken-lang/stdlib/pull/73/files +pub fn is_sqrt(self: Int, x: Int) -> Bool { + x * x <= self && ( x + 1 ) * ( x + 1 ) > self +} + +test is_sqrt1() { + is_sqrt(44203, 210) +} + +test is_sqrt2() { + is_sqrt(975461057789971041, 987654321) } /// Check whether a specific value has *exactly* the right amount of tokens @@ -192,4 +209,61 @@ pub fn pool_token_names(pool_ident: Ident) { pool_nft_name(pool_ident), pool_lp_name(pool_ident), ) -} \ No newline at end of file +} + +// Test for count_orders, with 16 inputs of which 10 are orders. +test count_orders_test() { + let hash_of_pool_script = + #"00000000000000000000000000000000000000000000000000000000" + let pool_address = script_address(hash_of_pool_script) + let pool_input = + Input { + output_reference: mk_output_reference(0), + output: Output { + address: pool_address, + value: value.from_lovelace(0), + datum: NoDatum, + reference_script: None, + }, + } + + let hash_of_escrow_script = + #"00000000000000000000000000000000000000000000000000000000" + let escrow_address = script_address(hash_of_escrow_script) + let escrow1_in = Input { + output_reference: mk_output_reference(2), + output: Output { + address: escrow_address, + value: value.from_lovelace(0), + datum: NoDatum, + reference_script: None, + }, + } + let escrow2_in = Input { + output_reference: mk_output_reference(3), + output: Output { + address: escrow_address, + value: value.from_lovelace(0), + datum: NoDatum, + reference_script: None, + }, + } + let other_address = wallet_address(#"fede0000000000000000000000000000000000000000000000000000") + let other_in = Input { + output_reference: mk_output_reference(4), + output: Output { + address: other_address, + value: value.from_lovelace(0), + datum: NoDatum, + reference_script: None, + }, + } + + let inputs = [ + pool_input, other_in, escrow1_in, escrow2_in, other_in, escrow1_in, + escrow2_in, other_in, escrow1_in, escrow2_in, other_in, escrow1_in, + escrow2_in, other_in, escrow1_in, escrow2_in + ] + + count_orders(inputs) == 10 +} diff --git a/lib/tests/aiken/donation.ak b/lib/tests/aiken/donation.ak new file mode 100644 index 0000000..268e5c8 --- /dev/null +++ b/lib/tests/aiken/donation.ak @@ -0,0 +1,61 @@ +use aiken/transaction.{NoDatum, Output} +use aiken/transaction/credential.{Address, VerificationKeyCredential} +use aiken/transaction/value +use calculation/shared.{PoolState} as calc_shared +use calculation/donation.{do_donation} +use sundae/multisig +use types/order.{Destination, OrderDatum} + +test donation() { + let addr = + Address( + VerificationKeyCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ) + let ada = (#"", #"") + let rberry = + (#"01010101010101010101010101010101010101010101010101010101", "RBERRY") + let lp = (#"99999999999999999999999999999999999999999999999999999999", "LP") + let pool_state = + PoolState { + quantity_a: (#"", #"", 1_000_000_000), + quantity_b: (rberry.1st, rberry.2nd, 1_000_000_000), + quantity_lp: (lp.1st, lp.2nd, 1_000_000_000), + } + let input_value = + value.from_lovelace(3_500_000) + |> value.add(rberry.1st, rberry.2nd, 1_000_000) + let assets = ( + (ada.1st, ada.2nd, 1_000_000), + (rberry.1st, rberry.2nd, 1_000_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.Donation { + assets: assets, + }, + extension: Void, + } + // There's no remainder so do_donation totally ignores this Output record + let output = + Output { + address: addr, + value: value.from_lovelace(999_999_999_999_999_999), + datum: NoDatum, + reference_script: None, + } + let (final_pool_state, has_remainder) = + do_donation(pool_state, input_value, 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 + True +} diff --git a/validators/pool.ak b/validators/pool.ak index a93ac2c..4585530 100644 --- a/validators/pool.ak +++ b/validators/pool.ak @@ -1,31 +1,25 @@ -use aiken/builtin use aiken/bytearray use aiken/dict use aiken/hash use aiken/interval use aiken/list -use aiken/math -use aiken/option use aiken/transaction.{ - Datum, InlineDatum, Input, Mint, NoDatum, Output, OutputReference, - ScriptContext, Spend, Transaction, TransactionId, + InlineDatum, Input, Mint, Output, OutputReference, + ScriptContext, Transaction, TransactionId, } use aiken/transaction/credential.{ - Address, ScriptCredential, VerificationKeyCredential, Inline + Address, Inline, ScriptCredential, } use aiken/transaction/value.{MintedValue, PolicyId, Value, ada_policy_id, ada_asset_name} use calculation/process.{pool_input_to_state, process_orders} use calculation/shared.{PoolState} as calc_shared use shared.{AssetClass, Ident, spent_output, pool_nft_name, pool_lp_name, count_orders} use sundae/multisig -use tests/examples/ex_settings.{mk_valid_settings_input} -use tests/examples/ex_shared.{wallet_address, script_address, mk_output_reference, mk_tx_hash} -use types/order.{Destination, OrderDatum, Swap, Deposit} use types/pool.{ - CreatePool, MintLP, PoolDatum, PoolMintRedeemer, PoolRedeemer, PoolScoop, WithdrawFees + CreatePool, MintLP, PoolDatum, PoolMintRedeemer, PoolRedeemer, PoolScoop, + WithdrawFees, } use types/settings.{SettingsDatum, find_settings_datum} - /// The core / base "pooled AMM" script for the SundaeSwap v3 protocol /// /// Parameterized by the Settings policy ID, which makes the script unique, as well as lets us validate / read global settings. @@ -53,7 +47,7 @@ use types/settings.{SettingsDatum, find_settings_datum} /// /// A + B*n + C < (A + B) * n validator(settings_policy_id: PolicyId) { - fn spend(datum: PoolDatum, redeemer: PoolRedeemer, ctx: ScriptContext) { + pub fn spend(datum: PoolDatum, redeemer: PoolRedeemer, ctx: ScriptContext) { // First, we destructure the transaction right upfront, because field access is O(n), // and we want access to these fields with just a single pass over the transaction // This will be a common pattern throughout the scripts @@ -86,6 +80,13 @@ validator(settings_policy_id: PolicyId) { expect InlineDatum(output_datum) = pool_output.datum expect output_datum: PoolDatum = output_datum + // Ensure that the pool output is to the same payment credential; This is critical, because it ensures that the pool NFT + // or liquidity aren't paid to some other script in control of an attacker. + // Note that we check the stake credential is correctly updated (or not) in the various redeemer cases below. + // We also check that the pool output has the correct output, which ensures it contains the pool NFT, + // meaning this can't just be a "token output" with the correct payment credential, but everything paid elsewhere. + expect pool_output.address.payment_credential == ScriptCredential(pool_script_hash) + // Similarly, destructure the pool datum we found on the output, to access the fields we need to process the scoop let PoolDatum { identifier: actual_identifier, @@ -215,7 +216,6 @@ validator(settings_policy_id: PolicyId) { outcome, actual_protocol_fees, ) - // Now, we check various things about the output datum to ensure they're each correct. // Check that the datum correctly records the final circulating LP, accounting for any deposits and withdrawals // In particular, this is important because that circulating supply is exaclty what determines the users ownership of assets in the pool @@ -230,16 +230,12 @@ validator(settings_policy_id: PolicyId) { datum.assets == output_datum.assets, datum.fees_per_10_thousand == output_datum.fees_per_10_thousand, datum.market_open == output_datum.market_open, - // Finally, make sure we don't change the stake credential; this can only be done when withdrawing fees, by the treasury administrator pool_input.address.stake_credential == pool_output.address.stake_credential, } } WithdrawFees { amount, treasury_output } -> { - let PoolDatum { - protocol_fees: initial_protocol_fees, - .. - } = datum + let PoolDatum { protocol_fees: initial_protocol_fees, .. } = datum // Make sure we withdraw *only* up to what we've earned // We allow less than, so that you can leave some behind for the minUTXO cost, or continuing to earn staking rewards, etc. expect amount <= initial_protocol_fees @@ -289,7 +285,6 @@ validator(settings_policy_id: PolicyId) { // that worked with the void datum, and paid to the real treasury with the correct datum. // TODO: should we just let the treasury admin specify the datum on the redeemer? Or include it in the settings? expect treasury_output.datum == InlineDatum(Void) - expect pool_output.address.payment_credential == pool_input.address.payment_credential expect list.any(settings_datum.authorized_staking_keys, fn(a) { pool_output.address.stake_credential == Some(Inline(a)) }) @@ -300,7 +295,7 @@ validator(settings_policy_id: PolicyId) { } } - fn mint(r: PoolMintRedeemer, ctx: ScriptContext) { + pub fn mint(r: PoolMintRedeemer, ctx: ScriptContext) { // When minting, we can be doing one of two things: minting the pool itself, or minting the LP token when r is { // For creating a new pool, one of our design objectives was to avoid requiring interaction with any global @@ -388,11 +383,14 @@ validator(settings_policy_id: PolicyId) { coin_a_amt } - // Calculate the initial number of LP tokens; In particular, we adopt Uniswaps convention of - // computing this value as the square root of the product of the two values. + // Check that the quantity of LP tokens is correct; In particular, we adopt Uniswaps convention of + // using the sqrt of the product of the two values for the initial number of LP tokens to mint.. // This helps minimize precision loss: it gives decent initial liquidity values for a range of // sizes of pools, such that an individual LP token is granular enough for depositing and withdrawing for most users. - expect Some(initial_lq) = math.sqrt(coin_a_amt_sans_protocol_fees * coin_b_amt) + // In particular, though, we don't calculate the sqrt here, which is an expensive function; we instead verify that the + // amount minted is valid by checking that it squares to the correct product + let initial_lq = pool_output_datum.circulating_lp + expect shared.is_sqrt(coin_a_amt_sans_protocol_fees * coin_b_amt, initial_lq) // And check that we mint the correct tokens, and nothing else. let expected_mint = @@ -414,7 +412,7 @@ validator(settings_policy_id: PolicyId) { new_pool_nft_token, ) == 1 } - + // Make sure we send the pool metadata token to the metadata admin // We use an index from the redeemer to skip to the right output, in case there are multiple outputs to the metadata admin // This is safe to do for the usual reasons: if they point at a UTXO without the ref token, the transaction will fail. @@ -422,7 +420,13 @@ validator(settings_policy_id: PolicyId) { list.at(ctx.transaction.outputs, metadata_output_ix) expect metadata_output.address == settings_datum.metadata_admin expect value.quantity_of(metadata_output.value, own_policy_id, new_pool_ref_token) == 1 + // We also check that the datum on the metadata output is void; It would be complex and in-flexible to enforce any particular structure on this, so we + // instead leave it to the metadata admin to spend the output and provide it the correct datum; We also don't want to leave it unspecified, because + // 1) the metadata admin might actually be a script address, in which case having no datum will permanently lock the metadata + // 2) the pool minter might include malicious metadata, such as an icon pointing at hardcore porn; until the metadata admin spent it, this would appear in users wallets, + // and potentially even on access UIs for the Sundae protocol + expect metadata_output.datum == InlineDatum(Void) // And check that the datum is initialized correctly; This is part of why we have a minting policy handling this, // as it allows us to authenticate the providence of the datum. @@ -439,6 +443,13 @@ validator(settings_policy_id: PolicyId) { pool_output_datum.market_open <= pool_output_datum.fee_finalized, } + // Make sure that the pool output is paid into own_policy_id (the pool script, remember this is a multivalidator) + // and that one of the valid staking addresses is attached + expect pool_output.address.payment_credential == ScriptCredential(own_policy_id) + expect list.any(settings_datum.authorized_staking_keys, fn(a) { + pool_output.address.stake_credential == Some(Inline(a)) + }) + // And then check each of the conditions above as the condition for minting and { coin_pair_ordering_is_canonical, @@ -447,27 +458,29 @@ validator(settings_policy_id: PolicyId) { pool_output_datum_correct, } } - // when minting an LP token, we just need to make sure the pool NFT is present in one of the inputs, - // meaning the pool script will enforce the correct name and quantity. - // - // Of particular note, you might expect this to fail when minting the initial LP tokens - // but the minting policy only runs once, so it would be running with a different redeemer in that case - // And it's not possible to include a *separate* minting redeemer to run the script twice; (TODO: check this) - // even if it were, the pool token wouldn't be on the **inuputs** when we're minting the pool. + // When minting an LP token, we just need to make sure the pool script is being spent, as it will enforce the correct + // name and quantity of the LP tokens. // - // It's also important that the pool script and the minting script checks that *no other* tokens of this policy are minted, - // for example for a different pool. It should be ok if a token from a *different* policy is minted, though. + // To do that, we could check for the pool NFT on the inputs, but this is expensive, especially if the pool input ends up being one of the last. + // So instead we check that the pool NFT is in the first output (this is safe to assume because it's unique, and if it's in any other output it will fail) + // and that we're not minting the pool token (i.e. someone could "pretend" to mint LP tokens, but also mint the pool token to make it look like a scoop) + // + // So, lets enumerate the possible cases: + // - We use the CreatePool redeemer; this checks that *only* the correct pool token and correct number of LP tokens are minted + // - We use the MintLP redeemer; this checks that the pool token (which is unique and locked in the pool script) is in the outputs, and not minted + // - the pool script checks that only the correct number of LP tokens, and nothing else under this policy ID, are minted + // And the impossible cases: + // - During CreatePool, it would be impossible to mint multiple of the same pool tokens; a different pool token; a different number of LP tokens; or a different pool's LP tokens + // - During MintLP, it would be impossible to mint the relevant pool token; thus, the pool script must run, and thus it will be impossible to mint another pool token, a different pool + // ident pool token, a different quantity of LP tokens, or a different pools LP tokens MintLP(pool_ident) -> { expect Mint(own_policy_id) = ctx.purpose let pool_nft_name = shared.pool_nft_name(pool_ident) - let allows_to_spend = - fn(v) { - value.quantity_of(v, own_policy_id, pool_nft_name) == 1 - } - list.any( - ctx.transaction.inputs, - fn(input) { allows_to_spend(input.output.value) }, - ) + expect Some(pool_output) = list.head(ctx.transaction.outputs) + and { + (pool_output.value |> value.quantity_of(own_policy_id, pool_nft_name)) == 1, + (ctx.transaction.mint |> value.from_minted_value |> value.quantity_of(own_policy_id, pool_nft_name)) == 0, + } } } } @@ -503,7 +516,7 @@ fn minted_correct_pool_tokens( /// Check that the UTXO contents are correct given a specific pool outcome /// In particular, it must have the final A reserves, the final B reserves, the pool NFT, and the protocol fees -fn has_expected_pool_value( +pub fn has_expected_pool_value( pool_script_hash: PolicyId, identifier: Ident, output_value: Value, @@ -516,6 +529,8 @@ fn has_expected_pool_value( // Asset A *could* be ADA; in which case there should be 3 tokens on the output // (ADA, Asset B, and the NFT) if quantity_a_policy_id == ada_policy_id { + // Rather than constructing a value directly (which can be expensive) + // we can just compare the expected token count and amounts with a single pass over the value (3, final_protocol_fees + quantity_a_amt, quantity_b_amt, 1) == list.foldl( value.flatten(output_value), @@ -534,6 +549,8 @@ fn has_expected_pool_value( } ) } else { + // Asset A isn't ADA, Asset B will *never* be ADA; in this case, there should be 4 tokens on the output: + // ADA, the Pool NFT, Asset A, and Asset B (4, final_protocol_fees, quantity_a_amt, quantity_b_amt, 1) == list.foldl( value.flatten(output_value), @@ -568,648 +585,7 @@ fn compare_asset_class(a: AssetClass, b: AssetClass) { } // Convert a specific integer (like a UTXO index) into a byte array, so we can construct a hashable string when minting the pool -fn int_to_ident(n: Int) -> Ident { +pub fn int_to_ident(n: Int) -> Ident { expect n < 256 bytearray.push(#"", n) -} - -// Tests - -type ScoopTestOptions { - edit_escrow_1_value: Option, - edit_escrow_2_value: Option, - edit_escrow_destination: Option
, - edit_fee: Option, - edit_swap_fees: Option<(Int,Int)>, - edit_pool_output_value: Option, - edit_settings_datum: Option, -} - -fn default_scoop_test_options() -> ScoopTestOptions { - ScoopTestOptions { - edit_escrow_1_value: None, - edit_escrow_2_value: None, - edit_escrow_destination: None, - edit_fee: None, - edit_swap_fees: None, - edit_pool_output_value: None, - edit_settings_datum: None, - } -} - -test ok_scoop_swap_swap() { - let options = default_scoop_test_options() - scoop(options) -} - -test ok_scoop_swap_deposit() { - let options = default_scoop_test_options() - scoop_swap_deposit(options) -} - -test scoop_bad_destination() fail { - let burn_addr = - wallet_address(#"12000000000000000000000000000000000000000000000000000000") - let options = - ScoopTestOptions { - ..default_scoop_test_options(), - edit_escrow_destination: Some(burn_addr), - } - scoop(options) -} - -test scoop_payouts_swapped() fail { - let dummy_policy_id = - #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" - let dummy_asset_name = #"53554e444145" - let options = - ScoopTestOptions { - ..default_scoop_test_options(), - edit_escrow_1_value: Some( - value.from_lovelace(2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 9_702_095), - ), - edit_escrow_2_value: Some( - value.from_lovelace(2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 9_896_088), - ), - } - scoop(options) -} - -test pool_validator_ignores_fee() { - let options = - ScoopTestOptions { - ..default_scoop_test_options(), - edit_fee: Some(value.from_lovelace(1_000_000_000)), - } - scoop(options) -} - -test scoop_high_swap_fees() { - let hash_of_pool_script = - #"00000000000000000000000000000000000000000000000000000000" - let pool_id = #"00000000000000000000000000000000000000000000000000000000" - let pool_nft_name = shared.pool_nft_name(pool_id) - let dummy_policy_id = - #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" - let dummy_asset_name = #"53554e444145" - let swap_fee = 100 - let options = - ScoopTestOptions { - ..default_scoop_test_options(), - edit_swap_fees: Some((swap_fee, swap_fee)), - edit_escrow_1_value: Some( - value.from_lovelace(2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 9_802_950), - ), - edit_escrow_2_value: Some( - value.from_lovelace(2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 9_611_678), - ), - edit_pool_output_value: Some( - value.from_lovelace(1_000_000_000 + 20_000_000 + 5_000_000 + 2_000_000) - |> value.add( - dummy_policy_id, - dummy_asset_name, - 1_000_000_000 - ( 9_802_950 + 9_611_678 ), - ) - |> value.add(hash_of_pool_script, pool_nft_name, 1), - ), - } - scoop(options) -} - -test output_missing_nft() fail { - let dummy_policy_id = - #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" - let dummy_asset_name = #"53554e444145" - let options = - ScoopTestOptions { - ..default_scoop_test_options(), - edit_pool_output_value: Some( - value.from_lovelace(1_000_000_000 + 20_000_000 + 5_000_000 + 2_000_000) - |> value.add( - dummy_policy_id, - dummy_asset_name, - 1_000_000_000 - ( 9_896_088 + 9_702_095 ), - ), - ), - } - scoop(options) -} - -test scooper_not_in_settings() fail { - let somebody = #"11111111111111111111111111111111111111111111111111111111" - let options = - ScoopTestOptions { - ..default_scoop_test_options(), - edit_settings_datum: Some( - InlineDatum( - SettingsDatum { - settings_admin: multisig.AnyOf([]), - metadata_admin: Address( - VerificationKeyCredential( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - None, - ), - treasury_admin: multisig.AnyOf([]), - treasury_address: Address( - VerificationKeyCredential( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ), - None, - ), - treasury_allowance: (1, 10), - authorized_scoopers: Some([somebody]), - authorized_staking_keys: [], - base_fee: 0, - simple_fee: 2_500_000, - strategy_fee: 5_000_000, - pool_creation_fee: 0, - extensions: Void, - }, - ), - ), - } - scoop(options) -} - -fn scoop(options: ScoopTestOptions) { - let settings_policy_id = - #"00000000000000000000000000000000000000000000000000000000" - let dummy_policy_id = - #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" - let dummy_asset_name = #"53554e444145" - let scooper = #"00000000000000000000000000000000000000000000000000000000" - let hash_of_pool_script = - #"00000000000000000000000000000000000000000000000000000000" - let hash_of_escrow_script = - #"00000000000000000000000000000000000000000000000000000000" - let user_addr = - wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513") - let owner = - multisig.Signature( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ) - let pool_id = #"00000000000000000000000000000000000000000000000000000000" - let pool_datum = - PoolDatum { - identifier: pool_id, - assets: ((#"", #""), (dummy_policy_id, dummy_asset_name)), - circulating_lp: 1_000_000_000, - fees_per_10_thousand: option.or_else(options.edit_swap_fees, (5,5)), - market_open: 0, - fee_finalized: 100, - protocol_fees: 2_000_000, - } - let pool_out_datum = - PoolDatum { - identifier: pool_id, - assets: ((#"", #""), (dummy_policy_id, dummy_asset_name)), - circulating_lp: 1_000_000_000, - fees_per_10_thousand: option.or_else(options.edit_swap_fees, (5,5)), - market_open: 0, - fee_finalized: 0, - protocol_fees: 7_000_000, - } - let pool_nft_name = shared.pool_nft_name(pool_id) - let pool_address = script_address(hash_of_pool_script) - let pool_input = - Input { - output_reference: mk_output_reference(0), - output: Output { - address: pool_address, - value: value.from_lovelace(1_000_000_000 + 2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 1_000_000_000) - |> value.add(hash_of_pool_script, pool_nft_name, 1), - datum: InlineDatum(pool_datum), - reference_script: None, - }, - } - let dest = Destination { address: user_addr, datum: NoDatum } - let swap = - Swap((#"", #"", 10_000_000), (dummy_policy_id, dummy_asset_name, 0)) - let escrow_datum = - OrderDatum { - pool_ident: None, - owner, - max_protocol_fee: 2_500_000, - destination: dest, - details: swap, - extension: builtin.i_data(0), - } - let escrow_address = script_address(hash_of_escrow_script) - let escrow1_in = - Input { - output_reference: mk_output_reference(2), - output: Output { - address: escrow_address, - value: value.from_lovelace(4_500_000 + 10_000_000), - datum: InlineDatum(escrow_datum), - reference_script: None, - }, - } - let escrow2_in = - Input { - output_reference: mk_output_reference(3), - output: Output { - address: escrow_address, - value: value.from_lovelace(4_500_000 + 10_000_000), - datum: InlineDatum(escrow_datum), - reference_script: None, - }, - } - let settings_input = { - let Input { output_reference, output } = - mk_valid_settings_input([scooper], 1) - let updated_output = - Output { - ..output, - datum: option.or_else(options.edit_settings_datum, output.datum), - } - Input { output_reference, output: updated_output } - } - let escrow1_out = - Output { - address: option.or_else(options.edit_escrow_destination, user_addr), - value: option.or_else( - options.edit_escrow_1_value, - value.from_lovelace(2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 9_896_088), - ), - datum: NoDatum, - reference_script: None, - } - let escrow2_out = - Output { - address: option.or_else(options.edit_escrow_destination, user_addr), - value: option.or_else( - options.edit_escrow_2_value, - value.from_lovelace(2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 9_702_095), - ), - datum: NoDatum, - reference_script: None, - } - let pool_output = - Output { - address: pool_address, - value: option.or_else( - options.edit_pool_output_value, - value.from_lovelace(1_000_000_000 + 20_000_000 + 5_000_000 + 2_000_000) - |> value.add( - dummy_policy_id, - dummy_asset_name, - 1_000_000_000 - ( 9_896_088 + 9_702_095 ), - ) - |> value.add(hash_of_pool_script, pool_nft_name, 1), - ), - datum: InlineDatum(pool_out_datum), - reference_script: None, - } - let ctx = - ScriptContext { - transaction: Transaction { - inputs: [pool_input, escrow1_in, escrow2_in], - reference_inputs: [settings_input], - outputs: [pool_output, escrow1_out, escrow2_out], - fee: option.or_else(options.edit_fee, value.from_lovelace(1_000_000)), - mint: value.to_minted_value(value.from_lovelace(0)), - certificates: [], - withdrawals: dict.new(), - validity_range: interval.between(1, 2), - extra_signatories: [scooper], - redeemers: dict.new(), - datums: dict.new(), - id: mk_tx_hash(1), - }, - purpose: Spend(pool_input.output_reference), - } - let pool_redeemer = PoolScoop( - 0, - 0, - [ - (1, None, 0), - (2, None, 0), - ] - ) - let result = spend(settings_policy_id, pool_datum, pool_redeemer, ctx) - result -} - -fn scoop_swap_deposit(options: ScoopTestOptions) { - let settings_policy_id = #"00000000000000000000000000000000000000000000000000000000" - let dummy_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" - let dummy_asset_name = #"53554e444145" - let scooper = #"00000000000000000000000000000000000000000000000000000000" - let hash_of_pool_script = #"00000000000000000000000000000000000000000000000000000000" - let hash_of_escrow_script = #"00000000000000000000000000000000000000000000000000000000" - let user_addr = wallet_address( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513" - ) - let owner = multisig.Signature( - #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", - ) - let pool_id = #"000000000000000000000000000000000000000000000000000000000000" - let pool_datum = PoolDatum { - identifier: pool_id, - assets: ( - (#"", #""), - (dummy_policy_id, dummy_asset_name), - ), - circulating_lp: 1_000_000_000, - fees_per_10_thousand: option.or_else(options.edit_swap_fees, (5,5)), - market_open: 0, - fee_finalized: 0, - protocol_fees: 2_000_000, - } - let pool_out_datum = PoolDatum { - identifier: pool_id, - assets: ( - (#"", #""), - (dummy_policy_id, dummy_asset_name), - ), - circulating_lp: 1_009_900_990, - fees_per_10_thousand: option.or_else(options.edit_swap_fees, (5,5)), - market_open: 0, - fee_finalized: 0, - protocol_fees: 7_000_000, - } - let pool_nft_name = shared.pool_nft_name(pool_id) - let pool_address = script_address(hash_of_pool_script) - let pool_input = Input { - output_reference: mk_output_reference(0), - output: Output { - address: pool_address, - value: value.from_lovelace(1_000_000_000 + 2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 1_000_000_000) - |> value.add(hash_of_pool_script, pool_nft_name, 1), - datum: InlineDatum(pool_datum), - reference_script: None, - }, - } - let dest = Destination { address: user_addr, datum: NoDatum } - let swap = - Swap( - (#"", #"", 10_000_000), - (dummy_policy_id, dummy_asset_name, 0), - ) - let deposit = - Deposit(( - (#"", #"", 10_000_000), - (dummy_policy_id, dummy_asset_name, 10_000_000), - )) - let escrow_datum_1 = OrderDatum { - pool_ident: None, - owner: owner, - max_protocol_fee: 2_500_000, - destination: dest, - details: swap, - extension: builtin.i_data(0), - } - let escrow_datum_2 = OrderDatum { - pool_ident: None, - owner: owner, - max_protocol_fee: 2_500_000, - destination: dest, - details: deposit, - extension: builtin.i_data(0), - } - let escrow_address = script_address(hash_of_escrow_script) - let escrow1_in = Input { - output_reference: mk_output_reference(2), - output: Output { - address: escrow_address, - value: value.from_lovelace(4_500_000 + 10_000_000), - datum: InlineDatum(escrow_datum_1), - reference_script: None, - }, - } - let escrow2_in = Input { - output_reference: mk_output_reference(3), - output: Output { - address: escrow_address, - value: value.from_lovelace(4_500_000 + 10_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 10_000_000), - datum: InlineDatum(escrow_datum_2), - reference_script: None, - }, - } - let settings_input = { - let Input {output_reference, output} = mk_valid_settings_input([scooper], 1) - let updated_output = Output { - ..output, - datum: option.or_else(options.edit_settings_datum, output.datum) - } - Input { - output_reference: output_reference, - output: updated_output, - } - } - let escrow1_out = Output { - address: option.or_else(options.edit_escrow_destination, user_addr), - value: option.or_else(options.edit_escrow_1_value, - value.from_lovelace(2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 9_896_088)), - datum: NoDatum, - reference_script: None, - } - let escrow2_out = Output { - address: option.or_else(options.edit_escrow_destination, user_addr), - value: value.from_lovelace(2_000_000) - |> value.add(hash_of_pool_script, pool_lp_name(pool_id), 9_900_990) - |> value.add(dummy_policy_id, dummy_asset_name, 196_990), - datum: NoDatum, - reference_script: None, - } - let pool_output = Output { - address: pool_address, - value: option.or_else(options.edit_pool_output_value, - value.from_lovelace(1_000_000_000 + 20_000_000 + 5_000_000 + 2_000_000) - |> value.add(dummy_policy_id, dummy_asset_name, 1_000_000_000 - 9_896_088 + 10_000_000 - 196_990) - |> value.add(hash_of_pool_script, pool_nft_name, 1)), - datum: InlineDatum(pool_out_datum), - reference_script: None, - } - - let ctx = ScriptContext { - transaction: Transaction { - inputs: [pool_input, escrow1_in, escrow2_in], - reference_inputs: [settings_input], - outputs: [pool_output, escrow1_out, escrow2_out], - fee: option.or_else(options.edit_fee, value.from_lovelace(1_000_000)), - mint: value.to_minted_value( - value.from_lovelace(0) - |> value.add(hash_of_pool_script, pool_lp_name(pool_id), 9_900_990) - ), - certificates: [], - withdrawals: dict.new(), - validity_range: interval.between(1, 2), - extra_signatories: [scooper], - redeemers: dict.new(), - datums: dict.new(), - id: mk_tx_hash(1), - }, - purpose: Spend(pool_input.output_reference), - } - let pool_redeemer = PoolScoop( - 0, - 0, - [ - (1, None, 0), - (2, None, 0), - ] - ) - let result = spend(settings_policy_id, pool_datum, pool_redeemer, ctx) - result -} - -test mint_test() { - let settings_policy_id = #"00000000000000000000000000000000000000000000000000000000" - let hash_of_pool_script = #"00000000000000000000000000000000000000000000000000000000" - let pool_address = script_address(hash_of_pool_script) - let rberry_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" - let rberry_token_name = #"524245525259" - let user_address = - wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513") - let settings_input = mk_valid_settings_input([], 1) - let funds_input = Input { - output_reference: OutputReference { - transaction_id: mk_tx_hash(0), - output_index: 0 - }, - output: Output { - address: user_address, - value: value.from_lovelace(10_000_000_000) - |> value.add(rberry_policy_id, rberry_token_name, 1_000_000_000), - datum: NoDatum, - reference_script: None, - }, - } - let pool_id = - funds_input.output_reference.transaction_id.hash - |> bytearray.concat(#"23") // '#' character - |> - bytearray.concat(int_to_ident(funds_input.output_reference.output_index)) - |> hash.blake2b_256 - |> bytearray.drop(4) - let (new_pool_ref_token, new_pool_nft_token, new_pool_lp_token) = shared.pool_token_names(pool_id) - let pool_output = Output { - address: pool_address, - value: value.from_lovelace(1_002_000_000) - |> value.add(rberry_policy_id, rberry_token_name, 1_000_000_000) - |> value.add(hash_of_pool_script, new_pool_nft_token, 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 lp_output = Output { - address: user_address, - value: value.from_lovelace(2_000_000) - |> value.add(hash_of_pool_script, pool_lp_name(pool_id), 1_000_000_000), - datum: NoDatum, - reference_script: None, - } - let ref_output = Output { - address: user_address, - value: value.from_lovelace(2_000_000) - |> value.add(hash_of_pool_script, new_pool_ref_token, 1), - datum: NoDatum, - reference_script: None, - } - let poolMintRedeemer = CreatePool { - assets: ((#"", #""), (rberry_policy_id, rberry_token_name)), - pool_output: 0, - metadata_output: 2, - } - let ctx = ScriptContext { - transaction: Transaction { - inputs: [funds_input], - reference_inputs: [settings_input], - outputs: [pool_output, lp_output, ref_output], - fee: value.from_lovelace(1_000_000), - mint: value.to_minted_value( - value.from_lovelace(0) - |> value.add(hash_of_pool_script, new_pool_lp_token, 1_000_000_000) - |> value.add(hash_of_pool_script, new_pool_nft_token, 1) - |> value.add(hash_of_pool_script, new_pool_ref_token, 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: Mint(hash_of_pool_script), - } - let result = mint(settings_policy_id, poolMintRedeemer, ctx) - result -} - -test has_expected_pool_value_test() { - // ADA/rberry pool - let rberry_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" - let rberry_token_name = #"524245525259" - - let pool_script_hash = #"00000000000000000000000000000000000000000000000000000000" - let identifier = #"00000000000000000000000000000000000000000000000000000000" - let (_, pool_nft_token, pool_lp_token) = shared.pool_token_names(identifier) - let pool_value = value.from_lovelace(102_000_000) - |> value.add(rberry_policy_id, rberry_token_name, 100_000_000) - |> value.add(pool_script_hash, pool_nft_token, 1) - let outcome = PoolState { - quantity_lp: (pool_script_hash, pool_lp_token, 99), - quantity_a: (ada_policy_id, ada_asset_name, 100_000_000), - quantity_b: (rberry_policy_id, rberry_token_name, 100_000_000) - } - let protocol_fees = 2_000_000 - - has_expected_pool_value( - pool_script_hash, - identifier, - pool_value, - outcome, - protocol_fees, - ) -} - -test has_expected_pool_value_test2() { - // other/rberry pool - let other_policy_id = #"fafafafafafafafafafafafafafafafafafafafafafafafafafafafa" - let other_token_name = #"fafafafa" - let rberry_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" - let rberry_token_name = #"524245525259" - - let pool_script_hash = #"00000000000000000000000000000000000000000000000000000000" - let identifier = #"00000000000000000000000000000000000000000000000000000000" - let (_, pool_nft_token, pool_lp_token) = shared.pool_token_names(identifier) - let pool_value = value.from_lovelace(2_000_000) - |> value.add(other_policy_id, other_token_name, 100_000_000) - |> value.add(rberry_policy_id, rberry_token_name, 100_000_000) - |> value.add(pool_script_hash, pool_nft_token, 1) - let outcome = PoolState { - quantity_lp: (pool_script_hash, pool_lp_token, 99), - quantity_a: (other_policy_id, other_token_name, 100_000_000), - quantity_b: (rberry_policy_id, rberry_token_name, 100_000_000) - } - let protocol_fees = 2_000_000 - - has_expected_pool_value( - pool_script_hash, - identifier, - pool_value, - outcome, - protocol_fees, - ) -} +} \ No newline at end of file diff --git a/validators/settings.ak b/validators/settings.ak index 80a5f2e..fd1be31 100644 --- a/validators/settings.ak +++ b/validators/settings.ak @@ -1,5 +1,6 @@ use aiken/list use aiken/transaction.{InlineDatum, Mint, ScriptContext, OutputReference} +use aiken/transaction/credential.{ScriptCredential} use aiken/transaction/value use sundae/multisig use types/settings.{SettingsDatum, SettingsRedeemer, SettingsAdminUpdate, TreasuryAdminUpdate, settings_nft_name} @@ -9,7 +10,7 @@ use shared.{spent_output} /// /// It is parameterized by the protocol_boot_utxo, a constant to make it an NFT by the usual trick. validator(protocol_boot_utxo: OutputReference) { - fn spend(d: SettingsDatum, redeemer: SettingsRedeemer, ctx: ScriptContext) { + pub fn spend(d: SettingsDatum, redeemer: SettingsRedeemer, ctx: ScriptContext) { // Find our own input so we know the datum / our own address let own_input = spent_output(ctx) let own_address = own_input.address @@ -107,14 +108,14 @@ validator(protocol_boot_utxo: OutputReference) { } // Let us mint the settings NFT exactly once, by checking that one of the inputs is the protocol_boot_utxo - fn mint(_r: Data, ctx: ScriptContext) { + pub fn mint(_r: Data, ctx: ScriptContext) { expect Mint(own_policy_id) = ctx.purpose + let expected_mint = value.from_asset(own_policy_id, settings_nft_name, 1) // Check that we mint *only* one token, and it's exactly our own policy id, and the settings NFT name // This ensures that we don't sneakily mint a second NFT for "reasons" let mints_exactly_one_settings_nft = - value.from_minted_value(ctx.transaction.mint) == - value.from_asset(own_policy_id, settings_nft_name, 1) + value.from_minted_value(ctx.transaction.mint) == expected_mint // And, like mentioned above, ensure that this is a true NFT let spends_protocol_boot_utxo = @@ -122,9 +123,22 @@ validator(protocol_boot_utxo: OutputReference) { input.output_reference == protocol_boot_utxo }) + // Make sure the output value contains no extra tokens, and is paid to the settings script itself + expect [settings_output] = list.filter(ctx.transaction.outputs, fn(output) { + value.without_lovelace(output.value) == expected_mint + }) + let pays_to_settings_script = settings_output.address.payment_credential == ScriptCredential(own_policy_id) + // Make sure the datum is an inline datum, and has a well-formed datum + expect InlineDatum(settings_datum) = settings_output.datum + expect _: SettingsDatum = settings_datum + + // TODO: require a signature from the 3 initial admins (or at least the settings admin) to prevent bricking? + and { mints_exactly_one_settings_nft, spends_protocol_boot_utxo, + pays_to_settings_script, } } } + diff --git a/validators/tests/pool.ak b/validators/tests/pool.ak new file mode 100644 index 0000000..8fe41b0 --- /dev/null +++ b/validators/tests/pool.ak @@ -0,0 +1,754 @@ +use aiken/builtin +use aiken/bytearray +use aiken/dict +use aiken/hash +use aiken/interval +use aiken/option +use aiken/transaction.{ + Datum, InlineDatum, Input, NoDatum, Output, OutputReference, + ScriptContext, Spend, Transaction, TransactionId, +} +use aiken/transaction/credential.{ + Address, VerificationKeyCredential, from_verification_key, from_script, with_delegation_key +} +use aiken/transaction/value.{Value, ada_policy_id, ada_asset_name} +use shared.{ + pool_lp_name, +} +use sundae/multisig +use tests/examples/ex_settings.{mk_valid_settings_input} +use tests/examples/ex_shared.{ + mk_output_reference, mk_tx_hash, script_address, wallet_address, +} +use types/order.{Deposit, Destination, OrderDatum, Swap} +use types/pool.{ + CreatePool, PoolDatum, PoolScoop, +} +use calculation/shared.{PoolState} as calc_shared +use types/settings.{SettingsDatum} +use tx_util/builder.{add_asset_to_tx_output, add_tx_input, add_tx_output, add_tx_ref_input, build_txn_context, mint_assets, new_tx_input, new_tx_output} +use pool as pool_validator + + +type ScoopTestOptions { + edit_escrow_1_value: Option, + edit_escrow_2_value: Option, + edit_escrow_destination: Option
, + edit_fee: Option, + edit_swap_fees: Option<(Int,Int)>, + edit_pool_input_address: Option
, + edit_pool_output_address: Option
, + edit_pool_output_value: Option, + edit_settings_datum: Option, +} + +fn default_scoop_test_options() -> ScoopTestOptions { + ScoopTestOptions { + edit_escrow_1_value: None, + edit_escrow_2_value: None, + edit_escrow_destination: None, + edit_fee: None, + edit_swap_fees: None, + edit_pool_input_address: None, + edit_pool_output_address: None, + edit_pool_output_value: None, + edit_settings_datum: None, + } +} + +test ok_scoop_swap_swap() { + let options = default_scoop_test_options() + scoop(options) +} + +test ok_scoop_swap_deposit() { + let options = default_scoop_test_options() + scoop_swap_deposit(options) +} + +test scoop_bad_destination() fail { + let burn_addr = + wallet_address(#"12000000000000000000000000000000000000000000000000000000") + let options = + ScoopTestOptions { + ..default_scoop_test_options(), + edit_escrow_destination: Some(burn_addr), + } + scoop(options) +} + +test scoop_payouts_swapped() fail { + let dummy_policy_id = + #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" + let dummy_asset_name = #"53554e444145" + let options = + ScoopTestOptions { + ..default_scoop_test_options(), + edit_escrow_1_value: Some( + value.from_lovelace(2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 9_702_095), + ), + edit_escrow_2_value: Some( + value.from_lovelace(2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 9_896_088), + ), + } + scoop(options) +} + +test pool_validator_ignores_fee() { + let options = + ScoopTestOptions { + ..default_scoop_test_options(), + edit_fee: Some(value.from_lovelace(1_000_000_000)), + } + scoop(options) +} + +test scoop_high_swap_fees() { + let pool_id = #"00000000000000000000000000000000000000000000000000000000" + let pool_nft_name = shared.pool_nft_name(pool_id) + let dummy_policy_id = + #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" + let dummy_asset_name = #"53554e444145" + let swap_fee = 100 + let options = + ScoopTestOptions { + ..default_scoop_test_options(), + edit_swap_fees: Some((swap_fee, swap_fee)), + edit_escrow_1_value: Some( + value.from_lovelace(2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 9_802_950), + ), + edit_escrow_2_value: Some( + value.from_lovelace(2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 9_611_678), + ), + edit_pool_output_value: Some( + value.from_lovelace(1_000_000_000 + 20_000_000 + 5_000_000 + 2_000_000) + |> value.add( + dummy_policy_id, + dummy_asset_name, + 1_000_000_000 - ( 9_802_950 + 9_611_678 ), + ) + |> value.add(pool_script_hash, pool_nft_name, 1), + ), + } + scoop(options) +} + +test output_missing_nft() fail { + let dummy_policy_id = + #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" + let dummy_asset_name = #"53554e444145" + let options = + ScoopTestOptions { + ..default_scoop_test_options(), + edit_pool_output_value: Some( + value.from_lovelace(1_000_000_000 + 20_000_000 + 5_000_000 + 2_000_000) + |> value.add( + dummy_policy_id, + dummy_asset_name, + 1_000_000_000 - ( 9_896_088 + 9_702_095 ), + ), + ), + } + scoop(options) +} + +const pool_script_hash = #"00000000000000000000000000000000000000000000000000000000" +const random_hash = #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513" +const other_hash = #"01010101010101010101010101010101010101010101010101010101" +test scoop_pool_output_wallet_address() fail { + let options = + ScoopTestOptions { + ..default_scoop_test_options(), + edit_pool_output_address: Some(from_verification_key(random_hash)), + } + scoop(options) +} +test scoop_pool_output_wrong_script() fail { + let options = + ScoopTestOptions { + ..default_scoop_test_options(), + edit_pool_output_address: Some(from_script(random_hash)), + } + scoop(options) +} +test scoop_pool_output_change_staking_credential() fail { + let options = + ScoopTestOptions { + ..default_scoop_test_options(), + edit_pool_input_address: Some(from_script(pool_script_hash) |> with_delegation_key(random_hash)), + edit_pool_output_address: Some(from_script(pool_script_hash) |> with_delegation_key(other_hash)), + } + scoop(options) +} + +test scooper_not_in_settings() fail { + let somebody = #"11111111111111111111111111111111111111111111111111111111" + let options = + ScoopTestOptions { + ..default_scoop_test_options(), + edit_settings_datum: Some( + InlineDatum( + SettingsDatum { + settings_admin: multisig.AnyOf([]), + metadata_admin: Address( + VerificationKeyCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ), + treasury_admin: multisig.AnyOf([]), + treasury_address: Address( + VerificationKeyCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ), + treasury_allowance: (1, 10), + authorized_scoopers: Some([somebody]), + authorized_staking_keys: [], + base_fee: 0, + simple_fee: 2_500_000, + strategy_fee: 5_000_000, + pool_creation_fee: 0, + extensions: Void, + }, + ), + ), + } + scoop(options) +} + +fn scoop(options: ScoopTestOptions) { + let settings_policy_id = + #"00000000000000000000000000000000000000000000000000000000" + let dummy_policy_id = + #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" + let dummy_asset_name = #"53554e444145" + let scooper = #"00000000000000000000000000000000000000000000000000000000" + let hash_of_escrow_script = + #"00000000000000000000000000000000000000000000000000000000" + let user_addr = + wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513") + let owner = + multisig.Signature( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ) + let pool_id = #"00000000000000000000000000000000000000000000000000000000" + let pool_datum = + PoolDatum { + identifier: pool_id, + assets: ((#"", #""), (dummy_policy_id, dummy_asset_name)), + circulating_lp: 1_000_000_000, + fees_per_10_thousand: option.or_else(options.edit_swap_fees, (5,5)), + market_open: 0, + fee_finalized: 100, + protocol_fees: 2_000_000, + } + let pool_out_datum = + PoolDatum { + identifier: pool_id, + assets: ((#"", #""), (dummy_policy_id, dummy_asset_name)), + circulating_lp: 1_000_000_000, + fees_per_10_thousand: option.or_else(options.edit_swap_fees, (5,5)), + market_open: 0, + fee_finalized: 0, + protocol_fees: 7_000_000, + } + let pool_nft_name = shared.pool_nft_name(pool_id) + let pool_address = script_address(pool_script_hash) + let pool_input = + Input { + output_reference: mk_output_reference(0), + output: Output { + address: option.or_else(options.edit_pool_input_address, pool_address), + value: value.from_lovelace(1_000_000_000 + 2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 1_000_000_000) + |> value.add(pool_script_hash, pool_nft_name, 1), + datum: InlineDatum(pool_datum), + reference_script: None, + }, + } + let dest = Destination { address: user_addr, datum: NoDatum } + let swap = + Swap((#"", #"", 10_000_000), (dummy_policy_id, dummy_asset_name, 0)) + let escrow_datum = + OrderDatum { + pool_ident: None, + owner, + max_protocol_fee: 2_500_000, + destination: dest, + details: swap, + extension: builtin.i_data(0), + } + let escrow_address = script_address(hash_of_escrow_script) + let escrow1_in = + Input { + output_reference: mk_output_reference(2), + output: Output { + address: escrow_address, + value: value.from_lovelace(4_500_000 + 10_000_000), + datum: InlineDatum(escrow_datum), + reference_script: None, + }, + } + let escrow2_in = + Input { + output_reference: mk_output_reference(3), + output: Output { + address: escrow_address, + value: value.from_lovelace(4_500_000 + 10_000_000), + datum: InlineDatum(escrow_datum), + reference_script: None, + }, + } + let settings_input = { + let Input { output_reference, output } = + mk_valid_settings_input([scooper], 1) + let updated_output = + Output { + ..output, + datum: option.or_else(options.edit_settings_datum, output.datum), + } + Input { output_reference, output: updated_output } + } + let escrow1_out = + Output { + address: option.or_else(options.edit_escrow_destination, user_addr), + value: option.or_else( + options.edit_escrow_1_value, + value.from_lovelace(2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 9_896_088), + ), + datum: NoDatum, + reference_script: None, + } + let escrow2_out = + Output { + address: option.or_else(options.edit_escrow_destination, user_addr), + value: option.or_else( + options.edit_escrow_2_value, + value.from_lovelace(2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 9_702_095), + ), + datum: NoDatum, + reference_script: None, + } + let pool_output = + Output { + address: option.or_else(options.edit_pool_output_address, pool_address), + value: option.or_else( + options.edit_pool_output_value, + value.from_lovelace(1_000_000_000 + 20_000_000 + 5_000_000 + 2_000_000) + |> value.add( + dummy_policy_id, + dummy_asset_name, + 1_000_000_000 - ( 9_896_088 + 9_702_095 ), + ) + |> value.add(pool_script_hash, pool_nft_name, 1), + ), + datum: InlineDatum(pool_out_datum), + reference_script: None, + } + let ctx = + ScriptContext { + transaction: Transaction { + inputs: [pool_input, escrow1_in, escrow2_in], + reference_inputs: [settings_input], + outputs: [pool_output, escrow1_out, escrow2_out], + fee: option.or_else(options.edit_fee, value.from_lovelace(1_000_000)), + mint: value.to_minted_value(value.from_lovelace(0)), + certificates: [], + withdrawals: dict.new(), + validity_range: interval.between(1, 2), + extra_signatories: [scooper], + redeemers: dict.new(), + datums: dict.new(), + id: mk_tx_hash(1), + }, + purpose: Spend(pool_input.output_reference), + } + let pool_redeemer = PoolScoop( + 0, + 0, + [ + (1, None, 0), + (2, None, 0), + ] + ) + let result = pool_validator.spend(settings_policy_id, pool_datum, pool_redeemer, ctx) + result +} + +fn scoop_swap_deposit(options: ScoopTestOptions) { + let settings_policy_id = #"00000000000000000000000000000000000000000000000000000000" + let dummy_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" + let dummy_asset_name = #"53554e444145" + let scooper = #"00000000000000000000000000000000000000000000000000000000" + let hash_of_escrow_script = #"00000000000000000000000000000000000000000000000000000000" + let user_addr = wallet_address( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513" + ) + let owner = multisig.Signature( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ) + let pool_id = #"000000000000000000000000000000000000000000000000000000000000" + let pool_datum = PoolDatum { + identifier: pool_id, + assets: ( + (#"", #""), + (dummy_policy_id, dummy_asset_name), + ), + circulating_lp: 1_000_000_000, + fees_per_10_thousand: option.or_else(options.edit_swap_fees, (5,5)), + market_open: 0, + fee_finalized: 0, + protocol_fees: 2_000_000, + } + let pool_out_datum = PoolDatum { + identifier: pool_id, + assets: ( + (#"", #""), + (dummy_policy_id, dummy_asset_name), + ), + circulating_lp: 1_009_900_990, + fees_per_10_thousand: option.or_else(options.edit_swap_fees, (5,5)), + market_open: 0, + fee_finalized: 0, + protocol_fees: 7_000_000, + } + let pool_nft_name = shared.pool_nft_name(pool_id) + let pool_address = script_address(pool_script_hash) + let pool_input = Input { + output_reference: mk_output_reference(0), + output: Output { + address: pool_address, + value: value.from_lovelace(1_000_000_000 + 2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 1_000_000_000) + |> value.add(pool_script_hash, pool_nft_name, 1), + datum: InlineDatum(pool_datum), + reference_script: None, + }, + } + let dest = Destination { address: user_addr, datum: NoDatum } + let swap = + Swap( + (#"", #"", 10_000_000), + (dummy_policy_id, dummy_asset_name, 0), + ) + let deposit = + Deposit(( + (#"", #"", 10_000_000), + (dummy_policy_id, dummy_asset_name, 10_000_000), + )) + let escrow_datum_1 = OrderDatum { + pool_ident: None, + owner: owner, + max_protocol_fee: 2_500_000, + destination: dest, + details: swap, + extension: builtin.i_data(0), + } + let escrow_datum_2 = OrderDatum { + pool_ident: None, + owner: owner, + max_protocol_fee: 2_500_000, + destination: dest, + details: deposit, + extension: builtin.i_data(0), + } + let escrow_address = script_address(hash_of_escrow_script) + let escrow1_in = Input { + output_reference: mk_output_reference(2), + output: Output { + address: escrow_address, + value: value.from_lovelace(4_500_000 + 10_000_000), + datum: InlineDatum(escrow_datum_1), + reference_script: None, + }, + } + let escrow2_in = Input { + output_reference: mk_output_reference(3), + output: Output { + address: escrow_address, + value: value.from_lovelace(4_500_000 + 10_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 10_000_000), + datum: InlineDatum(escrow_datum_2), + reference_script: None, + }, + } + let settings_input = { + let Input {output_reference, output} = mk_valid_settings_input([scooper], 1) + let updated_output = Output { + ..output, + datum: option.or_else(options.edit_settings_datum, output.datum) + } + Input { + output_reference: output_reference, + output: updated_output, + } + } + let escrow1_out = Output { + address: option.or_else(options.edit_escrow_destination, user_addr), + value: option.or_else(options.edit_escrow_1_value, + value.from_lovelace(2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 9_896_088)), + datum: NoDatum, + reference_script: None, + } + let escrow2_out = Output { + address: option.or_else(options.edit_escrow_destination, user_addr), + value: value.from_lovelace(2_000_000) + |> value.add(pool_script_hash, pool_lp_name(pool_id), 9_900_990) + |> value.add(dummy_policy_id, dummy_asset_name, 196_990), + datum: NoDatum, + reference_script: None, + } + let pool_output = Output { + address: pool_address, + value: option.or_else(options.edit_pool_output_value, + value.from_lovelace(1_000_000_000 + 20_000_000 + 5_000_000 + 2_000_000) + |> value.add(dummy_policy_id, dummy_asset_name, 1_000_000_000 - 9_896_088 + 10_000_000 - 196_990) + |> value.add(pool_script_hash, pool_nft_name, 1)), + datum: InlineDatum(pool_out_datum), + reference_script: None, + } + + let ctx = ScriptContext { + transaction: Transaction { + inputs: [pool_input, escrow1_in, escrow2_in], + reference_inputs: [settings_input], + outputs: [pool_output, escrow1_out, escrow2_out], + fee: option.or_else(options.edit_fee, value.from_lovelace(1_000_000)), + mint: value.to_minted_value( + value.from_lovelace(0) + |> value.add(pool_script_hash, pool_lp_name(pool_id), 9_900_990) + ), + certificates: [], + withdrawals: dict.new(), + validity_range: interval.between(1, 2), + extra_signatories: [scooper], + redeemers: dict.new(), + datums: dict.new(), + id: mk_tx_hash(1), + }, + purpose: Spend(pool_input.output_reference), + } + let pool_redeemer = PoolScoop( + 0, + 0, + [ + (1, None, 0), + (2, None, 0), + ] + ) + let result = pool_validator.spend(settings_policy_id, pool_datum, pool_redeemer, ctx) + result +} + +fn pool_test_tx_input() -> Input { + let funds_input = + new_tx_input( + mk_tx_hash(0).hash, + wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513"), + 0, + NoDatum, + ) + funds_input +} + +test mint_test_two_nfts() fail { + let pool_id = pool_ident_from_input(pool_test_tx_input()) + let (_, new_pool_nft_token, _) = shared.pool_token_names(pool_id) + // if we add on another pool NFT token to the pool output, it should fail + mint_test_modify( + fn(output) { Output { ..output, value: value.add(output.value, pool_script_hash, new_pool_nft_token, 1) } }, + identity, + identity, + identity + ) +} + +fn pool_ident_from_input (tx_input: Input) -> ByteArray { + tx_input.output_reference.transaction_id.hash + |> bytearray.concat(#"23") // '#' character + |> + bytearray.concat(pool_validator.int_to_ident(tx_input.output_reference.output_index)) + |> hash.blake2b_256 + |> bytearray.drop(4) +} + +fn mint_test_modify( + modify_pool_output: fn(Output) -> Output, + modify_lp_output: fn(Output) -> Output, + modify_ref_output: fn(Output) -> Output, + modify_datum: fn(Datum) -> Datum) -> Bool { + let settings_policy_id = #"00000000000000000000000000000000000000000000000000000000" + let pool_address = script_address(pool_script_hash) |> with_delegation_key(#"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad") + let rberry_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" + let rberry_token_name = #"524245525259" + let user_address = + wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513") + let settings_input = mk_valid_settings_input([], 1) + + let funds_input = pool_test_tx_input() + let pool_id = pool_ident_from_input(funds_input) + let (new_pool_ref_token, new_pool_nft_token, new_pool_lp_token) = shared.pool_token_names(pool_id) + let inline_pool_datum = modify_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, + } + )) + let pool_output_val = + value.from_asset(rberry_policy_id, rberry_token_name, 1_000_000_000) + |> value.add(pool_script_hash, new_pool_nft_token, 1) + |> value.merge(value.from_lovelace(1_002_000_000)) + let pool_output = new_tx_output(pool_address, 0, inline_pool_datum) // 1_002_000_000 = 1_000_000_000 ADA for pool + 2_000_000 ADA for protocol_fees + |> add_asset_to_tx_output(pool_output_val) + |> modify_pool_output + + let lp_output_val = + value.from_asset(pool_script_hash, pool_lp_name(pool_id), 1_000_000_000) + |> value.merge(value.from_lovelace(2_000_000)) + let lp_output = + new_tx_output(user_address, 0, NoDatum) // we can probably get rid of the rider, it gets auto added + |> add_asset_to_tx_output(lp_output_val) + |> modify_lp_output + + let ref_output_val = + value.from_asset(pool_script_hash, new_pool_ref_token, 1) + |> value.merge(value.from_lovelace(2_000_000)) + let ref_output = + new_tx_output(user_address, 0, InlineDatum(Void)) // we can probably get rid of the rider, it gets auto added + |> add_asset_to_tx_output(ref_output_val) + |> modify_ref_output + + let pool_mint_redeemer = CreatePool { + assets: ((#"", #""), (rberry_policy_id, rberry_token_name)), + pool_output: 0, + metadata_output: 2, + } + + let ctx = interval.between(1,2) + |> build_txn_context() + |> mint_assets(pool_script_hash, value.to_minted_value( + value.from_lovelace(0) + |> value.add(pool_script_hash, new_pool_lp_token, 1_000_000_000) + |> value.add(pool_script_hash, new_pool_nft_token, 1) + |> value.add(pool_script_hash, new_pool_ref_token, 1) + )) + |> add_tx_input(funds_input) + |> add_tx_ref_input(settings_input) + + // these must be in reverse order like so, in order to get [pool_output, lp_output, ref_output] + |> add_tx_output(ref_output) + |> add_tx_output(lp_output) + |> add_tx_output(pool_output) + + let result = pool_validator.mint(settings_policy_id, pool_mint_redeemer, ctx) + result +} + +test mint_test() { + mint_test_modify(identity, identity, identity, identity) +} + +// make sure pool_output.address is checked to be the pool address +test mint_test_wrong_address () fail { + let minted = mint_test_modify( + // change pool nft output address to destination that shouldn't be possible + fn (output) { + Output{ + ..output, + // TODO: move some of this stuff to constants? + address: from_verification_key(random_hash) |> with_delegation_key(#"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad") + } + }, + identity, + identity, + identity + ) + minted +} + +// make sure we can't include any spam on the datum +test mint_test_nonvoid_datum() fail { + let minted = mint_test_modify( + identity, + identity, + fn (ref_metadata_output) { + Output { + ..ref_metadata_output, + datum: InlineDatum("Evil data") + } + }, + identity + ) + minted +} + +test has_expected_pool_value_test() { + // ADA/rberry pool + let rberry_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" + let rberry_token_name = #"524245525259" + + let pool_script_hash = #"00000000000000000000000000000000000000000000000000000000" + let identifier = #"00000000000000000000000000000000000000000000000000000000" + let (_, pool_nft_token, pool_lp_token) = shared.pool_token_names(identifier) + let pool_value = value.from_lovelace(102_000_000) + |> value.add(rberry_policy_id, rberry_token_name, 100_000_000) + |> value.add(pool_script_hash, pool_nft_token, 1) + let outcome = PoolState { + quantity_lp: (pool_script_hash, pool_lp_token, 99), + quantity_a: (ada_policy_id, ada_asset_name, 100_000_000), + quantity_b: (rberry_policy_id, rberry_token_name, 100_000_000) + } + let protocol_fees = 2_000_000 + + pool_validator.has_expected_pool_value( + pool_script_hash, + identifier, + pool_value, + outcome, + protocol_fees, + ) +} + +test has_expected_pool_value_test2() { + // other/rberry pool + let other_policy_id = #"fafafafafafafafafafafafafafafafafafafafafafafafafafafafa" + let other_token_name = #"fafafafa" + let rberry_policy_id = #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" + let rberry_token_name = #"524245525259" + + let pool_script_hash = #"00000000000000000000000000000000000000000000000000000000" + let identifier = #"00000000000000000000000000000000000000000000000000000000" + let (_, pool_nft_token, pool_lp_token) = shared.pool_token_names(identifier) + let pool_value = value.from_lovelace(2_000_000) + |> value.add(other_policy_id, other_token_name, 100_000_000) + |> value.add(rberry_policy_id, rberry_token_name, 100_000_000) + |> value.add(pool_script_hash, pool_nft_token, 1) + let outcome = PoolState { + quantity_lp: (pool_script_hash, pool_lp_token, 99), + quantity_a: (other_policy_id, other_token_name, 100_000_000), + quantity_b: (rberry_policy_id, rberry_token_name, 100_000_000) + } + let protocol_fees = 2_000_000 + + pool_validator.has_expected_pool_value( + pool_script_hash, + identifier, + pool_value, + outcome, + protocol_fees, + ) +} \ No newline at end of file diff --git a/validators/tests/settings.ak b/validators/tests/settings.ak new file mode 100644 index 0000000..a6e5570 --- /dev/null +++ b/validators/tests/settings.ak @@ -0,0 +1,96 @@ +use aiken/interval +use aiken/transaction.{InlineDatum, NoDatum, OutputReference, TransactionId} +use aiken/transaction/credential.{VerificationKeyCredential, Address, from_script} +use aiken/transaction/value +use sundae/multisig +use types/settings.{SettingsDatum, settings_nft_name} +use tx_util/builder.{ + build_txn_context, + mint_assets, + add_tx_input, + add_tx_output, + new_tx_output, + new_tx_input, + with_asset_of_tx_input, + add_asset_to_tx_output, +} +use settings as settings_validator + +fn test_mint_settings(settings_nfts_count: Int) { + let settings_nft_policy = #"00" + + let settings_nft = value.to_minted_value(value.from_asset(settings_nft_policy, settings_nft_name, settings_nfts_count)) + + let settings_datum = mk_valid_settings_datum([]) // Some([]) for authorized_scoopers means no one can scoop + + let settings_output = new_tx_output( + from_script(settings_nft_policy), + 2_000_000, + InlineDatum(settings_datum) + ) |> add_asset_to_tx_output(value.from_asset(settings_nft_policy, settings_nft_name, settings_nfts_count)) + + let protocol_boot_utxo = OutputReference { transaction_id: TransactionId { hash: #"00"}, output_index: 0 } + let protocol_boot_utxo_policy = #"00" + + let protocol_boot_utxo_input = new_tx_input( + protocol_boot_utxo.transaction_id.hash, + from_script(protocol_boot_utxo_policy), + 2_000_000, + NoDatum, + ) |> with_asset_of_tx_input(value.from_asset(protocol_boot_utxo_policy, "boot utxo name", 1)) + let ctx = + interval.between(1, 2) + |> build_txn_context() + |> mint_assets(settings_nft_policy, settings_nft) + |> add_tx_input(protocol_boot_utxo_input) + |> add_tx_output(settings_output) + + + let minted = settings_validator.mint(protocol_boot_utxo, Void, ctx) + minted +} + +test mint_invalid_settings_multiple_nft() fail { + test_mint_settings(2) +} + +test mint_valid_settings() { + test_mint_settings(1) +} + +fn mk_valid_settings_datum( + scoopers: List, +) -> SettingsDatum { + SettingsDatum { + settings_admin: multisig.Signature( + #"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad", + ), + metadata_admin: Address( + VerificationKeyCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ), + treasury_admin: multisig.Signature( + #"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad", + ), + treasury_address: Address( + VerificationKeyCredential( + #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513", + ), + None, + ), + treasury_allowance: (1, 10), + authorized_scoopers: Some(scoopers), + authorized_staking_keys: [ + VerificationKeyCredential( + #"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad" + ), + ], + base_fee: 0, + simple_fee: 2_500_000, + strategy_fee: 5_000_000, + pool_creation_fee: 0, + extensions: Void, + } +} \ No newline at end of file