diff --git a/validators/pool.ak b/validators/pool.ak index 9379915..66b6028 100644 --- a/validators/pool.ak +++ b/validators/pool.ak @@ -85,6 +85,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, @@ -283,7 +290,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)) }) @@ -437,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, diff --git a/validators/tests/pool.ak b/validators/tests/pool.ak index 59705af..c79b00c 100644 --- a/validators/tests/pool.ak +++ b/validators/tests/pool.ak @@ -9,7 +9,7 @@ use aiken/transaction.{ ScriptContext, Spend, Transaction, TransactionId, } use aiken/transaction/credential.{ - Address, VerificationKeyCredential, + Address, VerificationKeyCredential, from_verification_key, from_script, with_delegation_key } use aiken/transaction/value.{Value} use shared.{ @@ -35,6 +35,8 @@ type ScoopTestOptions { 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, } @@ -46,6 +48,8 @@ fn default_scoop_test_options() -> ScoopTestOptions { 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, } @@ -101,8 +105,6 @@ test pool_validator_ignores_fee() { } 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 = @@ -128,7 +130,7 @@ test scoop_high_swap_fees() { dummy_asset_name, 1_000_000_000 - ( 9_802_950 + 9_611_678 ), ) - |> value.add(hash_of_pool_script, pool_nft_name, 1), + |> value.add(pool_script_hash, pool_nft_name, 1), ), } scoop(options) @@ -153,6 +155,35 @@ test output_missing_nft() fail { 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 = @@ -197,8 +228,6 @@ fn scoop(options: ScoopTestOptions) { #"9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77" let dummy_asset_name = #"53554e444145" let scooper = #"00000000000000000000000000000000000000000000000000000000" - let hash_of_pool_script = - #"00000000000000000000000000000000000000000000000000000000" let hash_of_escrow_script = #"00000000000000000000000000000000000000000000000000000000" let user_addr = @@ -229,15 +258,15 @@ fn scoop(options: ScoopTestOptions) { 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_address = script_address(pool_script_hash) let pool_input = Input { output_reference: mk_output_reference(0), output: Output { - address: pool_address, + 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(hash_of_pool_script, pool_nft_name, 1), + |> value.add(pool_script_hash, pool_nft_name, 1), datum: InlineDatum(pool_datum), reference_script: None, }, @@ -309,7 +338,7 @@ fn scoop(options: ScoopTestOptions) { } let pool_output = Output { - address: pool_address, + 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) @@ -318,7 +347,7 @@ fn scoop(options: ScoopTestOptions) { dummy_asset_name, 1_000_000_000 - ( 9_896_088 + 9_702_095 ), ) - |> value.add(hash_of_pool_script, pool_nft_name, 1), + |> value.add(pool_script_hash, pool_nft_name, 1), ), datum: InlineDatum(pool_out_datum), reference_script: None, @@ -358,7 +387,6 @@ fn scoop_swap_deposit(options: ScoopTestOptions) { 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" @@ -392,14 +420,14 @@ fn scoop_swap_deposit(options: ScoopTestOptions) { 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_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(hash_of_pool_script, pool_nft_name, 1), + |> value.add(pool_script_hash, pool_nft_name, 1), datum: InlineDatum(pool_datum), reference_script: None, }, @@ -473,7 +501,7 @@ fn scoop_swap_deposit(options: ScoopTestOptions) { 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(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, @@ -483,7 +511,7 @@ fn scoop_swap_deposit(options: ScoopTestOptions) { 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)), + |> value.add(pool_script_hash, pool_nft_name, 1)), datum: InlineDatum(pool_out_datum), reference_script: None, } @@ -496,7 +524,7 @@ fn scoop_swap_deposit(options: ScoopTestOptions) { 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) + |> value.add(pool_script_hash, pool_lp_name(pool_id), 9_900_990) ), certificates: [], withdrawals: dict.new(), @@ -520,7 +548,6 @@ fn scoop_swap_deposit(options: ScoopTestOptions) { result } -const hash_of_pool_script = #"00000000000000000000000000000000000000000000000000000000" fn pool_test_tx_input() -> Input { let funds_input = new_tx_input( @@ -532,12 +559,12 @@ fn pool_test_tx_input() -> Input { funds_input } -test mint_test_two_nfts() { +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, hash_of_pool_script, new_pool_nft_token, 1) } }, + mint_test_modify( + fn(output) { Output { ..output, value: value.add(output.value, pool_script_hash, new_pool_nft_token, 1) } }, identity, identity, identity @@ -559,8 +586,7 @@ fn mint_test_modify( modify_ref_output: fn(Output) -> Output, modify_datum: fn(Datum) -> Datum) -> Bool { let settings_policy_id = #"00000000000000000000000000000000000000000000000000000000" - let hash_of_pool_script = #"00000000000000000000000000000000000000000000000000000000" - let pool_address = script_address(hash_of_pool_script) + 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 = @@ -583,14 +609,14 @@ fn mint_test_modify( )) let pool_output_val = value.from_asset(rberry_policy_id, rberry_token_name, 1_000_000_000) - |> value.add(hash_of_pool_script, new_pool_nft_token, 1) + |> 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(hash_of_pool_script, pool_lp_name(pool_id), 1_000_000_000) + 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 @@ -598,14 +624,14 @@ fn mint_test_modify( |> modify_lp_output let ref_output_val = - value.from_asset(hash_of_pool_script, new_pool_ref_token, 1) + 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, NoDatum) // we can probably get rid of the rider, it gets auto added |> add_asset_to_tx_output(ref_output_val) |> modify_ref_output - let poolMintRedeemer = CreatePool { + let pool_mint_redeemer = CreatePool { assets: ((#"", #""), (rberry_policy_id, rberry_token_name)), pool_output: 0, metadata_output: 2, @@ -613,11 +639,11 @@ fn mint_test_modify( let ctx = interval.between(1,2) |> build_txn_context() - |> mint_assets(hash_of_pool_script, value.to_minted_value( + |> mint_assets(pool_script_hash, 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) + |> 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) @@ -627,7 +653,7 @@ fn mint_test_modify( |> add_tx_output(lp_output) |> add_tx_output(pool_output) - let result = pool_validator.mint(settings_policy_id, poolMintRedeemer, ctx) + let result = pool_validator.mint(settings_policy_id, pool_mint_redeemer, ctx) result } @@ -635,3 +661,20 @@ 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 +} \ No newline at end of file