Skip to content
Merged
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
4 changes: 4 additions & 0 deletions book/src/contract-keywords/token-cost.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ When the transition is processed:
3. A cost outside the signer's `minimumTokenCost` and `maximumTokenCost` is refused (`IdentityHasNotAgreedToPayRequiredTokenAmountError`, 40116).
4. The signer's gas request must be one the cost offers, and the whole batch must name one payer (40129, 40130).
5. Against state: a signer whose account for the token is frozen is refused (`IdentityTokenAccountFrozenError`, 40702), and so is one whose balance is below `amount` (`IdentityDoesNotHaveEnoughTokenBalanceError`, 40700).
6. From protocol version 14, a transparent payment that transfers or burns tokens is then refused if the token is paused (`TokenIsPausedError`, 40711).
7. From protocol version 14, for a transfer to a different identity, the contract owner's account for the token is checked next. A frozen recipient is refused (40702) only when the token issuer's `allowTransferToFrozenBalance` is `false`; its default is `true`. For another contract's token, the issuing contract's policy applies, and the recipient remains the document contract's owner.

Every state read is charged, including on refusal. From protocol version 14, the issuer metadata and, for an external token, its contract are read only when the recipient is frozen. A transparent owner self-payment retains the payer freeze and balance checks and makes no transfer, so it performs neither the pause nor recipient checks. A [payment from the shielded pool](../data-model/token-shielded-pools.md#documents-paid-from-the-pool) retains its separate pool validation.

The signer pays: the creator for a create, the owner for a replace, delete, transfer or price update, and the buyer for a purchase.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ use crate::error::execution::ExecutionError;
use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext;
use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::state_v0::DocumentBaseTransitionActionStateValidationV0;
use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::state_v1::DocumentBaseTransitionActionStateValidationV1;
use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::state_v2::DocumentBaseTransitionActionStateValidationV2;
use crate::platform_types::platform::PlatformStateRef;

mod state_v0;
mod state_v1;
mod state_v2;

pub trait DocumentBaseTransitionActionValidation {
#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -64,9 +66,18 @@ impl DocumentBaseTransitionActionValidation for DocumentBaseTransitionAction {
transaction,
platform_version,
),
2 => self.validate_state_v2(
platform,
owner_id,
block_info,
transition_type,
execution_context,
transaction,
platform_version,
),
version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch {
method: "DocumentBaseTransitionAction::validate_state".to_string(),
known_versions: vec![0, 1],
known_versions: vec![0, 1, 2],
received: version,
})),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use crate::error::execution::ExecutionError;
use crate::error::Error;
use crate::execution::types::execution_operation::ValidationOperation;
use crate::execution::types::state_transition_execution_context::{
StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0,
};
use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::state_v0::DocumentBaseTransitionActionStateValidationV0;
use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::validate_token_not_paused;
use crate::platform_types::platform::PlatformStateRef;
use dpp::block::block_info::BlockInfo;
use dpp::consensus::state::token::IdentityTokenAccountFrozenError;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::data_contract::accessors::v1::DataContractV1Getters;
use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters;
use dpp::identifier::Identifier;
use dpp::tokens::calculate_token_id;
use dpp::tokens::contract_info::v0::TokenContractInfoV0Accessors;
use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors;
use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect;
use dpp::validation::SimpleConsensusValidationResult;
use dpp::version::PlatformVersion;
use drive::error::drive::DriveError;
use drive::error::Error as DriveErrorWrapper;
use drive::grovedb::TransactionArg;
use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{
DocumentBaseTransitionAction, DocumentBaseTransitionActionAccessorsV0,
};

pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentBaseTransitionActionStateValidationV2 {
#[allow(clippy::too_many_arguments)]
fn validate_state_v2(
&self,
platform: &PlatformStateRef,
owner_id: Identifier,
block_info: &BlockInfo,
transition_type: &str,
execution_context: &mut StateTransitionExecutionContext,
transaction: TransactionArg,
platform_version: &PlatformVersion,
) -> Result<SimpleConsensusValidationResult, Error>;
}

