From fa25f977ef6a981e09918ea09d7e1d6650879463 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Tue, 24 Sep 2024 14:39:36 +0200 Subject: [PATCH] Simplify spending validator by always requiring admin approval In the initial version of the contract, the delegate would sometimes have to spend from the validator, and was allowed to do so under specific conditions (assets are strictly forwarded, etc..). This was no longer needed, but kept, making the validator needlessly more complex than it needs to be. Less code means less rooms for mistakes, so it's better removed. --- aiken.toml | 2 +- lib/zhuli/predicate.ak | 59 ++++++-------- validators/zhuli.test.ak | 164 +++++++++++++++------------------------ 3 files changed, 85 insertions(+), 140 deletions(-) diff --git a/aiken.toml b/aiken.toml index 27acb83..85acd3b 100644 --- a/aiken.toml +++ b/aiken.toml @@ -22,7 +22,7 @@ source = "github" [[dependencies]] name = "aiken-lang/fuzz" -version = "v2" +version = "v2.1.0" source = "github" [config.default] diff --git a/lib/zhuli/predicate.ak b/lib/zhuli/predicate.ak index a0c6861..c453789 100644 --- a/lib/zhuli/predicate.ak +++ b/lib/zhuli/predicate.ak @@ -6,7 +6,7 @@ use aiken/collection/dict use aiken/collection/list use aiken/crypto.{ScriptHash} use cardano/address.{Address, Credential, Inline, Script} -use cardano/assets.{Lovelace, PolicyId, Value, ada_policy_id} +use cardano/assets.{PolicyId, Value, ada_policy_id} use cardano/certificate.{ RegisterDelegateRepresentative, UnregisterDelegateRepresentative, } @@ -59,25 +59,19 @@ pub fn must_forward_script( } // Ensure that the contract's assets follow. + // // Note that there might be more UTxOs locked at the validator address; which - // is handled down below. Under normal use of the protocol, however, we - // should only ever see Ada and our minting policy. + // is why we can't simply look at assets under `from`, but must look the + // whole UTxO. Under normal use of the protocol, however, we should only ever + // see Ada and our minting policy. // // Anything else is treated as an anomaly and must be approved by the - // administrator. Note however that even the administrator can't take away - // the assets from the script. - let (from_lovelaces, from_assets, has_foreign) = - partition_assets(self.inputs, our_policy_id) - - let authorize_special_operation = - if assets.lovelace_of(to) < from_lovelaces || has_foreign? { - must_be_approved_by_administrator(self, administrator)? - } else { - True - } + // administrator anyway. Note however that even the administrator can't take + // away the assets from the script. + let from_assets = total_value_restricted_by(self.inputs, our_policy_id) and { - authorize_special_operation?, + must_be_approved_by_administrator(self, administrator)?, (assets.without_lovelace(to) == from_assets)?, } } @@ -102,39 +96,32 @@ pub fn must_forward_strict_assets( } } -pub fn partition_assets( +/// Compute the total value from a UTxO and a script hash restricted to: +/// +/// - Assets owned by that script hash +/// - Assets minted by that same script hash +pub fn total_value_restricted_by( utxo: List, our_policy_id: PolicyId, -) -> (Lovelace, Value, Bool) { +) -> Value { list.foldr( utxo, - (0, assets.zero, False), - fn(input, (our_lovelaces, our_assets, has_foreign)) { + assets.zero, + fn(input, our_assets) { if input.output.address.payment_credential == Script(our_policy_id) { assets.reduce( input.output.value, - (our_lovelaces, our_assets, has_foreign), - fn( - policy_id, - asset_name, - quantity, - (our_lovelaces, our_assets, has_foreign), - ) { - if policy_id == ada_policy_id { - (our_lovelaces + quantity, our_assets, has_foreign) - } else if policy_id == our_policy_id { - ( - our_lovelaces, - assets.add(our_assets, policy_id, asset_name, quantity), - has_foreign, - ) + our_assets, + fn(policy_id, asset_name, quantity, our_assets) { + if policy_id == our_policy_id { + assets.add(our_assets, policy_id, asset_name, quantity) } else { - (our_lovelaces, our_assets, True) + our_assets } }, ) } else { - (our_lovelaces, our_assets, has_foreign) + our_assets } }, ) diff --git a/validators/zhuli.test.ak b/validators/zhuli.test.ak index 8d51076..7e413a4 100644 --- a/validators/zhuli.test.ak +++ b/validators/zhuli.test.ak @@ -8,7 +8,7 @@ use aiken/collection/list/extra.{insert} use aiken/collection/pairs use aiken/crypto.{ScriptHash, VerificationKeyHash} use aiken/fuzz.{ - and_then, bytearray_between, constant, int_at_least, int_between, label, + and_then, bytearray_between, constant, int_at_least, int_between, label_if, label_when, list_between, map, } use aiken/fuzz/scenario.{Done, Label, Scenario} @@ -72,41 +72,39 @@ const default_state = // ------------------------------------------------------------------- scenarios -const sc_missing_admin_approval_for_foreign_assets: String = - @"Missing administrator approval for foreign assets" +const sc_missing_admin_approval_for_transfer: String = + @"x missing administrator approval for transfer" const sc_missing_admin_approval_for_register: String = - @"Missing administrator approval for registration" + @"x missing administrator approval for registration" const sc_missing_admin_approval_for_unregister: String = - @"Missing administrator approval for unregistration" - -const sc_insufficient_lovelace_forwarded: String = - @"Insufficient lovelace quantity forwarded" + @"x missing administrator approval for unregistration" const sc_lock_without_delegation: String = - @"Locked without delegation credentials" + @"x locked without delegation credentials" const sc_initialize_illegal_quantity: String = - @"Initialized state token with illegal quantity" + @"x initialized state token with illegal quantity" const sc_initialize_illegal_asset_name: String = - @"Initialized state token with illegal asset name" + @"x initialized state token with illegal asset name" -const sc_no_required_initial_output: String = @"No initial output when required" +const sc_no_required_initial_output: String = + @"x no initial output when required" -const sc_too_many_initial_outputs: String = @"Too many initial outputs" +const sc_too_many_initial_outputs: String = @"x too many initial outputs" -const sc_untrapped_minted_tokens: String = @"Untrapped state tokens" +const sc_untrapped_minted_tokens: String = @"x untrapped state tokens" -const sc_escaping_state_tokens: String = @"Escaping state tokens" +const sc_escaping_state_tokens: String = @"x escaping state tokens" -const sc_forwarded_noise: String = @"Forwarded noise assets" +const sc_forwarded_noise: String = @"x forwarded noise assets" -const sc_too_many_forwarding_outputs: String = @"Too many forwarding outputs" +const sc_too_many_forwarding_outputs: String = @"x too many forwarding outputs" const sc_missing_certificate_with_mint: String = - @"Missing certificates during mint/burn" + @"x missing certificates during mint/burn" // ------------------------------------------------------------------ properties @@ -187,10 +185,10 @@ fn post_conditions(steps: List) { }, ) - @"contains solo registration" |> label_if(is_register) - @"contains re-registration" |> label_if(is_reregister) - @"contains solo unregistration" |> label_if(is_unregister) - @"contains forward-only" |> label_if(is_forward) + @"✓ solo registration" |> label_if(is_register) + @"✓ re-registration" |> label_if(is_reregister) + @"✓ solo unregistration" |> label_if(is_unregister) + @"✓ forward-only" |> label_if(is_forward) // Analyze minting let minted = @@ -232,14 +230,6 @@ fn post_conditions(steps: List) { } } -fn label_if(str: String, predicate: Bool) { - if predicate { - label(str) - } else { - Void - } -} - // ------------------------------------------------------------------ generators // TODO: Voting @@ -260,7 +250,7 @@ fn step(st: State, utxo: List) -> Fuzzer> { scenario.fork( if st.registered && !st.unregistered { // Attempts to unregister - 208 + 160 } else { // Only forward 0 @@ -301,7 +291,7 @@ fn register( if lovelace > 1 { let removed <- map(int_between(1, lovelace - 1)) ( - [sc_insufficient_lovelace_forwarded, ..labels], + labels, assets.add(our_assets, ada_policy_id, ada_asset_name, -removed), ) } else { @@ -359,24 +349,29 @@ fn register( ) } - let labels = - if utxo_holds_foreign_assets(utxo) { - [sc_missing_admin_approval_for_foreign_assets, ..labels] - } else { - labels - } - let st = State { ..st, registered: is_minting } if list.is_empty(certs) { - constant(Scenario(labels, st, tx)) + finalize_with_signature( + 245, + labels, + st, + tx, + fn(labels) { + if our_assets == assets.zero { + labels + } else { + [sc_missing_admin_approval_for_transfer, ..labels] + } + }, + ) } else { finalize_with_signature( 240, labels, st, tx, - sc_missing_admin_approval_for_register, + fn(labels) { [sc_missing_admin_approval_for_register, ..labels] }, ) } } @@ -392,7 +387,7 @@ fn unregister( let (mint, mint_redeemers) <- and_then( scenario.fork( - 208, + 128, // Replace a delegate with another. fn() { constant( @@ -492,7 +487,7 @@ fn unregister( labels, State { ..st, unregistered: True }, tx, - sc_missing_admin_approval_for_unregister, + fn(labels) { [sc_missing_admin_approval_for_unregister, ..labels] }, ) } @@ -524,15 +519,12 @@ fn forward_assets( let all_lovelace = total_lovelace(utxo) - let (labels, outputs, forwarded_lovelace) <- + let (labels, outputs) <- and_then( scenario.fork( - 240, + 220, fn() { - // Most of the time, forward all input lovelaces. Sometimes, forward less. Note that - // this isn't an immediate K.O. scenario because it depends whether the transaction is - // authorized by the administrators or not. So the label is applied down below, depending - // on this fact. + // Most of the time, forward all input lovelaces. Sometimes, forward less. let forwarded_lovelace <- and_then( scenario.fork( @@ -663,16 +655,16 @@ fn forward_assets( }, ) - constant((labels, outputs, forwarded_lovelace)) + constant((labels, outputs)) }, fn() { // Otherwise, distribute all assets at random which should // be caught and rejected by the spend validator. let outputs <- map(any_split(all_value)) if assets.tokens(all_value, validator_hash) != dict.empty { - ([sc_escaping_state_tokens, ..labels], outputs, 0) + ([sc_escaping_state_tokens, ..labels], outputs) } else { - (labels, outputs, 0) + (labels, outputs) } }, ), @@ -690,34 +682,19 @@ fn forward_assets( redeemers: spend_redeemers, } - // NOTE: A signature from the administrator might be needed in two cases: - // - // - We are trying to spend assets that do long belong to the validator - // - We are trying to re-allocate some lovelace quantity - // - // So we do sign conditionally, but we distinguish the two failing scenarios - // to generate better errors/reporting. - if utxo_holds_foreign_assets(our_utxo) { - finalize_with_signature( - 128, - labels, - st, - tx, - sc_missing_admin_approval_for_foreign_assets, - ) - } else if our_lovelace > 0 && forwarded_lovelace < our_lovelace { - finalize_with_signature( - 220, - labels, - st, - tx, - sc_insufficient_lovelace_forwarded, - ) - } else { - // NOTE: This else branch is *not necessarily* O.K. as it depends on - // the way we constructed the output which is captured within the 'labels' - constant(Scenario(labels, st, tx)) - } + finalize_with_signature( + 220, + labels, + st, + tx, + fn(labels) { + if our_utxo == [] { + labels + } else { + [sc_missing_admin_approval_for_transfer, ..labels] + } + }, + ) } /// Generate a random distribution of outputs given a total value. @@ -750,13 +727,7 @@ fn any_initial_outputs( (labels, [output]) }, // Generate 0 initial outputs - fn() { - if assets.lovelace_of(in) > 0 { - constant(([sc_insufficient_lovelace_forwarded, ..labels], [])) - } else { - constant((labels, [])) - } - }, + fn() { constant((labels, [])) }, fn() { // Generate many (>= 2) initial outputs. let (fst_labels, fst_output) <- @@ -942,7 +913,7 @@ fn finalize_with_signature( labels: List