Clarity Post-Conditions: Enforcing Asset Safety on Stacks
Clarity post-conditions are mandatory safety rails that constrain which asset movements a contract may perform. This guide shows how to declare them, a worked STX-token swap example, common mistakes, and how to verify enforcement on Stacks.
01 Jul 2026, 10:58 UTC

The Problem: Unconstrained Asset Movements in Smart Contracts
When a Clarity contract executes a token transfer or STX payment, the blockchain does not automatically restrict where those assets go. Without explicit constraints, a bug or malicious call could drain contract funds to an attacker's address. Post-conditions solve this by letting the contract declare exactly which asset movements are permitted for any transaction that calls it. If the actual movements deviate, the Stacks blockchain rejects the entire transaction.
How Post-Conditions Work
Post-conditions are declared at the contract level using the post-condition keyword. They are not optional annotations; they are part of the contract's interface and are enforced by the Stacks blockchain during transaction execution. When a user submits a transaction that calls a public function, the blockchain simulates the execution, records every asset transfer (STX, fungible tokens, non-fungible tokens), and then checks each transfer against the declared post-conditions. If any transfer violates the conditions, the transaction fails and state changes are rolled back.
Key points:
- Post-conditions are evaluated per contract call, not per transaction globally.
- They apply to all public functions in the contract unless overridden in a specific function.
- The blockchain enforces them; the contract code itself cannot bypass them.
Worked Example: STX-for-Token Swap
Consider a contract that lets users swap STX for a SIP-010 fungible token. The contract should only send STX to the caller and receive the token from the caller. Any other movement (e.g., sending STX to a third party) must be rejected.
(define-fungible-token swap-token)
;; Declare post-conditions for the entire contract
(post-condition
;; Allow the contract to send STX to the transaction sender
(allowed-sender tx-sender stx)
;; Allow the contract to receive swap-token from the transaction sender
(allowed-receiver tx-sender (ft swap-token))
)
(define-public (swap (amount-stx uint) (min-tokens uint))
(let ((tokens-out (calculate-tokens amount-stx)))
(asserts! (>= tokens-out min-tokens) (err u100))
;; Transfer STX from contract to caller
(stx-transfer? amount-stx tx-sender (as-contract tx-sender))
;; Transfer tokens from caller to contract
(ft-transfer? swap-token tokens-out tx-sender (as-contract tx-sender))
(ok tokens-out)
)
)
In this example:
allowed-sender tx-sender stxpermits the contract to send STX only to the transaction sender (tx-sender).allowed-receiver tx-sender (ft swap-token)permits the contract to receive theswap-tokenonly from the transaction sender.- If a malicious caller somehow triggers an extra
stx-transfer?to another address, the blockchain will detect the unauthorized sender and abort.
Common Mistakes and Limits
Assuming Post-Conditions Are Optional
They are not. Every Clarity contract that performs asset transfers should declare post-conditions. Omitting them leaves the contract open to unintended asset flows.
Overly Permissive Conditions
Writing (allowed-sender any stx) or (allowed-receiver any (ft swap-token)) defeats the purpose. Always scope to specific principals (e.g., tx-sender, contract principal, or a known address).
Contract-Level vs. Transaction-Level
Post-conditions are evaluated at the contract level. If a transaction calls multiple contracts, each contract's post-conditions are checked independently. A transfer allowed by Contract A but forbidden by Contract B will cause Contract B's call to fail, potentially reverting the whole transaction if the calls are atomic.
Cannot Prevent Valid Transfers
If the declared conditions are met, the blockchain allows the transfer even if it is logically undesirable (e.g., a pricing bug that sends too many tokens). Post-conditions guard who receives assets, not how many.
Verifying Post-Conditions in Practice
- Unit test with Clarinet: Write tests that submit a valid swap and assert success, then submit a crafted call that attempts an extra STX transfer and assert failure with a post-condition violation error.
- Inspect transaction receipts: After deploying to testnet or mainnet, use the Stacks Explorer or
stacks-clito view the transaction's post-condition evaluation result. Look forpost_condition_codein the receipt; a non-zero value indicates a violation. - Static analysis: Run
clarinet checkto ensure post-condition syntax is valid and all asset-transfer functions are covered.
Practical Checklist Before Deploying
- Every public function that calls
stx-transfer?,ft-transfer?, ornft-transfer?is covered by a post-condition. - Conditions reference concrete principals (
tx-sender,contract-caller, or a constant principal), notany. - Test both success and failure paths with Clarinet or the Stacks REPL.
- Verify on testnet that a violating transaction returns a post-condition error (typically error code 18).
Diagram
Contract declares post-conditions → Transaction submitted → Blockchain validates asset movements → Transaction commits or reverts
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.