Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into pi/script-strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
Quantumplation committed Mar 6, 2024
2 parents cf369f8 + d5890b1 commit 0f58c80
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 94 deletions.
53 changes: 51 additions & 2 deletions lib/tests/examples/ex_order.ak
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use aiken/cbor
use types/order.{Destination, OrderDatum, Swap, Deposit, Withdrawal, Scoop, Cancel}
use aiken/transaction.{NoDatum}
use types/order.{Destination, OrderDatum, Swap, Deposit, Withdrawal, Strategy, StrategyExecution, SignedStrategyExecution, Signature, Scoop, Cancel}
use aiken/transaction.{NoDatum, OutputReference, TransactionId}
use aiken/interval
use sundae/multisig
use tests/examples/ex_shared.{print_example, wallet_address}

Expand Down Expand Up @@ -87,6 +88,54 @@ test example_withdrawal() {
print_example(mk_withdrawal())
}


fn mk_strategy() {
let addr = wallet_address(#"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513")
let dest = Destination { address: addr, datum: NoDatum }
let strategy = Strategy(Signature(#"d441227553a0f1a965fee7d60a0f724b368dd1bddbc208730fccebcf"))
OrderDatum {
pool_ident: Some(#"fc2c4a6ae8048b0b5affc169dfd496a7ace7d08288c476d9d7a5804e"),
owner: multisig.Signature(
#"c279a3fb3b4e62bbc78e288783b58045d4ae82a18867d8352d02775a",
),
max_protocol_fee: 1000000,
destination: dest,
details: strategy,
extension: Void,
}
}

test example_strategy() {
print_example(mk_strategy())
}

fn mk_strategy_execution() {
let tx_ref = OutputReference(TransactionId(#"797831ec63153a84ae1393bd5fea14196684f1dd12d6485e93cfe373d142e0d3"), 1)
let valid_range = interval.between(1000, 10000)
StrategyExecution {
tx_ref: tx_ref,
validity_range: valid_range,
details: mk_swap().details,
extensions: Void,
}
}

test example_strategy_execution() {
print_example(mk_strategy_execution())
}

fn mk_signed_strategy_execution() {
let sig = #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513"
SignedStrategyExecution {
strategy: mk_strategy_execution(),
signature: Some(sig),
}
}

test example_signed_strategy_execution() {
print_example(mk_signed_strategy_execution())
}

test example_cancel_redeemer() {
print_example(cbor.serialise(Cancel))
}
Expand Down
10 changes: 6 additions & 4 deletions lib/tests/examples/ex_settings.ak
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ use types/settings.{SettingsDatum}
use aiken/transaction.{Input, Output, InlineDatum}
use tests/examples/ex_shared.{print_example, script_address, mk_output_reference}

pub const example_settings_admin = #"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad"

pub fn mk_valid_settings_datum(
scoopers: List<ByteArray>,
) -> SettingsDatum {
SettingsDatum {
settings_admin: multisig.Signature(
#"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad",
example_settings_admin,
),
metadata_admin: Address(
VerificationKeyCredential(
Expand All @@ -20,7 +22,7 @@ pub fn mk_valid_settings_datum(
None,
),
treasury_admin: multisig.Signature(
#"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad",
example_settings_admin,
),
treasury_address: Address(
VerificationKeyCredential(
Expand All @@ -32,7 +34,7 @@ pub fn mk_valid_settings_datum(
authorized_scoopers: Some(scoopers),
authorized_staking_keys: [
VerificationKeyCredential(
#"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad"
example_settings_admin,
),
],
base_fee: 0,
Expand Down Expand Up @@ -70,7 +72,7 @@ pub fn mk_valid_settings_input(
test example_settings_datum() {
print_example(
cbor.serialise(mk_valid_settings_datum([
#"725011d2c296eb3341e159b6c5c6991de11e81062b95108c9aa024ad",
example_settings_admin,
])),
)
}
Expand Down
103 changes: 58 additions & 45 deletions validators/pool.ak
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,10 @@ use aiken/transaction.{
use aiken/transaction/credential.{
Address, Inline, ScriptCredential,
}
use aiken/transaction/value.{MintedValue, PolicyId, Value, ada_policy_id}
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, count_orders, has_exact_token_count, pool_lp_name,
pool_nft_name, spent_output,
}
use shared.{AssetClass, Ident, spent_output, pool_nft_name, pool_lp_name, count_orders}
use sundae/multisig
use types/pool.{
CreatePool, MintLP, PoolDatum, PoolMintRedeemer, PoolRedeemer, PoolScoop,
Expand Down Expand Up @@ -405,18 +402,12 @@ validator(settings_policy_id: PolicyId) {
let mint_is_correct =
value.from_minted_value(ctx.transaction.mint) == expected_mint

// Confirm that the correct funds (at least one asset A, at least one asset B, exactly 3 tokens, including the pool NFT) is correct here
// Note: should we instead just compare the expected pool output?
let funds_spent_to_pool = and {
coin_a_amt_sans_protocol_fees >= 1,
coin_b_amt >= 1,
list.length(value.flatten(pool_output.value)) <= 3,
value.quantity_of(
pool_output.value,
own_policy_id,
new_pool_nft_token,
) == 1
}
// Confirm that the correct funds (asset A, asset B, the correct amount of ADA, and the pool NFT) get paid to the pool output
let funds_spent_to_pool = has_expected_pool_value(own_policy_id, new_pool_id, pool_output.value, PoolState {
quantity_a: (asset_a.1st, asset_a.2nd, coin_a_amt_sans_protocol_fees),
quantity_b: (asset_b.1st, asset_b.2nd, coin_b_amt),
quantity_lp: (own_policy_id, new_pool_lp_token, initial_lq),
}, pool_output_datum.protocol_fees)

// 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
Expand All @@ -439,13 +430,19 @@ validator(settings_policy_id: PolicyId) {
// - the pool identifier is set correctly
// - the assets is set correctly
// - the initial circulating supply is set correctly
// - and the market open time is before the fee finalized time; TODO: should we relax this?
// - the market open time is before the fee finalized time; TODO: should we relax this?
// I'm not sure it's harmful if someone initializes this with a feeFinalized in the past
// - the initial and final fees per 10,000 are both non-negative (>= 0%)
// - the intitial and final fees per 10,000 are both less than or equal to 10000 (<= 100%)
let pool_output_datum_correct = and {
pool_output_datum.identifier == new_pool_id,
pool_output_datum.assets == (asset_a, asset_b),
pool_output_datum.circulating_lp == initial_lq,
pool_output_datum.market_open <= pool_output_datum.fee_finalized,
pool_output_datum.fees_per_10_thousand.1st >= 0,
pool_output_datum.fees_per_10_thousand.2nd >= 0,
pool_output_datum.fees_per_10_thousand.1st <= 10000,
pool_output_datum.fees_per_10_thousand.2nd <= 10000,
}

// Make sure that the pool output is paid into own_policy_id (the pool script, remember this is a multivalidator)
Expand Down Expand Up @@ -521,7 +518,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,
Expand All @@ -533,33 +530,49 @@ fn has_expected_pool_value(
let (quantity_b_policy_id, quantity_b_name, quantity_b_amt) = quantity_b
// Asset A *could* be ADA; in which case there should be 3 tokens on the output
// (ADA, Asset B, and the NFT)
// We do this as an optimization, since constructing values is expensive
// OPTIMIZATION: is it possible to check this with a single traversal? Each quantity_of represents a small linear scan
if quantity_a_policy_id == ada_policy_id {
and {
has_exact_token_count(output_value, 3),
value.lovelace_of(output_value) == final_protocol_fees + quantity_a_amt,
value.quantity_of(output_value, quantity_b_policy_id, quantity_b_name) == quantity_b_amt,
value.quantity_of(
output_value,
pool_script_hash,
pool_nft_name(identifier),
) == 1,
}
let actual = list.foldl(
value.flatten(output_value),
// (token count, lovelace amount, token b amount, pool nft amount)
(0, 0, 0, 0),
fn (asset, acc) {
let token_count = acc.1st + 1
if asset.1st == quantity_a_policy_id {
(token_count, acc.2nd + asset.3rd, acc.3rd, acc.4th)
} else if asset.1st == quantity_b_policy_id && asset.2nd == quantity_b_name {
(token_count, acc.2nd, acc.3rd + asset.3rd, acc.4th)
} else {
expect asset == (pool_script_hash, pool_nft_name(identifier), 1)
(token_count, acc.2nd, acc.3rd, acc.4th + 1)
}
}
)
let expected = (3, final_protocol_fees + quantity_a_amt, quantity_b_amt, 1)
// 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
expected == actual
} else {
// Otherwise, we expect 4 tokens (ADA, Asset A, Asset B, and the NFT)
// and the corresponding values
and {
has_exact_token_count(output_value, 4),
value.lovelace_of(output_value) == final_protocol_fees,
value.quantity_of(output_value, quantity_a_policy_id, quantity_a_name) == quantity_a_amt,
value.quantity_of(output_value, quantity_b_policy_id, quantity_b_name) == quantity_b_amt,
value.quantity_of(
output_value,
pool_script_hash,
pool_nft_name(identifier),
) == 1,
}
// 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),
// (token count, lovelace amount, token a amount, token b amount, pool nft amount)
(0, 0, 0, 0, 0),
fn (asset, acc) {
let token_count = acc.1st + 1
if asset.1st == ada_policy_id && asset.2nd == ada_asset_name {
(token_count, acc.2nd + asset.3rd, acc.3rd, acc.4th, acc.5th)
} else if asset.1st == quantity_a_policy_id && asset.2nd == quantity_a_name {
(token_count, acc.2nd, acc.3rd + asset.3rd, acc.4th, acc.5th)
} else if asset.1st == quantity_b_policy_id && asset.2nd == quantity_b_name {
(token_count, acc.2nd, acc.3rd, acc.4th + asset.3rd, acc.5th)
} else {
expect asset == (pool_script_hash, pool_nft_name(identifier), 1)
(token_count, acc.2nd, acc.3rd, acc.4th, acc.5th + 1)
}
}
)
}
}

Expand All @@ -578,4 +591,4 @@ fn compare_asset_class(a: AssetClass, b: AssetClass) {
pub fn int_to_ident(n: Int) -> Ident {
expect n < 256
bytearray.push(#"", n)
}
}
59 changes: 27 additions & 32 deletions validators/settings.ak
Original file line number Diff line number Diff line change
Expand Up @@ -10,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) {
pub fn spend(d: SettingsDatum, redeemer: SettingsRedeemer, ctx: ScriptContext) {
pub fn spend(input_datum: 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
Expand All @@ -28,10 +28,11 @@ validator(protocol_boot_utxo: OutputReference) {
expect output_datum: SettingsDatum = output_datum

// We check that the value on the input and output are equal, to ensure the settings NFT gets paid back into the output
// TODO: should we allow the ADA to change, just in case the minUTXO changes? that would allow us to add or reclaim
// ADA if minUTXO changed
// We compare `without_lovelace` to leave full freedom for ADA to be added/removed as the minUTXO requirement changes
// Note that this can only be spent by the SettingsAdmin or TreasuryAdmin, so we won't leak a small amount of ADA to arbitrary users
// Also, it is not expected that the ADA ever be more than the minUTXO cost, so this doesn't expose hundreds of ADA (for example) to the treasury admin
let value_not_changed =
own_output.value == own_input.value
value.without_lovelace(own_output.value) == value.without_lovelace(own_input.value)

// Make sure we don't mint anything, otherwise someone might mint another settings token
let no_mint =
Expand All @@ -48,58 +49,53 @@ validator(protocol_boot_utxo: OutputReference) {
// so large that it exceeds execution units?
let signed_by_admin =
multisig.satisfied(
d.settings_admin,
input_datum.settings_admin,
ctx.transaction.extra_signatories,
ctx.transaction.validity_range,
)

// Settings admin can change any datum fields except for these
// TODO: This code is kind of hard to read: it's checking that some fields are *unchanged*
// but the way we think about admin controls is in what they *are* allowed to change
// one way to make this clearer is to construct an "allowed datum", by using
// fields allowed to change from the output, and fields not allowed to change from the input
// then comparing it to the output
let datum_updated_legally =
output_datum.authorized_staking_keys == d.authorized_staking_keys
&& output_datum.treasury_address == d.treasury_address
&& output_datum.treasury_allowance == d.treasury_allowance
let allowed_datum = SettingsDatum {
// Most fields *can* be updated by the admin, so we start from the output datum
..output_datum,
// But ensure that these fields haven't been changed
authorized_staking_keys: input_datum.authorized_staking_keys,
treasury_address: input_datum.treasury_address,
treasury_allowance: input_datum.treasury_allowance,
}
// TODO: move base_fee, simple_fee, and strategy_fee to the treasury admin instead, maybe?
// TODO: enforce maximum scooper keys to some large N, to prevent locking the datum?

and {
signed_by_admin,
datum_updated_legally,
output_datum == allowed_datum,
value_not_changed,
no_mint,
}
}
TreasuryAdminUpdate -> {
let signed_by_admin =
multisig.satisfied(
d.treasury_admin,
input_datum.treasury_admin,
ctx.transaction.extra_signatories,
ctx.transaction.validity_range,
)

// Treasury admin can change any datum fields except for these
// i.e. can change the treasury address and treasury allowance
// TODO: similar to above, and even more eggregiously here, it's confusing to parse this
// because we normally think about what the admin *can* change, not what they *can't* change
let datum_updated_legally =
output_datum.authorized_staking_keys == d.authorized_staking_keys
&& output_datum.settings_admin == d.settings_admin
&& output_datum.metadata_admin == d.metadata_admin
&& output_datum.treasury_admin == d.treasury_admin
&& output_datum.authorized_scoopers == d.authorized_scoopers
&& output_datum.base_fee == d.base_fee
&& output_datum.simple_fee == d.simple_fee
&& output_datum.strategy_fee == d.strategy_fee
&& output_datum.extensions == d.extensions
// i.e. can change the treasury address, treasury allowance, and the authorized staking keys
let allowed_datum = SettingsDatum {
// Most of the fields can't change, so we start from the input datum
..input_datum,
// These three can be updated, so we pull whatever is in the output datum
authorized_staking_keys: output_datum.authorized_staking_keys,
treasury_address: output_datum.treasury_address,
treasury_allowance: output_datum.treasury_allowance,
}
// TODO: enforce maximum staking keys to some large N, to prevent locking the Datum

and {
signed_by_admin,
datum_updated_legally,
output_datum == allowed_datum,
value_not_changed,
no_mint,
}
Expand Down Expand Up @@ -140,5 +136,4 @@ validator(protocol_boot_utxo: OutputReference) {
pays_to_settings_script,
}
}
}

}
Loading

0 comments on commit 0f58c80

Please sign in to comment.