Implementing post-conditions
Learn how to add post-conditions to protect your Stacks transactions
Post-conditions are a powerful security feature in Stacks that protect users from unexpected transaction outcomes. This tutorial will walk you through implementing post-conditions in your applications to ensure transactions behave exactly as users expect.
What you'll learn
Prerequisites
- Basic understanding of Stacks transactions
- Stacks.js library installed (
npm install @stacks/transactions
) - A development environment set up for Stacks
Constructing post-conditions
The Pc helper in Stacks.js provides a fluent, BDD-inspired API for constructing post-conditions. Start with Pc.principal()
to specify which address will be verified, then chain methods to define the condition.
import { Pc } from '@stacks/transactions';// Basic structure of a post-conditionconst postCondition = Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendEq(1000).ustx();
The Pc helper uses method chaining for intuitive condition building. Your IDE will provide auto-completion for available methods at each step.
Available transfer methods
Post-conditions support different comparison operators and asset types. Choose the appropriate method based on your security requirements.
STX and fungible token methods
// Exact amountPc.principal(address).willSendEq(1000).ustx();// Greater than or equalPc.principal(address).willSendGte(500).ustx();// Less thanPc.principal(address).willSendLt(2000).ustx();
Comparison methods available:
.willSendEq(amount)
- Exactly equal to amount.willSendGte(amount)
- Greater than or equal to amount.willSendGt(amount)
- Greater than amount.willSendLte(amount)
- Less than or equal to amount.willSendLt(amount)
- Less than amount
Asset type methods
// STX transfers.ustx()// Fungible token transfers.ft(contractAddress, tokenName)// NFT transfers.nft(assetIdentifier, tokenId)
NFT-specific methods
// Ensure NFT is sentPc.principal(address).willSendAsset().nft(...);// Ensure NFT is NOT sentPc.principal(address).willNotSendAsset().nft(...);
Setting the post-condition mode
The post-condition mode determines how the Stacks blockchain handles asset transfers not explicitly covered by your post-conditions. This is a critical security setting.
import { PostConditionMode, makeContractCall } from '@stacks/transactions';const tx = await makeContractCall({// ... other transaction propertiespostConditionMode: PostConditionMode.Deny, // Recommended defaultpostConditions: [// your post-conditions here],});
Mode options:
PostConditionMode.Deny
(default): Transaction fails if any unspecified transfers occurPostConditionMode.Allow
: Transaction allows transfers beyond specified post-conditions
type: warn
Always use Deny
mode unless you have a specific reason to allow additional transfers. This provides maximum security for users.
Common implementation patterns
STX transfer post-conditions
Protect STX transfers by specifying exact amounts or ranges.
import { Pc, makeSTXTokenTransfer } from '@stacks/transactions';// Exact amount post-conditionconst exactAmountCondition = Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendEq(1000).ustx();// Use in a transactionconst tx = await makeSTXTokenTransfer({recipient: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',amount: 1000,postConditions: [exactAmountCondition],postConditionMode: PostConditionMode.Deny,// ... other properties});
Fungible token post-conditions
Ensure fungible tokens are transferred as expected in contract calls.
import { Pc, makeContractCall } from '@stacks/transactions';// Minimum amount conditionconst ftCondition = Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendGte(500).ft('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.token-ft', 'token');// Use in a contract callconst tx = await makeContractCall({contractAddress: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',contractName: 'token-transfer',functionName: 'transfer',functionArgs: [// ... function arguments],postConditions: [ftCondition],// ... other properties});
NFT transfer post-conditions
Control NFT ownership changes with specific post-conditions.
import { Pc, Cl } from '@stacks/transactions';// Ensure NFT is sentconst sendNftCondition = Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendAsset().nft('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.nft-contract::nft-name', Cl.uint(1));// Ensure NFT is NOT sent (protection against unwanted transfers)const keepNftCondition = Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willNotSendAsset().nft('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.nft-contract::nft-name', Cl.uint(1));
Use willNotSendAsset()
to protect valuable NFTs from being transferred unexpectedly.
Semi-fungible token (SFT) post-conditions
SFTs require special handling as they have both fungible and non-fungible properties.
import { Cl, Pc } from '@stacks/transactions';// SFT as NFT (specific token ID)const sftNftCondition = Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendAsset().nft('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sft-contract::sft-id',Cl.tuple({'token-id': Cl.uint(1),owner: Cl.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6')}));// SFT as FT (amount-based)const sftFtCondition = Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendEq(500).ft('ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.sft-contract', 'sft-token');
Multiple post-conditions
Complex transactions often require multiple post-conditions to fully protect all asset transfers.
const tx = await makeContractCall({// ... transaction propertiespostConditions: [// Sender must send exactly 1000 uSTXPc.principal(senderAddress).willSendEq(1000).ustx(),// Contract must send at least 100 tokens to userPc.principal(contractAddress).willSendGte(100).ft(contractAddress + '.my-token', 'my-token'),// User must not lose their NFTPc.principal(senderAddress).willNotSendAsset().nft(nftContract + '::my-nft', Cl.uint(1)),],postConditionMode: PostConditionMode.Deny,});
Explore the Post-conditions template for interactive examples.