From 95c1f2b318af582dcaa308c14d4b31c8233210a6 Mon Sep 17 00:00:00 2001 From: Franco Luque Date: Wed, 29 Jan 2025 18:20:40 -0300 Subject: [PATCH] fix: remove onchain vulnerability in gather fuel (fuel can be stolen) (#109) --- onchain/src/validators/spacetime.ak | 13 +++- .../src/validators/tests/spacetime/gather.ak | 59 ++++++++++++------- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/onchain/src/validators/spacetime.ak b/onchain/src/validators/spacetime.ak index 448a057..f3303a7 100644 --- a/onchain/src/validators/spacetime.ak +++ b/onchain/src/validators/spacetime.ak @@ -1,5 +1,6 @@ use aiken/collection/dict use aiken/collection/list +use aiken/collection/pairs use aiken/interval.{Finite} use aiken/math/rational use aiken/option @@ -8,14 +9,14 @@ use aiken/primitive/string use asteria/types.{ AssetClass, AsteriaDatum, BurnShip, GatherFuel, MineAsteria, MintShip, MoveShip, PelletDatum, Quit, ScriptAddress, ShipDatum, ShipRedeemer, - ShipyardRedeemer, Speed, + ShipyardRedeemer, Speed, Provide, } use asteria/utils use cardano/address.{Address, Script, VerificationKey} use cardano/assets.{ PolicyId, add, flatten, from_asset, quantity_of, tokens, zero, } -use cardano/transaction.{InlineDatum, OutputReference, Transaction, find_input} +use cardano/transaction.{InlineDatum, OutputReference, Transaction, Spend, find_input} validator spacetime( pellet_validator_address: ScriptAddress, @@ -34,7 +35,7 @@ validator spacetime( utxo: OutputReference, self: Transaction, ) { - let Transaction { inputs, outputs, mint, validity_range, .. } = self + let Transaction { inputs, outputs, mint, validity_range, redeemers, .. } = self expect Some(datum) = datum let ShipDatum { pos_x, @@ -171,6 +172,12 @@ validator spacetime( quantity_of(ship_output.value, shipyard_policy, ship_token_name) == 1 let must_add_fuel_tokens = output_fuel == input_fuel + amount let must_hold_3_assets = list.length(ship_output.value |> flatten) == 3 + + // gathered fuel amount must be equal to amount provided by pellet + let pellet_purpose = Spend(pellet_input.output_reference) + let pellet_redeemer: Data = Provide(amount) + expect pairs.get_all(redeemers, pellet_purpose) == [pellet_redeemer] + let must_spend_two_script_inputs = list.length( list.filter( diff --git a/onchain/src/validators/tests/spacetime/gather.ak b/onchain/src/validators/tests/spacetime/gather.ak index 19f7d90..3672cd9 100644 --- a/onchain/src/validators/tests/spacetime/gather.ak +++ b/onchain/src/validators/tests/spacetime/gather.ak @@ -1,7 +1,7 @@ use aiken/collection/dict use aiken/interval.{Finite, Interval, IntervalBound} use asteria/test_mock as mock -use asteria/types.{AssetClass, GatherFuel, PelletDatum, ShipDatum, Speed} +use asteria/types.{AssetClass, GatherFuel, PelletDatum, ShipDatum, Speed, Provide} use cardano/address.{Address, Script, VerificationKey} use cardano/assets.{add, from_asset, from_lovelace, zero} use cardano/transaction.{ @@ -14,7 +14,8 @@ use spacetime // ============================================================================================== type GatherTestOptions { - provided_amount: Int, + ship_added_amount: Int, + pellet_provided_amount: Int, ship_initial_fuel: Int, ship_pos_x: Int, ship_pos_y: Int, @@ -31,7 +32,8 @@ type GatherTestOptions { fn default_gather_options() { GatherTestOptions { - provided_amount: 10, + ship_added_amount: 10, + pellet_provided_amount: 10, ship_initial_fuel: 40, ship_pos_x: 10, ship_pos_y: 10, @@ -67,7 +69,8 @@ fn gather(options: GatherTestOptions) -> Bool { let last_move_latest_time = 5_000 let admin_token = AssetClass { policy: mock.admin_policy, name: mock.admin_token_name } - let redeemer = GatherFuel(options.provided_amount) + let ship_redeemer = GatherFuel(options.ship_added_amount) + let pellet_redeemer = Provide(options.pellet_provided_amount) let pilot_address = Address { payment_credential: VerificationKey(mock.pilot_credential), @@ -84,6 +87,7 @@ fn gather(options: GatherTestOptions) -> Bool { pos_y: pellet_pos_y, shipyard_policy: mock.shipyard_policy, } + let pellet_output_reference = OutputReference { transaction_id: mock.transaction_id_2, output_index: 0 } let pellet_input = { let output = Output { @@ -94,9 +98,7 @@ fn gather(options: GatherTestOptions) -> Bool { datum: InlineDatum(pellet_datum), reference_script: None, } - let output_reference = - OutputReference { transaction_id: mock.transaction_id_2, output_index: 0 } - Input { output_reference, output } + Input { output_reference: pellet_output_reference, output } } let ship_address = Address { @@ -117,7 +119,7 @@ fn gather(options: GatherTestOptions) -> Bool { |> add( mock.pellet_credential, "FUEL", - options.ship_initial_fuel + options.provided_amount, + options.ship_initial_fuel + options.ship_added_amount, ) |> add("aaaa", "tokenA", options.extra_token_amount) let ship_input_datum = @@ -155,6 +157,7 @@ fn gather(options: GatherTestOptions) -> Bool { OutputReference { transaction_id: mock.transaction_id_1, output_index: 0 } Input { output_reference, output } } + let ship_output_reference = OutputReference { transaction_id: mock.transaction_id_1, output_index: 0 } let ship_input = { let output = Output { @@ -163,9 +166,7 @@ fn gather(options: GatherTestOptions) -> Bool { datum: InlineDatum(ship_input_datum), reference_script: None, } - let output_reference = - OutputReference { transaction_id: mock.transaction_id_1, output_index: 0 } - Input { output_reference, output } + Input { output_reference: ship_output_reference, output } } let pilot_input = { let output = { @@ -194,7 +195,7 @@ fn gather(options: GatherTestOptions) -> Bool { ship_output_value } else { ship_output_value - |> add(mock.pellet_credential, "FUEL", -options.provided_amount) + |> add(mock.pellet_credential, "FUEL", -options.ship_added_amount) }, datum: InlineDatum(ship_output_datum), reference_script: None, @@ -233,16 +234,18 @@ fn gather(options: GatherTestOptions) -> Bool { extra_signatories: [], redeemers: [ Pair( - Spend( - OutputReference { - transaction_id: mock.transaction_id_1, - output_index: 0, - }, - ), + Spend(ship_output_reference), { - let redeemer_data: Data = redeemer + let redeemer_data: Data = ship_redeemer redeemer_data - }, + } + ), + Pair( + Spend(pellet_output_reference), + { + let redeemer_data: Data = pellet_redeemer + redeemer_data + } ), ], datums: dict.empty @@ -270,7 +273,7 @@ fn gather(options: GatherTestOptions) -> Bool { initial_pellet_fuel, min_asteria_distance, Some(ship_input_datum), - redeemer, + ship_redeemer, OutputReference { transaction_id: mock.transaction_id_1, output_index: 0 }, tx, ) @@ -329,7 +332,7 @@ test fuel_not_added() fail { } test exceed_fuel_capacity() fail { - gather(GatherTestOptions { ..default_gather_options(), provided_amount: 61 }) + gather(GatherTestOptions { ..default_gather_options(), ship_added_amount: 61 }) } test no_respect_latest_time() fail { @@ -361,3 +364,15 @@ test mint_tokens() fail { GatherTestOptions { ..default_gather_options(), mints_no_tokens: False }, ) } + +test fuel_removed() fail { + gather( + GatherTestOptions { ..default_gather_options(), ship_added_amount: -10 }, + ) +} + +test external_fuel_added() fail { + gather( + GatherTestOptions { ..default_gather_options(), ship_added_amount: 60 }, + ) +}