diff --git a/aiken.lock b/aiken.lock index 65a6d57..00a9bea 100644 --- a/aiken.lock +++ b/aiken.lock @@ -46,4 +46,4 @@ requirements = [] source = "github" [etags] -"aiken-lang/fuzz@main" = [{ secs_since_epoch = 1753181341, nanos_since_epoch = 812689792 }, "9843473958e51725a9274b487d2d4aac0395ec1a2e30f090724fa737226bc127"] +"aiken-lang/fuzz@main" = [{ secs_since_epoch = 1758102049, nanos_since_epoch = 210673971 }, "9843473958e51725a9274b487d2d4aac0395ec1a2e30f090724fa737226bc127"] diff --git a/lib/tests/examples/ex_settings.ak b/lib/tests/examples/ex_settings.ak index 700d475..1ab7d4e 100644 --- a/lib/tests/examples/ex_settings.ak +++ b/lib/tests/examples/ex_settings.ak @@ -1,11 +1,10 @@ -use aiken/cbor use cardano/address.{Address, Script, VerificationKey} use cardano/assets use cardano/transaction.{InlineDatum, Input, Output} use sundae/multisig use tests/examples/ex_shared.{mk_output_reference, print_example, script_address} -use types/settings.{SettingsDatum} +use types/settings.{ProtocolFeeBasisPointsExtension, SettingsDatum} pub const example_settings_admin = #"6313a1d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad" @@ -32,7 +31,14 @@ pub fn mk_valid_settings_datum(scoopers: List) -> SettingsDatum { simple_fee: 2_500_000, strategy_fee: 5_000_000, pool_creation_fee: 0, - extensions: Void, + extensions: [ + Pair( + 0, + as_data( + ProtocolFeeBasisPointsExtension { protocol_fee_basis_points: (0, 0) }, + ), + ), + ], } } @@ -57,9 +63,7 @@ pub fn mk_valid_settings_input(scoopers: List, ix: Int) -> Input { } test example_settings_datum() { - print_example( - cbor.serialise(mk_valid_settings_datum([example_settings_admin])), - ) + print_example(mk_valid_settings_datum([example_settings_admin])) } test example_big_settings_datum() { @@ -207,7 +211,16 @@ test example_mainnet_boot_settings_datum() { simple_fee: 168_000, strategy_fee: 168_000, pool_creation_fee: 0, - extensions: 0, + extensions: [ + Pair( + 0, + as_data( + ProtocolFeeBasisPointsExtension { + protocol_fee_basis_points: (0, 0), + }, + ), + ), + ], }, ) } diff --git a/lib/types/settings.ak b/lib/types/settings.ak index f331ce0..9078763 100644 --- a/lib/types/settings.ak +++ b/lib/types/settings.ak @@ -1,5 +1,6 @@ use aiken/builtin use aiken/collection/dict +use aiken/collection/pairs use aiken/crypto.{VerificationKey} use cardano/address.{Address, Credential} use cardano/assets.{AssetName, PolicyId} @@ -67,7 +68,12 @@ pub type SettingsDatum { /// - publish data for convenient and canonical access by off-chain actors /// - expose data to the script conditions in the multisig scripts of the two administrator roles /// - expose additional settings for more advanced pool and order types to operate off of - extensions: Data, + extensions: Pairs, +} + +pub type ProtocolFeeBasisPointsExtension { + /// The default protocol fee basis points to charge on each trade for bid (A -> B) and ask (B -> A) orders, for pools that don't override it + protocol_fee_basis_points: (Int, Int), } /// The settings redeemer can be spent for two different purposes @@ -81,6 +87,19 @@ pub type SettingsRedeemer { /// The name of the token that authenticates the settings UTXO pub const settings_nft_name: AssetName = "settings" +pub fn find_protocol_fee_extension( + settings: SettingsDatum, +) -> Option { + let maybe_extension = pairs.get_first(settings.extensions, 0) + when maybe_extension is { + Some(extension_data) -> { + expect extension: ProtocolFeeBasisPointsExtension = extension_data + Some(extension) + } + None -> None + } +} + /// Scan over the list of reference inputs to find the settings datum, and ensure it's the correct one /// Note that this makes the assumption that the settings datum is the first reference input, for performance /// This means that when storing reference scripts on-chain, there needs to be a small amount of "farming" to select diff --git a/validators/pool.ak b/validators/pool.ak index ce40465..9ff7321 100644 --- a/validators/pool.ak +++ b/validators/pool.ak @@ -24,7 +24,10 @@ use types/pool.{ MintLP, PoolMintRedeemer, PoolRedeemer, PoolScoop, StablePoolDatum, UpdatePoolFees, WithdrawFees, } as types_pool -use types/settings.{SettingsDatum, find_settings_datum} +use types/settings.{ + ProtocolFeeBasisPointsExtension, SettingsDatum, find_protocol_fee_extension, + find_settings_datum, +} /// An implementation of a curve-like "Stableswap" invariant that enables more efficient trades for stablecoin pairs. /// @@ -461,9 +464,9 @@ validator pool( reserve_a } - // We follow Curve's model for initial liquidity, by setting it to the initial sum invariant + // We follow Curve's model for initial liquidity, by setting it to the initial sum invariant, but divide by precision to avoid very large numbers potentially causing overflow expect - pool_output_datum.sum_invariant == pool_output_datum.circulating_lp + pool_output_datum.sum_invariant / calc_precision == pool_output_datum.circulating_lp // And check that we mint the correct tokens, and nothing else. let expected_mint = @@ -518,6 +521,9 @@ validator pool( // and potentially even on access UIs for the Sundae protocol expect metadata_output.datum == InlineDatum(Void) + expect Some(protocol_fee_extension) = + find_protocol_fee_extension(settings_datum) + // 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. // A datum is valid so long as @@ -541,6 +547,7 @@ validator pool( shared.fees_in_legal_range( pool_output_datum.protocol_fee_basis_points.2nd, ), + protocol_fee_extension.protocol_fee_basis_points == pool_output_datum.protocol_fee_basis_points, pool_output_datum.linear_amplification > 0, liquidity_invariant( reserve_a * calc_precision, @@ -901,7 +908,8 @@ validator manage(settings_policy_id: PolicyId) { pool_output_address.payment_credential == Script(pool_script_hash) // As part of withdrawing, we should decrease the protocol fees by the amount we're withdrawing - // but, importantly, *nothing else*; so we construct a datum with everything from the initial datum, plus the protofol fees updated + // but, the treasury admin is also allowed to update the protocol fees basis points, but nothing else in the datum + // is allowed to be changed. let expected_datum = StablePoolDatum { ..datum, @@ -910,6 +918,7 @@ validator manage(settings_policy_id: PolicyId) { initial_fee_a - amount.2nd, initial_fee_b - amount.3rd, ), + protocol_fee_basis_points: output_datum.protocol_fee_basis_points, } expect output_datum == expected_datum @@ -957,8 +966,6 @@ validator manage(settings_policy_id: PolicyId) { let StablePoolDatum { lp_fee_basis_points, - protocol_fee_basis_points, - protocol_fees, fee_manager: output_fee_manager, .. } = pool_output_datum @@ -966,8 +973,6 @@ validator manage(settings_policy_id: PolicyId) { // Make sure we don't update the fees to negative or above 100% expect shared.fees_in_legal_range(lp_fee_basis_points.1st) expect shared.fees_in_legal_range(lp_fee_basis_points.2nd) - expect shared.fees_in_legal_range(protocol_fee_basis_points.1st) - expect shared.fees_in_legal_range(protocol_fee_basis_points.2nd) // Check that the *current* fee manager approves the update expect Some(fee_manager) = datum.fee_manager @@ -983,12 +988,11 @@ validator manage(settings_policy_id: PolicyId) { StablePoolDatum { ..datum, protocol_fees: ( - protocol_fees.1st + lovelace_diff, - protocol_fees.2nd, - protocol_fees.3rd, + datum.protocol_fees.1st + lovelace_diff, + datum.protocol_fees.2nd, + datum.protocol_fees.3rd, ), lp_fee_basis_points: lp_fee_basis_points, - protocol_fee_basis_points: protocol_fee_basis_points, fee_manager: output_fee_manager, } pool_output_datum == expected_datum @@ -1010,7 +1014,6 @@ validator manage(settings_policy_id: PolicyId) { // We need the pool output to check that only the fees or fee manager are updated let StablePoolDatum { assets, - protocol_fees, linear_amplification, sum_invariant, linear_amplification_manager: output_linear_amplification_manager, @@ -1020,7 +1023,7 @@ validator manage(settings_policy_id: PolicyId) { let pool_quantity_a, pool_quantity_b, - <- pool_input_to_asset_reserves(assets, protocol_fees, pool_input) + <- pool_input_to_asset_reserves(assets, datum.protocol_fees, pool_input) expect liquidity_invariant( @@ -1048,9 +1051,9 @@ validator manage(settings_policy_id: PolicyId) { StablePoolDatum { ..datum, protocol_fees: ( - protocol_fees.1st + lovelace_diff, - protocol_fees.2nd, - protocol_fees.3rd, + datum.protocol_fees.1st + lovelace_diff, + datum.protocol_fees.2nd, + datum.protocol_fees.3rd, ), linear_amplification: linear_amplification, sum_invariant: sum_invariant, diff --git a/validators/tests/pool.ak b/validators/tests/pool.ak index 752b056..619ce3a 100644 --- a/validators/tests/pool.ak +++ b/validators/tests/pool.ak @@ -15,7 +15,7 @@ use cardano/transaction.{ Transaction, Withdraw, } use pool as pool_validator -use shared.{get_D, pool_lp_name} +use shared.{calc_precision, get_D, pool_lp_name} use sundae/multisig use tests/constants use tests/examples/ex_settings.{ @@ -36,7 +36,9 @@ use types/pool.{ BurnPool, CreatePool, Manage, PoolMintRedeemer, PoolScoop, StablePoolDatum, UpdatePoolFees, WithdrawFees, } -use types/settings.{SettingsDatum, settings_nft_name} +use types/settings.{ + SettingsDatum, settings_nft_name, +} type ScoopTestOptions { edit_order_1_in_value: Option, @@ -381,7 +383,7 @@ test scooper_not_in_settings() fail { simple_fee: 2_500_000, strategy_fee: 5_000_000, pool_creation_fee: 0, - extensions: Void, + extensions: [], }, ), ), @@ -1345,6 +1347,7 @@ fn mint_test_modify( let reserve_b = 1_000_000_000 let linear_amplification = 10 let sum_invariant = get_D(linear_amplification, reserve_a, reserve_b) + let initial_lp = sum_invariant / calc_precision let inline_pool_datum = modify_datum( InlineDatum( @@ -1354,7 +1357,7 @@ fn mint_test_modify( (ada_policy_id, ada_asset_name), (constants.rberry_policy, constants.rberry_asset_name), ), - circulating_lp: sum_invariant, + circulating_lp: initial_lp, lp_fee_basis_points: (5, 5), protocol_fee_basis_points: (0, 0), fee_manager: None, @@ -1384,7 +1387,7 @@ fn mint_test_modify( assets.from_asset( constants.pool_script_hash, pool_lp_name(pool_id), - sum_invariant, + initial_lp, ) |> assets.merge(assets.from_lovelace(2_000_000)) let lp_output = @@ -1424,7 +1427,7 @@ fn mint_test_modify( |> assets.add( constants.pool_script_hash, new_pool_lp_token, - sum_invariant, + initial_lp, ) |> assets.add(constants.pool_script_hash, new_pool_nft_token, 1) |> assets.add(constants.pool_script_hash, new_pool_ref_token, 1), diff --git a/validators/tests/pool.manage.ak b/validators/tests/pool.manage.ak index 4a720d0..6100e40 100644 --- a/validators/tests/pool.manage.ak +++ b/validators/tests/pool.manage.ak @@ -388,7 +388,7 @@ fn scenario_settings_input_baseline( simple_fee: 2_500_000, strategy_fee: 5_000_000, pool_creation_fee: 0, - extensions: Void, + extensions: [], } }, ) diff --git a/validators/tests/settings.ak b/validators/tests/settings.ak index 068b837..f8cbc03 100644 --- a/validators/tests/settings.ak +++ b/validators/tests/settings.ak @@ -92,7 +92,7 @@ fn mk_valid_settings_datum(scoopers: List) -> SettingsDatum { simple_fee: 2_500_000, strategy_fee: 5_000_000, pool_creation_fee: 0, - extensions: Void, + extensions: [], } }