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 sent
makeStandardFungiblePostCondition(
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 STX
const exactAmount = makeStandardSTXPostCondition(
senderAddress,
FungibleConditionCode.Equal,
1000000 // 1 STX in microSTX
);
// Sender sends at most 1 STX
const maxAmount = makeStandardSTXPostCondition(
senderAddress,
FungibleConditionCode.LessEqual,
1000000
);
// Sender sends at least 1 STX
const 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 sent
const tokenCondition = makeStandardFungiblePostCondition(
senderAddress,
FungibleConditionCode.Equal,
100000000, // 100 USDC with 6 decimals
assetInfo
);

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 NFT
const sendNFT = makeStandardNonFungiblePostCondition(
senderAddress,
NonFungibleConditionCode.Sends,
nftAsset,
bufferCVFromString('nft-id-123')
);
// Sender must NOT send specific NFT
const keepNFT = makeStandardNonFungiblePostCondition(
senderAddress,
NonFungibleConditionCode.DoesNotSend,
nftAsset,
bufferCVFromString('rare-nft-456')
);

Condition codes explained

Different codes for different constraints:

enum FungibleConditionCode {
Equal = 0x01, // Exactly this amount
Greater = 0x02, // More than this amount
GreaterEqual = 0x03, // At least this amount
Less = 0x04, // Less than this amount
LessEqual = 0x05, // At most this amount
}
enum NonFungibleConditionCode {
Sends = 0x10, // Must send the NFT
DoesNotSend = 0x11, // Must NOT send the NFT
}

Contract principal post-conditions

Constrain contract behavior:

import { makeContractSTXPostCondition } from '@stacks/transactions';
// Ensure contract sends exactly 1 STX
const contractCondition = makeContractSTXPostCondition(
contractAddress,
contractName,
FungibleConditionCode.Equal,
1000000
);
// Ensure contract's token balance decreases
const 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 options
postConditions: [condition1, condition2],
// Strict mode (default): ONLY specified transfers allowed
postConditionMode: PostConditionMode.Deny,
// Allow mode: Additional transfers permitted
postConditionMode: 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 amountA
makeStandardFungiblePostCondition(
userAddress,
FungibleConditionCode.Equal,
amountAToSend,
tokenAInfo
),
// Contract sends at least minAmountB to user
makeContractFungiblePostCondition(
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 NFT
makeStandardNonFungiblePostCondition(
sellerAddress,
NonFungibleConditionCode.Sends,
nftAsset,
nftId
),
// Buyer sends exactly the price in STX
makeStandardSTXPostCondition(
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 escrow
makeStandardSTXPostCondition(
buyer,
FungibleConditionCode.Equal,
amount
),
// Escrow doesn't keep funds (passes through)
makeContractSTXPostCondition(
escrowContract,
'escrow',
FungibleConditionCode.Equal,
0
),
// Seller receives from escrow
makeStandardSTXPostCondition(
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 failed
txInfo.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 transfers
const 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 amount
const condition = makeStandardSTXPostCondition(
sender,
FungibleConditionCode.GreaterEqual,
0 // This provides no protection!
);
// Good: Specific constraint
const condition = makeStandardSTXPostCondition(
sender,
FungibleConditionCode.Equal,
1000000
);

Missing recipient conditions

// Bad: Only checks sender
const conditions = [
makeStandardSTXPostCondition(sender, FungibleConditionCode.Equal, amount),
];
// Good: Checks both parties
const conditions = [
makeStandardSTXPostCondition(sender, FungibleConditionCode.Equal, amount),
makeStandardSTXPostCondition(recipient, FungibleConditionCode.Equal, -amount),
];

Next steps