From eede4b1f0cae4e119c8d4dc21afc5f7224172536 Mon Sep 17 00:00:00 2001 From: Omar Date: Wed, 13 Sep 2023 18:45:00 +0300 Subject: [PATCH] Add detection of staking transaction type to core toolkit --- .vscode/settings.json | 1 + .../src/functions/execution.rs | 16 + .../visitors/transaction_type/mod.rs | 1 + .../transaction_type/stake_visitor.rs | 309 ++++++++ radix-engine-toolkit-core/src/utils.rs | 12 + radix-engine-toolkit-core/tests/execution.rs | 688 +++++++++++++++++- 6 files changed, 1022 insertions(+), 5 deletions(-) create mode 100644 radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/stake_visitor.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 0fe707b7..20df2d6e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,6 +23,7 @@ "hammunet", "kisharnet", "Localnet", + "LSU's", "mardunet", "Milestonenet", "moka", diff --git a/radix-engine-toolkit-core/src/functions/execution.rs b/radix-engine-toolkit-core/src/functions/execution.rs index 426481fa..73c877ad 100644 --- a/radix-engine-toolkit-core/src/functions/execution.rs +++ b/radix-engine-toolkit-core/src/functions/execution.rs @@ -30,6 +30,8 @@ use crate::instruction_visitor::visitors::transaction_type::general_transaction_ use crate::instruction_visitor::visitors::transaction_type::reserved_instructions::ReservedInstruction; use crate::instruction_visitor::visitors::transaction_type::reserved_instructions::ReservedInstructionsVisitor; use crate::instruction_visitor::visitors::transaction_type::simple_transfer_visitor::*; +use crate::instruction_visitor::visitors::transaction_type::stake_visitor::Stake; +use crate::instruction_visitor::visitors::transaction_type::stake_visitor::StakeVisitor; use crate::instruction_visitor::visitors::transaction_type::transfer_visitor::*; use crate::models::node_id::InvalidEntityTypeIdError; use crate::models::node_id::TypedNodeId; @@ -47,6 +49,7 @@ pub fn analyze( let mut account_deposit_settings_visitor = AccountDepositSettingsVisitor::default(); let mut general_transaction_visitor = GeneralTransactionTypeVisitor::new(execution_trace); let mut reserved_instructions_visitor = ReservedInstructionsVisitor::default(); + let mut stake_transaction_visitor = StakeVisitor::new(execution_trace); traverse( instructions, @@ -57,6 +60,7 @@ pub fn analyze( &mut account_deposit_settings_visitor, &mut general_transaction_visitor, &mut reserved_instructions_visitor, + &mut stake_transaction_visitor, ], )?; @@ -94,6 +98,11 @@ pub fn analyze( }, ))) } + if let Some((account, stakes)) = stake_transaction_visitor.output() { + transaction_types.push(TransactionType::StakeTransaction(Box::new( + StakeTransactionType { account, stakes }, + ))) + } if let Some((account_withdraws, account_deposits)) = general_transaction_visitor.output() { transaction_types.push(TransactionType::GeneralTransaction(Box::new( GeneralTransactionType { @@ -229,6 +238,7 @@ pub enum TransactionType { Transfer(Box), AccountDepositSettings(Box), GeneralTransaction(Box), + StakeTransaction(Box), } #[derive(Clone, Debug, PartialEq, Eq)] @@ -265,6 +275,12 @@ pub struct GeneralTransactionType { HashMap>, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StakeTransactionType { + pub account: ComponentAddress, + pub stakes: HashMap, +} + #[derive(Clone, Debug)] pub enum ExecutionModuleError { TransactionWasNotCommittedSuccessfully(TransactionReceiptV1), diff --git a/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/mod.rs b/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/mod.rs index 3544185e..f94c0d1c 100644 --- a/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/mod.rs +++ b/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/mod.rs @@ -19,4 +19,5 @@ pub mod account_deposit_settings_visitor; pub mod general_transaction_visitor; pub mod reserved_instructions; pub mod simple_transfer_visitor; +pub mod stake_visitor; pub mod transfer_visitor; diff --git a/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/stake_visitor.rs b/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/stake_visitor.rs new file mode 100644 index 00000000..8d67a155 --- /dev/null +++ b/radix-engine-toolkit-core/src/instruction_visitor/visitors/transaction_type/stake_visitor.rs @@ -0,0 +1,309 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::instruction_visitor::core::error::InstructionVisitorError; +use crate::instruction_visitor::core::traits::InstructionVisitor; +use crate::sbor::indexed_manifest_value::IndexedManifestValue; +use crate::utils::{is_account, is_validator}; + +use radix_engine::system::system_modules::execution_trace::{ResourceSpecifier, WorktopChange}; +use radix_engine::transaction::*; +use radix_engine_common::prelude::*; +use radix_engine_interface::blueprints::consensus_manager::VALIDATOR_STAKE_IDENT; +use scrypto::blueprints::account::*; +use scrypto::prelude::*; +use transaction::prelude::*; +use transaction::validation::ManifestIdAllocator; + +pub struct StakeVisitor<'r> { + /// The execution trace from the preview receipt + execution_trace: &'r TransactionExecutionTrace, + + /// The account withdrawn from - tracked to ensure that we deposit into the same account. + account_withdrawn_from: Option, + + /// Maps the validator component address to a map of the LSU's resource address and amount + /// obtained as part of staking. + validator_stake_mapping: HashMap, + + /// Tracks if the visitor is currently in an illegal state or not. + is_illegal_state: bool, + + /// Used to allocate new ids + id_allocator: ManifestIdAllocator, + + /// Tracks the buckets and their contents + bucket_tracker: HashMap, + + /// The index of the current instruction. + instruction_index: usize, +} + +impl<'r> StakeVisitor<'r> { + pub fn new(execution_trace: &'r TransactionExecutionTrace) -> Self { + Self { + execution_trace, + account_withdrawn_from: Default::default(), + validator_stake_mapping: Default::default(), + is_illegal_state: Default::default(), + id_allocator: Default::default(), + bucket_tracker: Default::default(), + instruction_index: Default::default(), + } + } + + fn is_take_from_worktop_allowed(&self, resource_address: &ResourceAddress) -> bool { + *resource_address == XRD + || self.validator_stake_mapping.values().any( + |Stake { + liquid_stake_units_resource_address, + .. + }| liquid_stake_units_resource_address == resource_address, + ) + } + + pub fn output(self) -> Option<(ComponentAddress, HashMap)> { + match ( + self.is_illegal_state, + self.validator_stake_mapping.is_empty(), + self.account_withdrawn_from, + ) { + (false, false, Some(account)) => Some((account, self.validator_stake_mapping)), + _ => None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stake { + pub staked_xrd: Decimal, + pub liquid_stake_units_resource_address: ResourceAddress, + pub liquid_stake_units_amount: Decimal, +} + +impl<'r> InstructionVisitor for StakeVisitor<'r> { + fn is_enabled(&self) -> bool { + !self.is_illegal_state + } + + fn post_visit(&mut self) -> Result<(), InstructionVisitorError> { + self.instruction_index += 1; + Ok(()) + } + + fn visit_instruction( + &mut self, + instruction: &InstructionV1, + ) -> Result<(), InstructionVisitorError> { + match instruction { + InstructionV1::CallMethod { + address, + method_name, + args, + } => { + // Filter: We only permit static address - no dynamic or named addresses are allowed + let global_address = if let DynamicGlobalAddress::Static(address) = address { + address + } else { + self.is_illegal_state = true; + return Ok(()); + }; + + /* Only withdraw of XRD is allowed from account */ + if is_account(global_address) && method_name == ACCOUNT_WITHDRAW_IDENT { + // Ensure arguments are valid and that the resource withdrawn is XRD. + let Some(AccountWithdrawInput { + resource_address: XRD, + .. + }) = manifest_encode(&args) + .ok() + .and_then(|encoded| manifest_decode(&encoded).ok()) + else { + self.is_illegal_state = true; + return Ok(()); + }; + // Ensure that this is either the first time we withdraw or that this is the + // account we withdraw from all throughout the manifest. + let account_address = ComponentAddress::try_from(*global_address) + .expect("We have checked that it's a component address"); + if let Some(previous_withdraw_component_address) = self.account_withdrawn_from { + if previous_withdraw_component_address != account_address { + self.is_illegal_state = true; + return Ok(()); + } + } else { + self.account_withdrawn_from = Some( + (*global_address) + .try_into() + .expect("We have checked that it's a component address"), + ); + } + } + /* + Only permit account deposits to the same account withdrawn from and only with authed + methods. + */ + else if is_account(global_address) + && (method_name == ACCOUNT_DEPOSIT_IDENT + || method_name == ACCOUNT_DEPOSIT_BATCH_IDENT) + { + match self.account_withdrawn_from { + Some(withdraw_account) + if withdraw_account.into_node_id() == global_address.into_node_id() => { + } + Some(..) | None => { + self.is_illegal_state = true; + return Ok(()); + } + } + let indexed_manifest_value = IndexedManifestValue::from_manifest_value(args); + for bucket in indexed_manifest_value.buckets() { + if self.bucket_tracker.remove(bucket).is_none() { + self.is_illegal_state = true; + return Ok(()); + } + } + } + /* Staking to a validator */ + else if is_validator(global_address) && method_name == VALIDATOR_STAKE_IDENT { + let validator_address = ComponentAddress::try_from(*global_address) + .expect("We have checked that it's a component address"); + + let Some((bucket @ ManifestBucket(..),)) = manifest_encode(&args) + .ok() + .and_then(|encoded| manifest_decode(&encoded).ok()) + else { + self.is_illegal_state = true; + return Ok(()); + }; + let Some((XRD, xrd_staked_amount)) = self.bucket_tracker.remove(&bucket) else { + self.is_illegal_state = true; + return Ok(()); + }; + + let (liquid_stake_units_resource_address, liquid_stake_units_amount) = + match self + .execution_trace + .worktop_changes() + .get(&self.instruction_index) + .map(|x| x.as_slice()) + { + Some( + [WorktopChange::Put(ResourceSpecifier::Amount( + resource_address, + amount, + ))], + ) => (*resource_address, *amount), + Some([]) | None => { + return Ok(()); + } + _ => { + self.is_illegal_state = true; + return Ok(()); + } + }; + + let entry = self + .validator_stake_mapping + .entry(validator_address) + .or_insert(Stake { + liquid_stake_units_resource_address, + liquid_stake_units_amount: Default::default(), + staked_xrd: Default::default(), + }); + entry.liquid_stake_units_amount += liquid_stake_units_amount; + entry.staked_xrd += xrd_staked_amount; + } + } + + InstructionV1::TakeAllFromWorktop { resource_address } => { + if self.is_take_from_worktop_allowed(resource_address) { + let amount = match self + .execution_trace + .worktop_changes() + .get(&self.instruction_index) + .map(|vec| vec.as_slice()) + { + Some( + [WorktopChange::Take(ResourceSpecifier::Amount( + take_resource_address, + amount, + ))], + ) if resource_address == take_resource_address => *amount, + Some([]) | None => Decimal::ZERO, + _ => { + self.is_illegal_state = true; + return Ok(()); + } + }; + let bucket_id = self.id_allocator.new_bucket_id(); + self.bucket_tracker + .insert(bucket_id, (*resource_address, amount)); + } else { + self.is_illegal_state = true; + return Ok(()); + } + } + InstructionV1::TakeFromWorktop { + resource_address, + amount, + } => { + if self.is_take_from_worktop_allowed(resource_address) { + let bucket_id = self.id_allocator.new_bucket_id(); + self.bucket_tracker + .insert(bucket_id, (*resource_address, *amount)); + } else { + self.is_illegal_state = true; + return Ok(()); + } + } + + /* Disallowed Instructions */ + InstructionV1::CallFunction { .. } + | InstructionV1::CallRoyaltyMethod { .. } + | InstructionV1::CallMetadataMethod { .. } + | InstructionV1::CallRoleAssignmentMethod { .. } + | InstructionV1::CallDirectVaultMethod { .. } + | InstructionV1::DropNamedProofs + | InstructionV1::DropAllProofs + | InstructionV1::DropAuthZoneProofs { .. } + | InstructionV1::DropAuthZoneRegularProofs { .. } + | InstructionV1::DropAuthZoneSignatureProofs { .. } + | InstructionV1::CreateProofFromAuthZoneOfAll { .. } + | InstructionV1::CreateProofFromBucketOfAmount { .. } + | InstructionV1::CreateProofFromBucketOfNonFungibles { .. } + | InstructionV1::CreateProofFromBucketOfAll { .. } + | InstructionV1::BurnResource { .. } + | InstructionV1::CloneProof { .. } + | InstructionV1::DropProof { .. } + | InstructionV1::TakeNonFungiblesFromWorktop { .. } + | InstructionV1::ReturnToWorktop { .. } + | InstructionV1::AssertWorktopContainsAny { .. } + | InstructionV1::AssertWorktopContains { .. } + | InstructionV1::AssertWorktopContainsNonFungibles { .. } + | InstructionV1::PopFromAuthZone { .. } + | InstructionV1::PushToAuthZone { .. } + | InstructionV1::CreateProofFromAuthZoneOfAmount { .. } + | InstructionV1::CreateProofFromAuthZoneOfNonFungibles { .. } + | InstructionV1::AllocateGlobalAddress { .. } => { + self.is_illegal_state = true; + return Ok(()); + } + }; + Ok(()) + } +} diff --git a/radix-engine-toolkit-core/src/utils.rs b/radix-engine-toolkit-core/src/utils.rs index 27682b7e..fdc37710 100644 --- a/radix-engine-toolkit-core/src/utils.rs +++ b/radix-engine-toolkit-core/src/utils.rs @@ -206,6 +206,18 @@ pub fn is_account + Clone>(node_id: &A) -> bool { } } +pub fn is_validator + Clone>(node_id: &A) -> bool { + match node_id.clone().into() { + DynamicGlobalAddress::Named(_) => false, + DynamicGlobalAddress::Static(address) => { + matches!( + address.as_node_id().entity_type(), + Some(EntityType::GlobalValidator) + ) + } + } +} + pub fn is_access_controller + Clone>(node_id: &A) -> bool { match node_id.clone().into() { DynamicGlobalAddress::Named(_) => false, diff --git a/radix-engine-toolkit-core/tests/execution.rs b/radix-engine-toolkit-core/tests/execution.rs index 621c4de0..d768ab7c 100644 --- a/radix-engine-toolkit-core/tests/execution.rs +++ b/radix-engine-toolkit-core/tests/execution.rs @@ -15,12 +15,15 @@ // specific language governing permissions and limitations // under the License. -use radix_engine::system::system_modules::execution_trace::ResourceSpecifier; -use radix_engine::transaction::{TransactionReceipt, VersionedTransactionReceipt}; -use radix_engine_interface::blueprints::account::{ACCOUNT_LOCK_FEE_IDENT, AccountLockFeeInput, ACCOUNT_LOCK_CONTINGENT_FEE_IDENT, AccountLockContingentFeeInput}; +use radix_engine::system::system_modules::execution_trace::*; +use radix_engine::transaction::*; +use radix_engine_interface::blueprints::account::*; +use radix_engine_interface::blueprints::consensus_manager::*; +use radix_engine_queries::typed_substate_layout::ValidatorSubstate; use radix_engine_toolkit_core::functions::execution::{self, *}; -use radix_engine_toolkit_core::instruction_visitor::visitors::transaction_type::general_transaction_visitor::{ResourceTracker, Source}; -use radix_engine_toolkit_core::instruction_visitor::visitors::transaction_type::transfer_visitor::Resources; +use radix_engine_toolkit_core::instruction_visitor::visitors::transaction_type::general_transaction_visitor::*; +use radix_engine_toolkit_core::instruction_visitor::visitors::transaction_type::stake_visitor::*; +use radix_engine_toolkit_core::instruction_visitor::visitors::transaction_type::transfer_visitor::*; use scrypto::prelude::*; use scrypto_unit::*; use transaction::prelude::*; @@ -328,6 +331,681 @@ fn manifest_with_a_lock_contingent_fee_should_not_be_conforming() { ) } +#[test] +fn simple_stake_transaction_can_be_caught_by_ret() { + // Arrange + let mut test_runner = TestRunnerBuilder::new().without_trace().build(); + let (pk, _, account) = test_runner.new_account(false); + let validator = test_runner.new_validator_with_pub_key(pk, account); + let ValidatorSubstate { + stake_unit_resource, + claim_nft: _, + .. + } = test_runner.get_validator_info(validator); + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .create_proof_from_account_of_non_fungible( + account, + NonFungibleGlobalId::new( + VALIDATOR_OWNER_BADGE, + NonFungibleLocalId::bytes(validator.as_node_id().0).unwrap(), + ), + ) + .register_validator(validator) + .call_method( + validator, + VALIDATOR_UPDATE_ACCEPT_DELEGATED_STAKE_IDENT, + ValidatorUpdateAcceptDelegatedStakeInput { + accept_delegated_stake: true, + }, + ) + .build(), + vec![NonFungibleGlobalId::from_public_key(&pk)], + ) + .expect_commit_success(); + test_runner.set_current_epoch(Epoch::of(20)); + + // Act + let manifest = ManifestBuilder::new() + .withdraw_from_account(account, XRD, 100) + .take_from_worktop(XRD, 100, "XRD") + .stake_validator(validator, "XRD") + .deposit_batch(account) + .build(); + let receipt = test_runner.preview_manifest( + manifest.clone(), + vec![pk.into()], + 0, + PreviewFlags { + use_free_credit: true, + assume_all_signature_proofs: true, + skip_epoch_check: true, + }, + ); + let transaction_types = transaction_types( + &manifest.instructions, + &ExecutionAnalysisTransactionReceipt::new(&VersionedTransactionReceipt::V1(receipt)) + .unwrap(), + ); + + // Assert + let Some(StakeTransactionType { + account: stake_account, + stakes, + }) = transaction_types.iter().find_map(|tx_type| match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + }) + else { + panic!("Stake transaction type not found!") + }; + assert_eq!(*stake_account, account); + assert_eq!( + stakes.get(&validator), + Some(&Stake { + liquid_stake_units_amount: dec!("100"), + staked_xrd: dec!("100"), + liquid_stake_units_resource_address: stake_unit_resource + }) + ); +} + +#[test] +fn staking_of_zero_xrd_is_considered_staking() { + // Arrange + let mut test_runner = TestRunnerBuilder::new().without_trace().build(); + let (pk, _, account) = test_runner.new_account(false); + let validator = test_runner.new_validator_with_pub_key(pk, account); + let ValidatorSubstate { + stake_unit_resource, + claim_nft: _, + .. + } = test_runner.get_validator_info(validator); + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .create_proof_from_account_of_non_fungible( + account, + NonFungibleGlobalId::new( + VALIDATOR_OWNER_BADGE, + NonFungibleLocalId::bytes(validator.as_node_id().0).unwrap(), + ), + ) + .register_validator(validator) + .call_method( + validator, + VALIDATOR_UPDATE_ACCEPT_DELEGATED_STAKE_IDENT, + ValidatorUpdateAcceptDelegatedStakeInput { + accept_delegated_stake: true, + }, + ) + .build(), + vec![NonFungibleGlobalId::from_public_key(&pk)], + ) + .expect_commit_success(); + test_runner.set_current_epoch(Epoch::of(20)); + + // Act + let manifest = ManifestBuilder::new() + .withdraw_from_account(account, XRD, 0) + .take_from_worktop(XRD, 0, "XRD") + .stake_validator(validator, "XRD") + .deposit_batch(account) + .build(); + let receipt = test_runner.preview_manifest( + manifest.clone(), + vec![pk.into()], + 0, + PreviewFlags { + use_free_credit: true, + assume_all_signature_proofs: true, + skip_epoch_check: true, + }, + ); + let transaction_types = transaction_types( + &manifest.instructions, + &ExecutionAnalysisTransactionReceipt::new(&VersionedTransactionReceipt::V1(receipt)) + .unwrap(), + ); + + // Assert + let Some(StakeTransactionType { + account: stake_account, + stakes, + }) = transaction_types.iter().find_map(|tx_type| match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + }) + else { + panic!("Stake transaction type not found!") + }; + assert_eq!(*stake_account, account); + assert_eq!( + stakes.get(&validator), + Some(&Stake { + liquid_stake_units_amount: dec!("0"), + staked_xrd: dec!("0"), + liquid_stake_units_resource_address: stake_unit_resource + }) + ); +} + +#[test] +fn additional_stake_of_zero_has_no_effect_on_detection() { + // Arrange + let mut test_runner = TestRunnerBuilder::new().without_trace().build(); + let (pk, _, account) = test_runner.new_account(false); + let validator = test_runner.new_validator_with_pub_key(pk, account); + let ValidatorSubstate { + stake_unit_resource, + claim_nft: _, + .. + } = test_runner.get_validator_info(validator); + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .create_proof_from_account_of_non_fungible( + account, + NonFungibleGlobalId::new( + VALIDATOR_OWNER_BADGE, + NonFungibleLocalId::bytes(validator.as_node_id().0).unwrap(), + ), + ) + .register_validator(validator) + .call_method( + validator, + VALIDATOR_UPDATE_ACCEPT_DELEGATED_STAKE_IDENT, + ValidatorUpdateAcceptDelegatedStakeInput { + accept_delegated_stake: true, + }, + ) + .build(), + vec![NonFungibleGlobalId::from_public_key(&pk)], + ) + .expect_commit_success(); + test_runner.set_current_epoch(Epoch::of(20)); + + // Act + let manifest = ManifestBuilder::new() + .withdraw_from_account(account, XRD, 100) + .take_from_worktop(XRD, 100, "XRD") + .stake_validator(validator, "XRD") + .withdraw_from_account(account, XRD, 0) + .take_from_worktop(XRD, 0, "XRD1") + .stake_validator(validator, "XRD1") + .deposit_batch(account) + .build(); + let receipt = test_runner.preview_manifest( + manifest.clone(), + vec![pk.into()], + 0, + PreviewFlags { + use_free_credit: true, + assume_all_signature_proofs: true, + skip_epoch_check: true, + }, + ); + let transaction_types = transaction_types( + &manifest.instructions, + &ExecutionAnalysisTransactionReceipt::new(&VersionedTransactionReceipt::V1(receipt)) + .unwrap(), + ); + + // Assert + let Some(StakeTransactionType { + account: stake_account, + stakes, + }) = transaction_types.iter().find_map(|tx_type| match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + }) + else { + panic!("Stake transaction type not found!") + }; + assert_eq!(*stake_account, account); + assert_eq!( + stakes.get(&validator), + Some(&Stake { + liquid_stake_units_amount: dec!("100"), + staked_xrd: dec!("100"), + liquid_stake_units_resource_address: stake_unit_resource + }) + ); +} + +#[test] +fn withdraws_from_multiple_accounts_invalidate_stake_rules() { + // Arrange + let mut test_runner = TestRunnerBuilder::new().without_trace().build(); + let (pk, _, account1) = test_runner.new_account(false); + let (_, _, account2) = test_runner.new_account(false); + let validator = test_runner.new_validator_with_pub_key(pk, account1); + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .create_proof_from_account_of_non_fungible( + account1, + NonFungibleGlobalId::new( + VALIDATOR_OWNER_BADGE, + NonFungibleLocalId::bytes(validator.as_node_id().0).unwrap(), + ), + ) + .register_validator(validator) + .call_method( + validator, + VALIDATOR_UPDATE_ACCEPT_DELEGATED_STAKE_IDENT, + ValidatorUpdateAcceptDelegatedStakeInput { + accept_delegated_stake: true, + }, + ) + .build(), + vec![NonFungibleGlobalId::from_public_key(&pk)], + ) + .expect_commit_success(); + test_runner.set_current_epoch(Epoch::of(20)); + + // Act + let manifest = ManifestBuilder::new() + .withdraw_from_account(account1, XRD, 100) + .withdraw_from_account(account2, XRD, 50) + .take_from_worktop(XRD, 100, "XRD") + .stake_validator(validator, "XRD") + .deposit_batch(account1) + .build(); + let receipt = test_runner.preview_manifest( + manifest.clone(), + vec![pk.into()], + 0, + PreviewFlags { + use_free_credit: true, + assume_all_signature_proofs: true, + skip_epoch_check: true, + }, + ); + let transaction_types = transaction_types( + &manifest.instructions, + &ExecutionAnalysisTransactionReceipt::new(&VersionedTransactionReceipt::V1(receipt)) + .unwrap(), + ); + + // Assert + assert!(transaction_types + .iter() + .find_map(|tx_type| { + match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + } + }) + .is_none()) +} + +#[test] +fn deposits_into_a_different_account_invalidate_stake_rules() { + // Arrange + let mut test_runner = TestRunnerBuilder::new().without_trace().build(); + let (pk, _, account1) = test_runner.new_account(false); + let (_, _, account2) = test_runner.new_account(false); + let validator = test_runner.new_validator_with_pub_key(pk, account1); + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .create_proof_from_account_of_non_fungible( + account1, + NonFungibleGlobalId::new( + VALIDATOR_OWNER_BADGE, + NonFungibleLocalId::bytes(validator.as_node_id().0).unwrap(), + ), + ) + .register_validator(validator) + .call_method( + validator, + VALIDATOR_UPDATE_ACCEPT_DELEGATED_STAKE_IDENT, + ValidatorUpdateAcceptDelegatedStakeInput { + accept_delegated_stake: true, + }, + ) + .build(), + vec![NonFungibleGlobalId::from_public_key(&pk)], + ) + .expect_commit_success(); + test_runner.set_current_epoch(Epoch::of(20)); + + // Act + let manifest = ManifestBuilder::new() + .withdraw_from_account(account1, XRD, 100) + .take_from_worktop(XRD, 100, "XRD") + .stake_validator(validator, "XRD") + .deposit_batch(account2) + .build(); + let receipt = test_runner.preview_manifest( + manifest.clone(), + vec![pk.into()], + 0, + PreviewFlags { + use_free_credit: true, + assume_all_signature_proofs: true, + skip_epoch_check: true, + }, + ); + let transaction_types = transaction_types( + &manifest.instructions, + &ExecutionAnalysisTransactionReceipt::new(&VersionedTransactionReceipt::V1(receipt)) + .unwrap(), + ); + + // Assert + assert!(transaction_types + .iter() + .find_map(|tx_type| { + match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + } + }) + .is_none()) +} + +#[test] +fn depositing_through_deposit_method_is_allowed() { + // Arrange + let mut test_runner = TestRunnerBuilder::new().without_trace().build(); + let (pk, _, account) = test_runner.new_account(false); + let validator = test_runner.new_validator_with_pub_key(pk, account); + let ValidatorSubstate { + stake_unit_resource, + .. + } = test_runner.get_validator_info(validator); + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .create_proof_from_account_of_non_fungible( + account, + NonFungibleGlobalId::new( + VALIDATOR_OWNER_BADGE, + NonFungibleLocalId::bytes(validator.as_node_id().0).unwrap(), + ), + ) + .register_validator(validator) + .call_method( + validator, + VALIDATOR_UPDATE_ACCEPT_DELEGATED_STAKE_IDENT, + ValidatorUpdateAcceptDelegatedStakeInput { + accept_delegated_stake: true, + }, + ) + .build(), + vec![NonFungibleGlobalId::from_public_key(&pk)], + ) + .expect_commit_success(); + test_runner.set_current_epoch(Epoch::of(20)); + + // Act + let manifest = ManifestBuilder::new() + .withdraw_from_account(account, XRD, 100) + .take_from_worktop(XRD, 100, "XRD") + .stake_validator(validator, "XRD") + .take_from_worktop(stake_unit_resource, 100, "StakeUnits") + .deposit(account, "StakeUnits") + .build(); + let receipt = test_runner.preview_manifest( + manifest.clone(), + vec![pk.into()], + 0, + PreviewFlags { + use_free_credit: true, + assume_all_signature_proofs: true, + skip_epoch_check: true, + }, + ); + let transaction_types = transaction_types( + &manifest.instructions, + &ExecutionAnalysisTransactionReceipt::new(&VersionedTransactionReceipt::V1(receipt)) + .unwrap(), + ); + + // Assert + let Some(StakeTransactionType { + account: stake_account, + stakes, + }) = transaction_types.iter().find_map(|tx_type| match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + }) + else { + panic!("Stake transaction type not found!") + }; + assert_eq!(*stake_account, account); + assert_eq!( + stakes.get(&validator), + Some(&Stake { + liquid_stake_units_amount: dec!("100"), + staked_xrd: dec!("100"), + liquid_stake_units_resource_address: stake_unit_resource + }) + ); +} + +#[test] +fn aggregation_of_stake_works_as_expected() { + // Arrange + let mut test_runner = TestRunnerBuilder::new().without_trace().build(); + let (pk, _, account) = test_runner.new_account(false); + let validator = test_runner.new_validator_with_pub_key(pk, account); + let ValidatorSubstate { + stake_unit_resource, + claim_nft: _, + .. + } = test_runner.get_validator_info(validator); + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .create_proof_from_account_of_non_fungible( + account, + NonFungibleGlobalId::new( + VALIDATOR_OWNER_BADGE, + NonFungibleLocalId::bytes(validator.as_node_id().0).unwrap(), + ), + ) + .register_validator(validator) + .call_method( + validator, + VALIDATOR_UPDATE_ACCEPT_DELEGATED_STAKE_IDENT, + ValidatorUpdateAcceptDelegatedStakeInput { + accept_delegated_stake: true, + }, + ) + .build(), + vec![NonFungibleGlobalId::from_public_key(&pk)], + ) + .expect_commit_success(); + test_runner.set_current_epoch(Epoch::of(20)); + + // Act + let manifest = ManifestBuilder::new() + .withdraw_from_account(account, XRD, 100) + .take_from_worktop(XRD, 100, "XRD") + .stake_validator(validator, "XRD") + .withdraw_from_account(account, XRD, 40) + .take_from_worktop(XRD, 40, "XRD1") + .stake_validator(validator, "XRD1") + .deposit_batch(account) + .build(); + let receipt = test_runner.preview_manifest( + manifest.clone(), + vec![pk.into()], + 0, + PreviewFlags { + use_free_credit: true, + assume_all_signature_proofs: true, + skip_epoch_check: true, + }, + ); + let transaction_types = transaction_types( + &manifest.instructions, + &ExecutionAnalysisTransactionReceipt::new(&VersionedTransactionReceipt::V1(receipt)) + .unwrap(), + ); + + // Assert + let Some(StakeTransactionType { + account: stake_account, + stakes, + }) = transaction_types.iter().find_map(|tx_type| match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + }) + else { + panic!("Stake transaction type not found!") + }; + assert_eq!(*stake_account, account); + assert_eq!( + stakes.get(&validator), + Some(&Stake { + liquid_stake_units_amount: dec!("140"), + staked_xrd: dec!("140"), + liquid_stake_units_resource_address: stake_unit_resource + }) + ); +} + +#[test] +fn multiple_validators_works_as_expected() { + // Arrange + let mut test_runner = TestRunnerBuilder::new().without_trace().build(); + let (pk, _, account) = test_runner.new_account(false); + let validator1 = test_runner.new_validator_with_pub_key(pk, account); + let ValidatorSubstate { + stake_unit_resource: stake_unit_resource1, + .. + } = test_runner.get_validator_info(validator1); + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .create_proof_from_account_of_non_fungible( + account, + NonFungibleGlobalId::new( + VALIDATOR_OWNER_BADGE, + NonFungibleLocalId::bytes(validator1.as_node_id().0).unwrap(), + ), + ) + .register_validator(validator1) + .call_method( + validator1, + VALIDATOR_UPDATE_ACCEPT_DELEGATED_STAKE_IDENT, + ValidatorUpdateAcceptDelegatedStakeInput { + accept_delegated_stake: true, + }, + ) + .build(), + vec![NonFungibleGlobalId::from_public_key(&pk)], + ) + .expect_commit_success(); + + let validator2 = test_runner.new_validator_with_pub_key(pk, account); + let ValidatorSubstate { + stake_unit_resource: stake_unit_resource2, + .. + } = test_runner.get_validator_info(validator2); + + test_runner + .execute_manifest( + ManifestBuilder::new() + .lock_fee_from_faucet() + .create_proof_from_account_of_non_fungible( + account, + NonFungibleGlobalId::new( + VALIDATOR_OWNER_BADGE, + NonFungibleLocalId::bytes(validator2.as_node_id().0).unwrap(), + ), + ) + .register_validator(validator2) + .call_method( + validator2, + VALIDATOR_UPDATE_ACCEPT_DELEGATED_STAKE_IDENT, + ValidatorUpdateAcceptDelegatedStakeInput { + accept_delegated_stake: true, + }, + ) + .build(), + vec![NonFungibleGlobalId::from_public_key(&pk)], + ) + .expect_commit_success(); + test_runner.set_current_epoch(Epoch::of(20)); + + // Act + let manifest = ManifestBuilder::new() + .withdraw_from_account(account, XRD, 100) + .take_from_worktop(XRD, 100, "XRD") + .stake_validator(validator1, "XRD") + .withdraw_from_account(account, XRD, 40) + .take_from_worktop(XRD, 40, "XRD1") + .stake_validator(validator2, "XRD1") + .deposit_batch(account) + .build(); + let receipt = test_runner.preview_manifest( + manifest.clone(), + vec![pk.into()], + 0, + PreviewFlags { + use_free_credit: true, + assume_all_signature_proofs: true, + skip_epoch_check: true, + }, + ); + let transaction_types = transaction_types( + &manifest.instructions, + &ExecutionAnalysisTransactionReceipt::new(&VersionedTransactionReceipt::V1(receipt)) + .unwrap(), + ); + + // Assert + let Some(StakeTransactionType { + account: stake_account, + stakes, + }) = transaction_types.iter().find_map(|tx_type| match tx_type { + TransactionType::StakeTransaction(stake) => Some(stake.as_ref()), + _ => None, + }) + else { + panic!("Stake transaction type not found!") + }; + assert_eq!(*stake_account, account); + assert_eq!( + stakes.get(&validator1), + Some(&Stake { + liquid_stake_units_amount: dec!("100"), + staked_xrd: dec!("100"), + liquid_stake_units_resource_address: stake_unit_resource1 + }) + ); + assert_eq!( + stakes.get(&validator2), + Some(&Stake { + liquid_stake_units_amount: dec!("40"), + staked_xrd: dec!("40"), + liquid_stake_units_resource_address: stake_unit_resource2 + }) + ); +} + fn transaction_types( manifest_instructions: &[InstructionV1], receipt: &TransactionReceipt,