Skip to main content
Glama
mandatory_post_conditions.md19.1 kB
# Mandatory Post-Conditions Framework ## Overview Post-conditions are **MANDATORY** for SIP-009 and SIP-010 compliance on Stacks - not optional! They are security guarantees that ensure transactions behave exactly as expected, preventing unexpected token transfers. **Critical Understanding:** - Post-conditions are **required by the SIP standards** - They prevent transaction malleability attacks - They provide explicit guarantees about what will happen in a transaction - **Transactions MUST fail** if post-conditions are not met ## Why Post-Conditions Are Mandatory ### Security Without Post-Conditions ❌ ```typescript // DANGEROUS: No post-conditions await openContractCall({ contractAddress: 'SP123...ABC', contractName: 'my-token', functionName: 'transfer', functionArgs: [uintCV(1000), principalCV(sender), principalCV(recipient)], // ❌ NO POST-CONDITIONS - VULNERABLE TO MANIPULATION }); ``` **Potential attacks:** - Contract could transfer more tokens than intended - Contract could transfer different tokens - Contract could make additional transfers - Malicious contracts could drain wallets ### Security With Post-Conditions ✅ ```typescript // SECURE: With mandatory post-conditions const postConditions = [ makeStandardFungiblePostCondition( senderAddress, FungibleConditionCode.Equal, 1000, // EXACT amount createAssetInfo(contractAddress, contractName, assetName) ), ]; await openContractCall({ contractAddress, contractName, functionName: 'transfer', functionArgs: [uintCV(1000), principalCV(sender), principalCV(recipient)], postConditions, // ✅ GUARANTEES EXACT BEHAVIOR postConditionMode: PostConditionMode.Deny, // ✅ FAIL IF CONDITIONS NOT MET }); ``` ## Post-Condition Types ### 1. Fungible Token Post-Conditions (SIP-010) ```typescript import { makeStandardFungiblePostCondition, makeContractFungiblePostCondition, FungibleConditionCode, createAssetInfo } from '@stacks/transactions'; // Standard fungible post-condition (most common) const standardFTCondition = makeStandardFungiblePostCondition( 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', // principal FungibleConditionCode.Equal, // condition 1000000, // amount (base units) createAssetInfo( // asset info 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', 'my-token', 'my-token' ) ); // Contract fungible post-condition const contractFTCondition = makeContractFungiblePostCondition( 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', // contract address 'my-contract', // contract name FungibleConditionCode.LessEqual, // condition 5000000, // max amount createAssetInfo( 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', 'reward-token', 'reward-token' ) ); ``` ### 2. Non-Fungible Token Post-Conditions (SIP-009) ```typescript import { makeStandardNonFungiblePostCondition, makeContractNonFungiblePostCondition, NonFungibleConditionCode, createAssetInfo } from '@stacks/transactions'; // Standard NFT post-condition const standardNFTCondition = makeStandardNonFungiblePostCondition( 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', // principal NonFungibleConditionCode.DoesNotOwn, // condition createAssetInfo( // asset info 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', 'my-nft', 'my-nft' ), uintCV(42) // token ID ); // Contract NFT post-condition const contractNFTCondition = makeContractNonFungiblePostCondition( 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', // contract address 'marketplace', // contract name NonFungibleConditionCode.Owns, // condition createAssetInfo( 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', 'collectible', 'collectible' ), uintCV(123) ); ``` ### 3. STX Post-Conditions ```typescript import { makeStandardSTXPostCondition, makeContractSTXPostCondition, FungibleConditionCode } from '@stacks/transactions'; // Standard STX post-condition const stxCondition = makeStandardSTXPostCondition( 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', // principal FungibleConditionCode.Equal, // condition 1000000 // amount in microSTX ); // Contract STX post-condition const contractSTXCondition = makeContractSTXPostCondition( 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', // contract address 'payment-contract', // contract name FungibleConditionCode.LessEqual, // condition 5000000 // max amount ); ``` ## Condition Codes Explained ### Fungible Condition Codes ```typescript enum FungibleConditionCode { SentEqual = 'sent_equal', // Exactly this amount sent SentGreater = 'sent_greater', // More than this amount sent SentGreaterEqual = 'sent_greater_equal', // At least this amount sent SentLess = 'sent_less', // Less than this amount sent SentLessEqual = 'sent_less_equal' // At most this amount sent } // Most common usage patterns: FungibleConditionCode.Equal // For exact transfers: "I will send exactly 1000 tokens" FungibleConditionCode.LessEqual // For max limits: "I will send at most 5000 tokens" FungibleConditionCode.GreaterEqual // For min requirements: "I will send at least 100 tokens" ``` ### Non-Fungible Condition Codes ```typescript enum NonFungibleConditionCode { Sent = 'sent', // NFT was sent NotSent = 'not_sent' // NFT was not sent } // Ownership-based conditions (more common): NonFungibleConditionCode.DoesNotOwn // "I will not own this NFT after transaction" NonFungibleConditionCode.Owns // "I will own this NFT after transaction" ``` ## Post-Condition Modes ```typescript enum PostConditionMode { Allow = 0x01, // Allow additional asset transfers beyond post-conditions Deny = 0x02 // ✅ RECOMMENDED: Deny any transfers not explicitly allowed } // ALWAYS use Deny mode for maximum security postConditionMode: PostConditionMode.Deny ``` ## Complete Implementation Examples ### 1. SIP-010 Token Transfer with Post-Conditions ```typescript import { openContractCall, PostConditionMode, FungibleConditionCode, createAssetInfo, makeStandardFungiblePostCondition, uintCV, principalCV, noneCV } from '@stacks/connect'; async function transferSIP010WithPostConditions( contractAddress: string, contractName: string, amount: number, sender: string, recipient: string, memo?: string ) { const functionArgs = [ uintCV(amount), principalCV(sender), principalCV(recipient), memo ? someCV(bufferCV(Buffer.from(memo, 'utf8'))) : noneCV() ]; // MANDATORY: Post-condition for exact transfer amount const postConditions = [ makeStandardFungiblePostCondition( sender, FungibleConditionCode.Equal, // Exactly this amount amount, createAssetInfo(contractAddress, contractName, contractName) ), ]; try { const result = await openContractCall({ contractAddress, contractName, functionName: 'transfer', functionArgs, postConditions, postConditionMode: PostConditionMode.Deny, // CRITICAL: Deny unexpected transfers onFinish: (data) => { console.log('Transfer completed:', data.txId); }, onCancel: () => { console.log('Transfer cancelled by user'); }, }); return result; } catch (error) { if (error.message.includes('post-condition')) { throw new Error('Post-condition failed: Transaction would not behave as expected'); } throw error; } } ``` ### 2. SIP-009 NFT Transfer with Post-Conditions ```typescript async function transferSIP009WithPostConditions( contractAddress: string, contractName: string, tokenId: number, sender: string, recipient: string ) { const functionArgs = [ uintCV(tokenId), principalCV(sender), principalCV(recipient) ]; // MANDATORY: Post-condition ensuring sender loses ownership const postConditions = [ makeStandardNonFungiblePostCondition( sender, NonFungibleConditionCode.DoesNotOwn, // Sender will not own after transfer createAssetInfo(contractAddress, contractName, contractName), uintCV(tokenId) ), ]; return await openContractCall({ contractAddress, contractName, functionName: 'transfer', functionArgs, postConditions, postConditionMode: PostConditionMode.Deny, onFinish: (data) => { console.log('NFT transfer completed:', data.txId); }, }); } ``` ### 3. Multi-Asset Transaction with Post-Conditions ```typescript async function multiAssetSwap( tokenAContract: string, tokenBContract: string, amountA: number, amountB: number, userAddress: string ) { const functionArgs = [ uintCV(amountA), uintCV(amountB) ]; // COMPREHENSIVE post-conditions for both tokens const postConditions = [ // User sends exactly amountA of token A makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Equal, amountA, createAssetInfo(tokenAContract, 'token-a', 'token-a') ), // User receives exactly amountB of token B makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Equal, amountB, createAssetInfo(tokenBContract, 'token-b', 'token-b') ), ]; return await openContractCall({ contractAddress: 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', contractName: 'dex-contract', functionName: 'swap', functionArgs, postConditions, postConditionMode: PostConditionMode.Deny, }); } ``` ### 4. Complex DeFi Transaction with Post-Conditions ```typescript async function addLiquidityWithPostConditions( poolContract: string, tokenAContract: string, tokenBContract: string, amountA: number, amountB: number, expectedLP: number, userAddress: string ) { const functionArgs = [ uintCV(amountA), uintCV(amountB), uintCV(expectedLP * 0.95) // 5% slippage tolerance ]; const postConditions = [ // Send token A to pool makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Equal, amountA, createAssetInfo(tokenAContract, 'token-a', 'token-a') ), // Send token B to pool makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Equal, amountB, createAssetInfo(tokenBContract, 'token-b', 'token-b') ), // Receive at least 95% of expected LP tokens makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.GreaterEqual, Math.floor(expectedLP * 0.95), createAssetInfo(poolContract, 'lp-token', 'lp-token') ), ]; return await openContractCall({ contractAddress: poolContract, contractName: 'amm-pool', functionName: 'add-liquidity', functionArgs, postConditions, postConditionMode: PostConditionMode.Deny, }); } ``` ## Smart Wallet Security Patterns ### 1. Multi-Signature with Post-Conditions ```typescript async function multiSigTransferWithPostConditions( multiSigContract: string, targetContract: string, amount: number, recipient: string, signers: string[] ) { // Post-conditions for multi-sig contract const postConditions = [ // Multi-sig contract sends the tokens makeContractFungiblePostCondition( multiSigContract, 'multi-sig-wallet', FungibleConditionCode.Equal, amount, createAssetInfo(targetContract, 'token', 'token') ), // Recipient receives the tokens makeStandardFungiblePostCondition( recipient, FungibleConditionCode.Equal, amount, createAssetInfo(targetContract, 'token', 'token') ), ]; return await openContractCall({ contractAddress: multiSigContract, contractName: 'multi-sig-wallet', functionName: 'execute-transfer', functionArgs: [ principalCV(targetContract), uintCV(amount), principalCV(recipient) ], postConditions, postConditionMode: PostConditionMode.Deny, }); } ``` ### 2. Atomic Transaction Compositions ```typescript async function atomicNFTSale( nftContract: string, tokenId: number, price: number, seller: string, buyer: string ) { const postConditions = [ // Buyer pays STX makeStandardSTXPostCondition( buyer, FungibleConditionCode.Equal, price ), // Seller receives STX makeStandardSTXPostCondition( seller, FungibleConditionCode.Equal, price ), // Buyer receives NFT makeStandardNonFungiblePostCondition( buyer, NonFungibleConditionCode.Owns, createAssetInfo(nftContract, 'nft', 'nft'), uintCV(tokenId) ), // Seller loses NFT makeStandardNonFungiblePostCondition( seller, NonFungibleConditionCode.DoesNotOwn, createAssetInfo(nftContract, 'nft', 'nft'), uintCV(tokenId) ), ]; return await openContractCall({ contractAddress: 'SP2PABAF9FTAJYNFZH93XENAJ8FVY99RRM50D2JG9', contractName: 'nft-marketplace', functionName: 'execute-sale', functionArgs: [ principalCV(nftContract), uintCV(tokenId), uintCV(price) ], postConditions, postConditionMode: PostConditionMode.Deny, }); } ``` ## Post-Condition Failure Debugging ### Common Failure Patterns ```typescript // 1. Amount mismatch const condition = makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Equal, 1000, // Expected 1000 assetInfo ); // Transaction transfers 1001 -> POST-CONDITION FAILURE ❌ // 2. Wrong condition code const condition = makeStandardFungiblePostCondition( userAddress, FungibleConditionCode.Greater, // Expects > 1000 1000, assetInfo ); // Transaction transfers exactly 1000 -> POST-CONDITION FAILURE ❌ // 3. Missing post-condition for actual transfer const postConditions = [ // Only covers token A makeStandardFungiblePostCondition(userAddress, FungibleConditionCode.Equal, 1000, tokenA) // Missing post-condition for token B transfer -> FAILURE ❌ ]; ``` ### Debugging Tools ```typescript // Log post-conditions before transaction console.log('Post-conditions:', postConditions.map(pc => ({ type: pc.type, principal: pc.principal, conditionCode: pc.conditionCode, amount: pc.amount, assetInfo: pc.assetInfo }))); // Simulate transaction locally first (if using Clarinet) const simulationResult = await clarinet.simulateTransaction( contractCall, postConditions ); if (!simulationResult.success) { console.error('Simulation failed:', simulationResult.error); } ``` ## Integration Patterns ### React Hook for Post-Condition Management ```typescript import { useState, useCallback } from 'react'; import { PostCondition } from '@stacks/transactions'; export const usePostConditions = () => { const [postConditions, setPostConditions] = useState<PostCondition[]>([]); const addFungibleCondition = useCallback(( principal: string, amount: number, assetInfo: any, conditionCode = FungibleConditionCode.Equal ) => { const condition = makeStandardFungiblePostCondition( principal, conditionCode, amount, assetInfo ); setPostConditions(prev => [...prev, condition]); }, []); const addNFTCondition = useCallback(( principal: string, tokenId: number, assetInfo: any, conditionCode = NonFungibleConditionCode.DoesNotOwn ) => { const condition = makeStandardNonFungiblePostCondition( principal, conditionCode, assetInfo, uintCV(tokenId) ); setPostConditions(prev => [...prev, condition]); }, []); const clearConditions = useCallback(() => { setPostConditions([]); }, []); return { postConditions, addFungibleCondition, addNFTCondition, clearConditions }; }; ``` ### Post-Condition Validation Component ```typescript import React from 'react'; import { PostCondition } from '@stacks/transactions'; interface PostConditionDisplayProps { postConditions: PostCondition[]; onApprove: () => void; onReject: () => void; } export const PostConditionDisplay: React.FC<PostConditionDisplayProps> = ({ postConditions, onApprove, onReject }) => { return ( <div className="post-condition-display"> <h3>Transaction Guarantees</h3> <p>This transaction will:</p> <ul> {postConditions.map((condition, index) => ( <li key={index}> {formatPostCondition(condition)} </li> ))} </ul> <div className="warning"> ⚠️ If these conditions are not met exactly, the transaction will fail. </div> <div className="actions"> <button onClick={onApprove}>Approve Transaction</button> <button onClick={onReject}>Cancel</button> </div> </div> ); }; function formatPostCondition(condition: PostCondition): string { // Format post-condition for human reading // Implementation depends on condition type and values return `Transfer ${condition.amount} tokens`; } ``` ## Security Best Practices ### ✅ Do This 1. **Always use Deny mode** ```typescript postConditionMode: PostConditionMode.Deny ``` 2. **Be explicit about amounts** ```typescript FungibleConditionCode.Equal // Exact amount ``` 3. **Cover all asset movements** ```typescript // If contract moves 2 tokens, have 2 post-conditions ``` 4. **Validate before signing** ```typescript // Show post-conditions to user for approval ``` 5. **Test post-conditions thoroughly** ```typescript // Unit tests + integration tests ``` ### ❌ Don't Do This 1. **Never use Allow mode for production** ```typescript postConditionMode: PostConditionMode.Allow // DANGEROUS ``` 2. **Don't skip post-conditions** ```typescript postConditions: [] // VULNERABLE ``` 3. **Don't use LessEqual without good reason** ```typescript FungibleConditionCode.LessEqual // Usually unnecessary ``` 4. **Don't trust user input for amounts** ```typescript // Validate amounts match expected values ``` ## Conclusion Post-conditions are **mandatory security features** for all SIP-009 and SIP-010 token operations. They: - **Prevent unexpected behavior** by explicitly defining what will happen - **Protect users** from malicious or buggy contracts - **Enable atomic transactions** with multiple asset movements - **Provide transparency** about transaction effects - **Are required by SIP standards** for compliance **Remember:** Every token transfer, whether fungible or non-fungible, MUST include appropriate post-conditions in deny mode. This is not optional - it's a security requirement.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/exponentlabshq/stacks-clarity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server