Understanding post-conditions
Learn how post-conditions protect users from unexpected transaction outcomes
Overview
Post-conditions are Stacks' unique security feature that ensure transactions only succeed if specific conditions are met after execution. They act as assertions about the final state, protecting users from unexpected token transfers, NFT movements, or contract behavior. If any post-condition fails, the entire transaction is reverted.
Why post-conditions matter
Traditional blockchains execute transactions blindly—you sign, hope for the best. Stacks is different:
// Without post-conditions: Risky!await contract.swap(tokenA, tokenB, amount);// What if the contract has a bug and takes more than expected?// With post-conditions: Safe!const postConditions = [// Ensure exactly 100 tokenA is sentmakeStandardFungiblePostCondition(myAddress,FungibleConditionCode.Equal,100,createAssetInfo(tokenAContract, 'token-a')),];
Post-conditions prevent:
- Malicious contracts draining wallets
- Bugs causing unexpected transfers
- Front-running attacks changing outcomes
- Hidden fees or commissions
How post-conditions work
Post-condition types
Stacks supports three asset types in post-conditions:
STX post-conditions
Constrain STX transfers:
import {makeStandardSTXPostCondition,FungibleConditionCode} from '@stacks/transactions';// Sender sends exactly 1 STXconst exactAmount = makeStandardSTXPostCondition(senderAddress,FungibleConditionCode.Equal,1000000 // 1 STX in microSTX);// Sender sends at most 1 STXconst maxAmount = makeStandardSTXPostCondition(senderAddress,FungibleConditionCode.LessEqual,1000000);// Sender sends at least 1 STXconst minAmount = makeStandardSTXPostCondition(senderAddress,FungibleConditionCode.GreaterEqual,1000000);
Fungible token post-conditions
Constrain token transfers:
import {makeStandardFungiblePostCondition,createAssetInfo} from '@stacks/transactions';const assetInfo = createAssetInfo('SP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZY','usdc-token','usdc');// Ensure exactly 100 USDC is sentconst tokenCondition = makeStandardFungiblePostCondition(senderAddress,FungibleConditionCode.Equal,100000000, // 100 USDC with 6 decimalsassetInfo);
NFT post-conditions
Constrain NFT transfers:
import {makeStandardNonFungiblePostCondition,NonFungibleConditionCode,createAssetInfo,bufferCVFromString} from '@stacks/transactions';const nftAsset = createAssetInfo('SP2J6Y09JMFWWZCT4VJX0BA5W7A9HZP5EX96Y6VZY','cool-nfts','nft');// Sender must send specific NFTconst sendNFT = makeStandardNonFungiblePostCondition(senderAddress,NonFungibleConditionCode.Sends,nftAsset,bufferCVFromString('nft-id-123'));// Sender must NOT send specific NFTconst keepNFT = makeStandardNonFungiblePostCondition(senderAddress,NonFungibleConditionCode.DoesNotSend,nftAsset,bufferCVFromString('rare-nft-456'));
Condition codes explained
Different codes for different constraints:
enum FungibleConditionCode {Equal = 0x01, // Exactly this amountGreater = 0x02, // More than this amountGreaterEqual = 0x03, // At least this amountLess = 0x04, // Less than this amountLessEqual = 0x05, // At most this amount}enum NonFungibleConditionCode {Sends = 0x10, // Must send the NFTDoesNotSend = 0x11, // Must NOT send the NFT}
Contract principal post-conditions
Constrain contract behavior:
import { makeContractSTXPostCondition } from '@stacks/transactions';// Ensure contract sends exactly 1 STXconst contractCondition = makeContractSTXPostCondition(contractAddress,contractName,FungibleConditionCode.Equal,1000000);// Ensure contract's token balance decreasesconst contractTokenCondition = makeContractFungiblePostCondition(contractAddress,contractName,FungibleConditionCode.GreaterEqual,1000000,assetInfo);
Post-condition modes
Control how strictly conditions are enforced:
import { PostConditionMode } from '@stacks/transactions';const txOptions = {// ... other optionspostConditions: [condition1, condition2],// Strict mode (default): ONLY specified transfers allowedpostConditionMode: PostConditionMode.Deny,// Allow mode: Additional transfers permittedpostConditionMode: PostConditionMode.Allow,};
Strict mode (Deny): Only transfers explicitly allowed by post-conditions can occur Allow mode: Additional transfers beyond post-conditions are permitted
Common patterns
Token swap protection
Ensure fair exchange in DEX operations:
function createSwapPostConditions(userAddress: string,tokenAInfo: AssetInfo,tokenBInfo: AssetInfo,amountAToSend: number,minAmountBToReceive: number) {return [// User sends exactly amountAmakeStandardFungiblePostCondition(userAddress,FungibleConditionCode.Equal,amountAToSend,tokenAInfo),// Contract sends at least minAmountB to usermakeContractFungiblePostCondition(dexContract,'dex-v1',FungibleConditionCode.GreaterEqual,minAmountBToReceive,tokenBInfo),];}
NFT marketplace safety
Protect NFT sales:
function createNFTSaleConditions(sellerAddress: string,buyerAddress: string,nftAsset: AssetInfo,nftId: BufferCV,price: number) {return [// Seller sends the NFTmakeStandardNonFungiblePostCondition(sellerAddress,NonFungibleConditionCode.Sends,nftAsset,nftId),// Buyer sends exactly the price in STXmakeStandardSTXPostCondition(buyerAddress,FungibleConditionCode.Equal,price),];}
Multi-party transactions
Complex conditions for multiple participants:
function createEscrowConditions(buyer: string,seller: string,escrowContract: string,amount: number) {return [// Buyer deposits to escrowmakeStandardSTXPostCondition(buyer,FungibleConditionCode.Equal,amount),// Escrow doesn't keep funds (passes through)makeContractSTXPostCondition(escrowContract,'escrow',FungibleConditionCode.Equal,0),// Seller receives from escrowmakeStandardSTXPostCondition(seller,FungibleConditionCode.Equal,-amount // Negative means receiving),];}
Debugging failed post-conditions
When post-conditions fail, debug systematically:
async function debugPostConditionFailure(txId: string) {const txInfo = await fetchTransaction(txId);if (txInfo.tx_status === 'abort_by_post_condition') {console.log('Post-condition failure details:');// Check which condition failedtxInfo.post_conditions.forEach((pc, index) => {console.log(`Condition ${index}:`, pc);console.log('Expected:', pc.condition_code);console.log('Actual events:', txInfo.events);});// Compare expected vs actual transfersconst actualTransfers = txInfo.events.filter(e =>e.event_type === 'fungible_token_asset');console.log('Actual transfers:', actualTransfers);}}
Post-conditions vs validation
Understand when to use each approach:
// Pre-transaction validation (read-only calls)async function validateBeforeTransaction() {const balance = await getTokenBalance(userAddress);if (balance < requiredAmount) {throw new Error('Insufficient balance');}}// Post-conditions (on-chain guarantee)const postConditions = [makeStandardFungiblePostCondition(userAddress,FungibleConditionCode.GreaterEqual,requiredAmount,tokenInfo),];// Use both for best UX:// 1. Validate first (better error messages)// 2. Include post-conditions (guaranteed safety)
Best practices
- Always use post-conditions for any transaction involving value transfer
- Be specific - use Equal when exact amounts are known
- Validate first - check conditions with read-only calls for better UX
- Test edge cases - ensure conditions handle all scenarios
- Document conditions - explain why each condition exists
Common mistakes
Too permissive conditions
// Bad: Allows any amountconst condition = makeStandardSTXPostCondition(sender,FungibleConditionCode.GreaterEqual,0 // This provides no protection!);// Good: Specific constraintconst condition = makeStandardSTXPostCondition(sender,FungibleConditionCode.Equal,1000000);
Missing recipient conditions
// Bad: Only checks senderconst conditions = [makeStandardSTXPostCondition(sender, FungibleConditionCode.Equal, amount),];// Good: Checks both partiesconst conditions = [makeStandardSTXPostCondition(sender, FungibleConditionCode.Equal, amount),makeStandardSTXPostCondition(recipient, FungibleConditionCode.Equal, -amount),];