Common patterns

Reusable post-condition patterns for typical blockchain operations

Overview

This reference provides battle-tested post-condition patterns for common Stacks operations. Copy and adapt these patterns to ensure your transactions are protected against unexpected behavior.

Token transfer patterns

Simple token transfer

Ensure exact token amounts are transferred:

function createTokenTransferConditions(
sender: string,
recipient: string,
amount: number,
tokenInfo: AssetInfo
): PostCondition[] {
return [
// Sender sends exactly the amount
makeStandardFungiblePostCondition(
sender,
FungibleConditionCode.Equal,
amount,
tokenInfo
),
// Optional: Verify recipient receives
makeStandardFungiblePostCondition(
recipient,
FungibleConditionCode.Equal,
-amount, // Negative for receiving
tokenInfo
),
];
}

Transfer with fees

Handle transfers where contracts take fees:

function createTransferWithFeeConditions(
sender: string,
recipient: string,
amount: number,
feeAmount: number,
tokenInfo: AssetInfo,
feeContract: string,
feeContractName: string
): PostCondition[] {
const totalAmount = amount + feeAmount;
return [
// Sender pays total (amount + fee)
makeStandardFungiblePostCondition(
sender,
FungibleConditionCode.Equal,
totalAmount,
tokenInfo
),
// Recipient receives amount (not fee)
makeStandardFungiblePostCondition(
recipient,
FungibleConditionCode.Equal,
-amount,
tokenInfo
),
// Contract receives exactly the fee
makeContractFungiblePostCondition(
feeContract,
feeContractName,
FungibleConditionCode.Equal,
-feeAmount,
tokenInfo
),
];
}

DEX and swap patterns

Token swap with slippage

Protect against excessive slippage in swaps:

interface SwapParams {
user: string;
dexContract: string;
dexName: string;
tokenIn: AssetInfo;
tokenOut: AssetInfo;
amountIn: number;
minAmountOut: number;
}
function createSwapConditions(params: SwapParams): PostCondition[] {
return [
// User sends exact amount of tokenIn
makeStandardFungiblePostCondition(
params.user,
FungibleConditionCode.Equal,
params.amountIn,
params.tokenIn
),
// User receives at least minAmountOut of tokenOut
makeStandardFungiblePostCondition(
params.user,
FungibleConditionCode.GreaterEqual,
-params.minAmountOut,
params.tokenOut
),
// DEX receives tokenIn
makeContractFungiblePostCondition(
params.dexContract,
params.dexName,
FungibleConditionCode.Equal,
-params.amountIn,
params.tokenIn
),
// DEX sends at least minAmountOut of tokenOut
makeContractFungiblePostCondition(
params.dexContract,
params.dexName,
FungibleConditionCode.GreaterEqual,
params.minAmountOut,
params.tokenOut
),
];
}

Liquidity provision

Add liquidity with balanced conditions:

function createAddLiquidityConditions(
user: string,
poolContract: string,
poolName: string,
tokenA: AssetInfo,
tokenB: AssetInfo,
amountA: number,
amountB: number,
minLPTokens: number,
lpToken: AssetInfo
): PostCondition[] {
return [
// User provides tokenA
makeStandardFungiblePostCondition(
user,
FungibleConditionCode.Equal,
amountA,
tokenA
),
// User provides tokenB
makeStandardFungiblePostCondition(
user,
FungibleConditionCode.Equal,
amountB,
tokenB
),
// User receives at least minimum LP tokens
makeStandardFungiblePostCondition(
user,
FungibleConditionCode.GreaterEqual,
-minLPTokens,
lpToken
),
// Pool receives exact amounts
makeContractFungiblePostCondition(
poolContract,
poolName,
FungibleConditionCode.Equal,
-amountA,
tokenA
),
makeContractFungiblePostCondition(
poolContract,
poolName,
FungibleConditionCode.Equal,
-amountB,
tokenB
),
];
}

NFT patterns

NFT sale

Ensure safe NFT marketplace transactions:

