Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions api/types/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,10 @@ impl<'a, S: StateView> MoveConverter<'a, S> {
extra_config,
}) => match extra_config {
aptos_types::transaction::TransactionExtraConfig::V1 {
multisig_address,
replay_protection_nonce: _,
multisig_address, ..
}
| aptos_types::transaction::TransactionExtraConfig::V2 {
multisig_address, ..
} => {
if let Some(multisig_address) = multisig_address {
match executable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
gas_feature_versions::{RELEASE_V1_14, RELEASE_V1_8, RELEASE_V1_9_SKIPPED},
gas_schedule::NativeGasParameters,
ver::gas_feature_versions::{
RELEASE_V1_12, RELEASE_V1_13, RELEASE_V1_23, RELEASE_V1_26, RELEASE_V1_28,
RELEASE_V1_12, RELEASE_V1_13, RELEASE_V1_23, RELEASE_V1_26, RELEASE_V1_28, RELEASE_V1_32,
},
};
use aptos_gas_algebra::{
Expand Down Expand Up @@ -314,6 +314,7 @@ crate::gas_schedule::macros::define_gas_parameters!(
[transaction_context_entry_function_payload_per_byte_in_str: InternalGasPerByte, {RELEASE_V1_12.. => "transaction_context.entry_function_payload.per_abstract_memory_unit"}, 18],
[transaction_context_multisig_payload_base: InternalGas, {RELEASE_V1_12.. => "transaction_context.multisig_payload.base"}, 735],
[transaction_context_multisig_payload_per_byte_in_str: InternalGasPerByte, {RELEASE_V1_12.. => "transaction_context.multisig_payload.per_abstract_memory_unit"}, 18],
[transaction_context_gas_payment_fa_metadata_base: InternalGas, {RELEASE_V1_32.. => "transaction_context.gas_payment_fa_metadata.base"}, 735],

[code_request_publish_base: InternalGas, "code.request_publish.base", 1838],
[code_request_publish_per_byte: InternalGasPerByte, "code.request_publish.per_byte", 7],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ pub enum FeatureFlag {
EnableLazyLoading,
CalculateTransactionFeeForDistribution,
DistributeTransactionFee,
GasPayableFa,
GovernedGasPool,
SteakRewardUsingTreasury,
ExtractAbortInfoExactMatch,
Expand Down Expand Up @@ -398,6 +399,7 @@ impl From<FeatureFlag> for AptosFeatureFlag {
AptosFeatureFlag::CALCULATE_TRANSACTION_FEE_FOR_DISTRIBUTION
},
FeatureFlag::DistributeTransactionFee => AptosFeatureFlag::DISTRIBUTE_TRANSACTION_FEE,
FeatureFlag::GasPayableFa => AptosFeatureFlag::GAS_PAYABLE_FA,
FeatureFlag::GovernedGasPool => AptosFeatureFlag::GOVERNED_GAS_POOL,
FeatureFlag::SteakRewardUsingTreasury => AptosFeatureFlag::STAKE_REWARD_USING_TREASURY,
FeatureFlag::ExtractAbortInfoExactMatch => {
Expand Down Expand Up @@ -576,6 +578,7 @@ impl From<AptosFeatureFlag> for FeatureFlag {
FeatureFlag::CalculateTransactionFeeForDistribution
},
AptosFeatureFlag::DISTRIBUTE_TRANSACTION_FEE => FeatureFlag::DistributeTransactionFee,
AptosFeatureFlag::GAS_PAYABLE_FA => FeatureFlag::GasPayableFa,
AptosFeatureFlag::GOVERNED_GAS_POOL => FeatureFlag::GovernedGasPool,
AptosFeatureFlag::STAKE_REWARD_USING_TREASURY => FeatureFlag::SteakRewardUsingTreasury,
AptosFeatureFlag::EXTRACT_ABORT_INFO_EXACT_MATCH => {
Expand Down
9 changes: 9 additions & 0 deletions aptos-move/aptos-vm/src/aptos_vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1734,6 +1734,15 @@ impl AptosVM {
}
}

if !self.features().is_gas_payable_fa_enabled()
&& transaction.extra_config().has_gas_fa_coin()
{
return Err(VMStatus::error(
StatusCode::FEATURE_UNDER_GATING,
Some("Paying gas in a fungible asset is not yet supported".to_string()),
));
}

// The prologue MUST be run AFTER any validation. Otherwise you may run prologue and hit
// SEQUENCE_NUMBER_TOO_NEW if there is more than one transaction from the same sender and
// end up skipping validation.
Expand Down
7 changes: 7 additions & 0 deletions aptos-move/aptos-vm/src/transaction_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub struct TransactionMetadata {
pub is_keyless: bool,
pub entry_function_payload: Option<EntryFunction>,
pub multisig_payload: Option<Multisig>,
pub gas_fa_coin: Option<AccountAddress>,
}

impl TransactionMetadata {
Expand Down Expand Up @@ -102,6 +103,10 @@ impl TransactionMetadata {
TransactionExtraConfig::V1 {
multisig_address: Some(multisig_address),
..
}
| TransactionExtraConfig::V2 {
multisig_address: Some(multisig_address),
..
},
}) => Some(Multisig {
multisig_address: *multisig_address,
Expand All @@ -115,6 +120,7 @@ impl TransactionMetadata {
}),
_ => None,
},
gas_fa_coin: txn.payload().extra_config().gas_fa_coin(),
}
}

Expand Down Expand Up @@ -209,6 +215,7 @@ impl TransactionMetadata {
.map(|entry_func| entry_func.as_entry_function_payload()),
self.multisig_payload()
.map(|multisig| multisig.as_multisig_payload()),
self.gas_fa_coin,
)
}
}
53 changes: 53 additions & 0 deletions aptos-move/e2e-move-tests/src/tests/fee_payer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,59 @@ fn test_existing_account_with_fee_payer() {
assert!(bob_start > bob_after);
}

/// A fee-payer (sponsored) transaction with account abstraction disabled routes gas collection
/// through the legacy `transaction_validation::epilogue_gas_payer_extended` Move function.
///
/// Per the VM dispatch in `run_epilogue`, that function is selected exactly when neither
/// account-abstraction feature is enabled AND the transaction has a fee payer; with account
/// abstraction on (the default), the unified epilogue is used instead. The gas is charged to the
/// fee payer, not the sender.
#[test]
fn test_fee_payer_runs_epilogue_gas_payer_extended() {
let mut h = MoveHarness::new_with_features(
vec![
FeatureFlag::GAS_PAYER_ENABLED,
FeatureFlag::SPONSORED_AUTOMATIC_ACCOUNT_V1_CREATION,
],
// Disabling account abstraction forces the non-unified (legacy) epilogue path.
vec![
FeatureFlag::DEFAULT_ACCOUNT_RESOURCE,
FeatureFlag::ACCOUNT_ABSTRACTION,
FeatureFlag::DERIVABLE_ACCOUNT_ABSTRACTION,
],
);

let alice = h.new_account_at(AccountAddress::from_hex_literal("0xa11ce").unwrap());
let bob = h.new_account_at(AccountAddress::from_hex_literal("0xb0b").unwrap());

let alice_start = h.read_aptos_balance(alice.address());
let bob_start = h.read_aptos_balance(bob.address());

// A no-op self-transfer; bob sponsors the gas.
let payload = aptos_stdlib::aptos_coin_transfer(*alice.address(), 0);
let transaction = TransactionBuilder::new(alice.clone())
.fee_payer(bob.clone())
.payload(payload)
.sequence_number(h.sequence_number(alice.address()))
.max_gas_amount(1_000_000)
.gas_unit_price(1)
.sign_fee_payer();

let output = h.run_raw(transaction);
assert_success!(*output.status());

// The fee payer (bob) paid the gas via epilogue_gas_payer_extended; the sender (alice) did not.
let alice_after = h.read_aptos_balance(alice.address());
let bob_after = h.read_aptos_balance(bob.address());
assert_eq!(alice_start, alice_after);
assert!(
bob_start > bob_after,
"fee payer should have paid the gas: {} -> {}",
bob_start,
bob_after
);
}

#[test]
fn test_existing_account_with_fee_payer_aborts() {
let mut h = MoveHarness::new_with_features(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

use crate::MoveHarness;
use aptos_cached_packages::aptos_stdlib;
use aptos_types::{
on_chain_config::FeatureFlag,
transaction::{
TransactionExecutable, TransactionExtraConfig, TransactionPayload,
TransactionPayloadInner, TransactionStatus,
},
};
use move_core_types::{account_address::AccountAddress, vm_status::StatusCode};

/// Builds a transaction payload in the new (versioned) format carrying an optional `gas_fa_coin`,
/// wrapping a simple APT transfer entry function as the executable.
fn transfer_payload_with_gas_fa_coin(
recipient: AccountAddress,
gas_fa_coin: Option<AccountAddress>,
) -> TransactionPayload {
let executable = match aptos_stdlib::aptos_account_transfer(recipient, 1) {
TransactionPayload::EntryFunction(entry_function) => {
TransactionExecutable::EntryFunction(entry_function)
},
_ => unreachable!("aptos_account_transfer builds an entry function payload"),
};
TransactionPayload::Payload(TransactionPayloadInner::V1 {
executable,
extra_config: TransactionExtraConfig::V2 {
multisig_address: None,
replay_protection_nonce: None,
gas_fa_coin,
},
})
}

/// A transaction that specifies a `gas_fa_coin` must be rejected while the `GAS_PAYABLE_FA` feature
/// is disabled, even though the underlying (versioned) payload format itself is enabled.
#[test]
fn gas_fa_coin_is_rejected_when_feature_disabled() {
// TRANSACTION_PAYLOAD_V2 is on so the versioned payload format is not what triggers the gate;
// GAS_PAYABLE_FA is off so the `gas_fa_coin` field is the sole reason for rejection.
let mut h = MoveHarness::new_with_features(
vec![FeatureFlag::TRANSACTION_PAYLOAD_V2],
vec![FeatureFlag::GAS_PAYABLE_FA],
);
let alice = h.new_account_with_key_pair();
let bob = h.new_account_with_key_pair();

let payload = transfer_payload_with_gas_fa_coin(*bob.address(), Some(*bob.address()));
let txn = h.create_transaction_payload(&alice, payload);
let output = h.run_raw(txn);

match output.status() {
TransactionStatus::Discard(status) => assert_eq!(
*status,
StatusCode::FEATURE_UNDER_GATING,
"expected a FEATURE_UNDER_GATING discard, but got: {:?}",
status
),
other => panic!(
"expected a transaction carrying gas_fa_coin to be discarded, but got: {:?}",
other
),
}
}

/// Control: an identical transaction in the same feature configuration but *without* a
/// `gas_fa_coin` is accepted. This proves the rejection above is caused by `gas_fa_coin`
/// specifically, not by the versioned payload format.
#[test]
fn transaction_without_gas_fa_coin_is_not_gated() {
let mut h = MoveHarness::new_with_features(
vec![FeatureFlag::TRANSACTION_PAYLOAD_V2],
vec![FeatureFlag::GAS_PAYABLE_FA],
);
let alice = h.new_account_with_key_pair();
let bob = h.new_account_with_key_pair();

let payload = transfer_payload_with_gas_fa_coin(*bob.address(), None);
let status = h.run_transaction_payload(&alice, payload);

assert!(
matches!(status, TransactionStatus::Keep(_)),
"an identical transaction without gas_fa_coin must not be gated, but got: {:?}",
status
);
}
Loading
Loading