For the complete documentation index, see llms.txt. This page is also available as Markdown.

Examples

These examples walk through multiple scenarios you might encounter as a Stacks developer.

No transfers

A contract function with no asset transfers specified.

On the frontend, this should be set to a postConditionMode of "deny", without needing a specific post-condition statement.

(define-public (no-transfers)
  (ok "no transfer events executed.")
)

We will specify the postConditionMode to deny. No other post-condition statements are needed. postConditions can be set to an empty array.

The user's expectation should be to deny any asset transfers.

import { request } from "@stacks/connect";

const response = await request("stx_callContract", {
  contract: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.post-conditions",
  functionName: "no-transfers",
  network: "devnet",
  postConditions: [],
  postConditionMode: "deny"
})

User Expectation: No assets will be transferred out of their wallet.

Result: No transfers made during transaction execution. Transaction is confirmed.


Single STX transfer

A function that contains a single asset transfer of 10 STX from the tx-sender to the contract.

(define-public (single-stx-transfer)
  (stx-transfer? u10000000 tx-sender (as-contract tx-sender))
)

A StxPostCondition will be set with the origin/user address, and how much the user expects to be sending out. The postConditionMode is set to deny to prevent any other unexpected transfers to happen.

import { request } from "@stacks/connect";
import type { StxPostCondition } from "@stacks/transactions";

const stxPostCondition: StxPostCondition = {
  type: 'stx-postcondition',
  address: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
  condition: 'eq',
  amount: '10000000',
};

const response = await request("stx_callContract", {
  contract: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.post-conditions",
  functionName: "single-stx-transfer",
  functionArgs: [],
  postConditionMode: "deny",
  postConditions: [stxPostCondition],
  network: "devnet"
})

User Expectation: Allow only 10 STX to be transferred.

Result: 10 STX are transferred from the sender's wallet as the user expected. Transaction is confirmed.


Multiple STX transfers

When multiple transfer events happen for the same asset on the same address, your post-condition statement should aggregate the net total amount of that asset being transferred during the execution of the entire function.

The contract first transfers 20 STX then transfers another 10 STX from the sender.

As the dev, you should notice that a total of 30 STX will be transferred from the user to the contract. This total amount should be accounted for as a single StxPostCondition since it's on the same asset and same principal. The postConditionMode is set to deny.

User Expectation: Allow at most 30 STX to be transferred.

Result: 30 STX was transferred out of the sender's wallet as expected. Transaction is confirmed.


Mint and Burn Events

A post-condition statement is needed for burn events, but not needed for mint events.

A post-condition for the burn event will only be needed. Mint events are not considered a transfer and are treated differently.

User Expectation: Allow exactly 10 STX to be transferred for burning.

Result: 10 STX transferred out of the sender's wallet to be burned. Some good-token were minted to the user. No other asset transfers happened. Transaction is confirmed.


Uncertain asset transfer amount

The function below demonstrates a scenario where the amount of STX to send is dynamic and uncertain. Having two post-condition statements that capture a range would be appropriate.

We're able to setup an amount range using post-conditions. We'll have one post-condition specifying that the user will transfer greater than or equal 0 STX, and another specifying that the user will transfer less than or equal to 1 STX.

User Expectation: Allow between 0 STX and 1 STX to be transferred from the user.

Result: If the actual amount of STX is within the specified expected range, the transaction will confirm. If not, the transaction will abort and fail.


Hidden asset transfers

The function below contains a bunch of asset transfer events that are obfuscated in a way where it may not be noticeable at first glance. It's setup for the tx-sender to pay for a cool-nft for 2 STX, but unbeknownst to the user, the function will attempt to transfer out a few of the user's good tokens AND send the users some evil tokens.

In this scenario, we'll simulate a failed transaction as a way to demonstrate where the dev and the user are both unaware of a few underlying transfers happening. Currently, the user assumes they will transfer 2 STX to receive a cool-nft asset.

We'll specify those 2 corresponding post-conditions with the post-condition mode set to deny. But when the transaction is executed and evaluated, it will fail because other previously unknown transfer events tried to execute saving our user from malicious activity.

User Expectation: Allow the transfer of 2 STX for a cool-nft asset.

Result: The contract attempted to perform other asset transfers that were not covered by the declared post-conditions. Transaction will abort and fail.


Staking STX (SIP-045)

Staking post-conditions guard STX being locked for staking rather than transferred. Calls to the pox-5 stake, register-for-bond, and stake-update functions are evaluated against them, and the transaction is rejected if the conditions are not met.

Here the user expects to lock at least 1 STX when calling the pox-5 stake function. The .ustxToLock() method constrains the locked amount; a plain .ustx() post-condition would not cover it, since staking is not a transfer.

User Expectation: At least 1 STX will be locked for staking.

Result: If the call locks 1 STX or more, the transaction confirms. If it would lock less, the transaction aborts and fails.


Guarding PoX actions (SIP-045)

PoX post-conditions guard PoX state changes that do not alter locking status: the pox-5 unstake, unstake-sbtc, update-bond-registration, and announce-l1-early-exit functions. They carry only a principal and one of three condition codes:

For example, when calling an unfamiliar third-party contract, add willNotPerformPox() to guarantee it cannot change your PoX state as a side effect:

User Expectation: The contract call will not unstake or otherwise modify the user's PoX state.

Result: If the contract attempts a gated PoX action on behalf of the user (such as unstake), the transaction aborts and fails. Otherwise it confirms.

Staking and PoX post-conditions are introduced by SIP-045 as part of the pox-5 Bitcoin staking framework, and require Stacks epoch 4.0 or later.

Last updated

Was this helpful?