function createNFTSaleConditions(
seller: string,
buyer: string,
marketContract: string,
marketName: string,
nftAsset: AssetInfo,
nftId: BufferCV,
price: number,
marketFee: number
): PostCondition[] {
return [
// Seller transfers NFT
makeStandardNonFungiblePostCondition(
seller,
NonFungibleConditionCode.Sends,
nftAsset,
nftId
),
// Buyer pays total price
makeStandardSTXPostCondition(
buyer,
FungibleConditionCode.Equal,
price
),
// Seller receives price minus fee
makeStandardSTXPostCondition(
seller,
FungibleConditionCode.Equal,
-(price - marketFee)
),
// Marketplace receives fee
makeContractSTXPostCondition(
marketContract,
marketName,
FungibleConditionCode.Equal,
-marketFee
),
];
}

NFT auction settlement

Complex conditions for auction completion:

function createAuctionSettlementConditions(
seller: string,
winner: string,
auctionContract: string,
auctionName: string,
nftAsset: AssetInfo,
nftId: BufferCV,
winningBid: number,
platformFee: number,
royalty: number,
royaltyRecipient: string
): PostCondition[] {
const sellerReceives = winningBid - platformFee - royalty;
return [
// NFT goes from seller to winner
makeStandardNonFungiblePostCondition(
seller,
NonFungibleConditionCode.Sends,
nftAsset,
nftId
),
// Winner pays winning bid
makeStandardSTXPostCondition(
winner,
FungibleConditionCode.Equal,
winningBid
),
// Seller receives bid minus fees
makeStandardSTXPostCondition(
seller,
FungibleConditionCode.Equal,
-sellerReceives
),
// Platform receives fee
makeContractSTXPostCondition(
auctionContract,
auctionName,
FungibleConditionCode.Equal,
-platformFee
),
// Royalty recipient receives royalty
makeStandardSTXPostCondition(
royaltyRecipient,
FungibleConditionCode.Equal,
-royalty
),
];
}

Staking patterns

Token staking

Conditions for staking tokens:

function createStakingConditions(
staker: string,
stakingContract: string,
stakingContractName: string,
stakeToken: AssetInfo,
rewardToken: AssetInfo,
stakeAmount: number,
immediateReward?: number
): PostCondition[] {
const conditions = [
// User stakes tokens
makeStandardFungiblePostCondition(
staker,
FungibleConditionCode.Equal,
stakeAmount,
stakeToken
),
// Contract receives stake
makeContractFungiblePostCondition(
stakingContract,
stakingContractName,
FungibleConditionCode.Equal,
-stakeAmount,
stakeToken
),
];
// If immediate rewards are given
if (immediateReward && immediateReward > 0) {
conditions.push(
makeContractFungiblePostCondition(
stakingContract,
stakingContractName,
FungibleConditionCode.Equal,
immediateReward,
rewardToken
),
makeStandardFungiblePostCondition(
staker,
FungibleConditionCode.Equal,
-immediateReward,
rewardToken
)
);
}
return conditions;
}

Unstaking with rewards

Handle unstaking with accumulated rewards:

function createUnstakingConditions(
staker: string,
stakingContract: string,
stakingContractName: string,
stakeToken: AssetInfo,
rewardToken: AssetInfo,
unstakeAmount: number,
minRewards: number
): PostCondition[] {
return [
// User receives staked tokens back
makeStandardFungiblePostCondition(
staker,
FungibleConditionCode.Equal,
-unstakeAmount,
stakeToken
),
// Contract returns staked tokens
makeContractFungiblePostCondition(
stakingContract,
stakingContractName,
FungibleConditionCode.Equal,
unstakeAmount,
stakeToken
),
// User receives at least minimum rewards
makeStandardFungiblePostCondition(
staker,
FungibleConditionCode.GreaterEqual,
-minRewards,
rewardToken
),
// Contract sends rewards
makeContractFungiblePostCondition(
stakingContract,
stakingContractName,
FungibleConditionCode.GreaterEqual,
minRewards,
rewardToken
),
];
}

Governance patterns

Voting with token lock

Ensure tokens are locked during voting:

function createVotingConditions(
voter: string,
governanceContract: string,
governanceName: string,
govToken: AssetInfo,
voteAmount: number
): PostCondition[] {
return [
// Voter locks tokens for voting
makeStandardFungiblePostCondition(
voter,
FungibleConditionCode.Equal,
voteAmount,
govToken
),
// Governance contract receives tokens
makeContractFungiblePostCondition(
governanceContract,
governanceName,
FungibleConditionCode.Equal,
-voteAmount,
govToken
),
];
}