impl DocumentBaseTransitionActionStateValidationV2 for DocumentBaseTransitionAction {
/// Version 2 (protocol version 14): transparent payments retain payer freeze and balance
/// checks, then enforce pause and the actual issuer's frozen-recipient policy for token
/// movements. Elided owner self-payments retain only the payer checks; pool payments
/// retain their separate shielded validation.
fn validate_state_v2(
&self,
platform: &PlatformStateRef,
owner_id: Identifier,
block_info: &BlockInfo,
transition_type: &str,
execution_context: &mut StateTransitionExecutionContext,
transaction: TransactionArg,
platform_version: &PlatformVersion,
) -> Result<SimpleConsensusValidationResult, Error> {
// Pool payments have their own pause, balance and proof validation after the document
// action is valid; they do not spend the signer's transparent balance.
if self.shielded_token_payment().is_some() {
return Ok(SimpleConsensusValidationResult::new());
}
let validation_result = self.validate_state_v0(
platform,
owner_id,
block_info,
transition_type,
execution_context,
transaction,
platform_version,
)?;
if !validation_result.is_valid() {
return Ok(validation_result);
}
let Some((token_id, effect, _)) = self.token_cost() else {
return Ok(validation_result);
};
let recipient_id = self.data_contract_fetch_info_ref().contract.owner_id();
// Owner self-payments emit no transfer. Preserve their payer checks without adding
// movement restrictions or reads for an operation that will not happen.
if effect == DocumentActionTokenEffect::TransferTokenToContractOwner
&& owner_id == recipient_id
{
return Ok(validation_result);
}

let validation_result = validate_token_not_paused(
platform,
token_id,
block_info,
execution_context,
transaction,
platform_version,
)?;
if !validation_result.is_valid() || effect == DocumentActionTokenEffect::BurnToken {
return Ok(validation_result);
}

let (recipient_info, fee) = platform.drive.fetch_identity_token_info_with_costs(
token_id.to_buffer(),
recipient_id.to_buffer(),
block_info,
true,
transaction,
platform_version,
)?;
execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee));
if !recipient_info.is_some_and(|info| info.frozen()) {
return Ok(validation_result);
}

// Only a frozen recipient needs the issuer's policy. The document contract can accept
// another contract's token, so its own token settings cannot decide this restriction.
let (token_contract_info, fee) = platform.drive.fetch_token_contract_info_with_costs(
token_id.to_buffer(),
block_info,
true,
transaction,
platform_version,
)?;
execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee));
let token_contract_info = token_contract_info.ok_or_else(|| {
Error::Drive(DriveErrorWrapper::Drive(DriveError::CorruptedDriveState(
format!("contract info is missing for document payment token {token_id}"),
)))
})?;
let token_contract_id = token_contract_info.contract_id();
let token_contract_position = token_contract_info.token_contract_position();
if calculate_token_id(token_contract_id.as_bytes(), token_contract_position)
!= token_id.to_buffer()
{
return Err(Error::Drive(DriveErrorWrapper::Drive(
DriveError::CorruptedDriveState(format!(
"contract info does not match document payment token {token_id}"
)),
)));
}
let token_contract = if token_contract_id == self.data_contract_id() {
self.data_contract_fetch_info()
} else {
let (fee, contract) = platform.drive.get_contract_with_fetch_info_and_fee(
token_contract_id.to_buffer(),
Some(&block_info.epoch),
false,
transaction,
platform_version,
)?;
let fee = fee.ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution(
"fee must exist when fetching a token's contract with an epoch",
)))?;
execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee));
contract.ok_or_else(|| {
Error::Drive(DriveErrorWrapper::Drive(DriveError::CorruptedDriveState(
format!("issuer contract {token_contract_id} is missing for document payment token {token_id}"),
)))
})?
};
let token_configuration = token_contract
.contract
.tokens()
.get(&token_contract_position)
.ok_or_else(|| {
Error::Drive(DriveErrorWrapper::Drive(DriveError::CorruptedDriveState(
format!("issuer contract {token_contract_id} has no token at document payment position {token_contract_position}"),
)))
})?;
if !token_configuration.is_allowed_transfer_to_frozen_balance() {
return Ok(SimpleConsensusValidationResult::new_with_error(
IdentityTokenAccountFrozenError::new(
token_id,
recipient_id,
format!("Document {} token payment", transition_type),
)
.into(),
));
}
Ok(validation_result)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod document_base_transaction_action;
#[cfg(test)]
pub(crate) use document_base_transaction_action::DocumentBaseTransitionActionValidation;
pub(crate) mod document_create_transition_action;
pub(crate) mod document_delete_transition_action;
pub(crate) mod document_index_only_delete_transition_action;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ mod reference_test_setup;
mod replacement;
mod required_since;
mod system_agreement;
mod token_cost_policy;
mod transfer;
mod typed_array_references;

Expand Down
Loading
Loading