Multi-asset patterns

Batch operations

Handle multiple assets in one transaction:

function createBatchTransferConditions(
sender: string,
transfers: Array<{
recipient: string;
amount: number;
asset: AssetInfo;
}>
): PostCondition[] {
const conditions: PostCondition[] = [];
// Group by asset to calculate total per asset
const assetTotals = new Map<string, number>();
transfers.forEach(transfer => {
const assetKey = `${transfer.asset.contractAddress}.${transfer.asset.contractName}::${transfer.asset.assetName}`;
const current = assetTotals.get(assetKey) || 0;
assetTotals.set(assetKey, current + transfer.amount);
// Add recipient condition
conditions.push(
makeStandardFungiblePostCondition(
transfer.recipient,
FungibleConditionCode.Equal,
-transfer.amount,
transfer.asset
)
);
});
// Add sender conditions for each asset
assetTotals.forEach((total, assetKey) => {
const [contractPart, assetName] = assetKey.split('::');
const [contractAddress, contractName] = contractPart.split('.');
const asset = createAssetInfo(contractAddress, contractName, assetName);
conditions.push(
makeStandardFungiblePostCondition(
sender,
FungibleConditionCode.Equal,
total,
asset
)
);
});
return conditions;
}

Utility functions

Post-condition builder

Flexible builder for complex scenarios:

class PostConditionPatternBuilder {
private patterns: Map<string, (params: any) => PostCondition[]> = new Map();
constructor() {
// Register common patterns
this.patterns.set('token-transfer', createTokenTransferConditions);
this.patterns.set('token-swap', createSwapConditions);
this.patterns.set('nft-sale', createNFTSaleConditions);
this.patterns.set('staking', createStakingConditions);
}
register(name: string, pattern: (params: any) => PostCondition[]) {
this.patterns.set(name, pattern);
}
build(patternName: string, params: any): PostCondition[] {
const pattern = this.patterns.get(patternName);
if (!pattern) {
throw new Error(`Unknown pattern: ${patternName}`);
}
return pattern(params);
}
combine(...conditionSets: PostCondition[][]): PostCondition[] {
return conditionSets.flat();
}
}
// Usage
const builder = new PostConditionPatternBuilder();
const swapConditions = builder.build('token-swap', {
user: userAddress,
dexContract,
dexName: 'amm-v1',
tokenIn: tokenAInfo,
tokenOut: tokenBInfo,
amountIn: 1000,
minAmountOut: 950,
});

Condition validator

Validate conditions before transaction:

function validatePostConditions(
conditions: PostCondition[],
expectedTransfers: Array<{
from: string;
to?: string;
amount: number;
asset: 'STX' | AssetInfo;
}>
): boolean {
// Verify all expected transfers have conditions
for (const transfer of expectedTransfers) {
const hasCondition = conditions.some(condition => {
if (transfer.asset === 'STX' && condition.conditionType === ConditionType.STX) {
return condition.principal === transfer.from;
}
// Additional validation logic
return false;
});
if (!hasCondition) {
console.warn(`Missing condition for transfer from ${transfer.from}`);
return false;
}
}
return true;
}

Testing patterns

Test your post-conditions:

import { describe, it, expect } from 'vitest';
describe('Post-condition patterns', () => {
it('should create correct swap conditions', () => {
const conditions = createSwapConditions({
user: 'ST1TEST...',
dexContract: 'ST2DEX...',
dexName: 'amm-v1',
tokenIn: tokenAInfo,
tokenOut: tokenBInfo,
amountIn: 1000,
minAmountOut: 950,
});
expect(conditions).toHaveLength(4);
expect(conditions[0].conditionCode).toBe(FungibleConditionCode.Equal);
expect(conditions[1].conditionCode).toBe(FungibleConditionCode.GreaterEqual);
});
});

Best practices

  • Use specific patterns: Don't reinvent the wheel
  • Test edge cases: Ensure patterns handle all scenarios
  • Document parameters: Make patterns self-explanatory
  • Version patterns: Update carefully to avoid breaking changes
  • Combine wisely: Some patterns can be combined, others shouldn't

Next steps