Improved Post-Conditions

Introduction

In the current ecosystem, Deny mode is the safest way to interact with smart
contracts, and users should be in the habit of never signing a transaction in
Allow mode. The reality is that some applications, e.g. DeFi apps, move assets
between a variety of contracts as part of one contract call. The movements may
be dynamic and difficult or impossible to predict in advance, so users end up
reverting to signing Allow mode transactions, just to make it work. This is a
very bad habit to teach our users, and we can do better. What the user really
cares about is something like, “When I call contract X, at most m STX and n
USDCx may be moved from my wallet.” They don’t care if contract X sends sBTC to
contract Y, which sends USDCx to contract Z, etc. Originator mode allows the
user to specify only the restrictions on their own assets, allowing any
movements of assets amongst other principals.

MAY SEND fills an obvious gap in the existing post-condition implementation,
and does not require any more introduction or motivation.

Specification

The specification for both of these is based on and builds off of the original
post-condition specification in
SIP-005.

Originator Mode

All post-conditions continue to be evaluated in the same way, with the addition
that in Originator mode, the transaction’s origin account’s assets are
protected as in Deny mode, while other principals are protected as in Allow
mode.

The origin account is the account identified by the origin spending condition in
the transaction authorization structure (SIP-005). It is fixed for the
transaction and does not change during execution. It is distinct from
tx-sender, and does not change under as-contract?. In sponsored
transactions, it is the account that signs first (the origin).

In Originator mode, all post-conditions MUST hold. Additionally, when
enforcing allowlist coverage, a transaction MUST be rejected only if there
exists an asset transfer not covered by any post-condition whose sending
principal is the origin account. Uncovered transfers from any other principal
are permitted.

The transaction encoding reserves 1-byte for the post-condition mode.
Originator mode uses the value 0x03.

MAY SEND Condition

This new MAY SEND condition shall be implemented as a check on the NFT asset
movement upon completion of the transaction. It is always satisfied regardless
of whether the specified NFT is sent, and it is treated as covering that
specific NFT instance for the specified principal when enforcing allowlist
coverage in Deny or Originator mode. In Allow mode, it is redundant but
valid.

The transaction encoding reserves 1-byte for the non-fungible condition code.
MAY SEND adds a new acceptable value for this byte, 0x12.

Updates to the SIP-005 Specification

Adding these changes into SIP-005’s spec produces the following diff:

diff --git a/sips/sip-005/sip-005-blocks-and-transactions.md b/sips/sip-005/sip-005-blocks-and-transactions.md
index 64d3a5d..253164b 100644
--- a/sips/sip-005/sip-005-blocks-and-transactions.md
+++ b/sips/sip-005/sip-005-blocks-and-transactions.md
@@ -305,10 +305,12 @@ The Stacks blockchain supports the following two types of comparators:
 * **Non-fungible asset state** -- that is, a question of _whether or not_ an
   account sent a non-fungible asset when the transaction ran.

-In addition, the Stacks blockchain supports an "allow" or "deny" mode for
-evaluating post-conditions:  in "allow" mode, other asset transfers not covered
-by the post-conditions are permitted, but in "deny" mode, no other asset
-transfers are permitted besides those named in the post-conditions.
+In addition, the Stacks blockchain supports an "allow", "deny", or "originator"
+mode for evaluating post-conditions:  in "allow" mode, other asset transfers
+not covered by the post-conditions are permitted, but in "deny" mode, no other
+asset transfers are permitted besides those named in the post-conditions, and
+in "originator" mode, no other asset transfers from the transaction's origin
+account are permitted besides those named in the post-conditions.

 Post-conditions are meant to be added by the user (or by the user's wallet
 software) at the moment they sign with their origin account.  Because the
@@ -359,6 +361,8 @@ encoded as big-endian.
      post-conditions.
    * `0x02`:  This transaction may NOT affect other assets besides those listed
      in the post-conditions.
+   * `0x03`: This transaction may NOT affect other assets from the origin
+     account besides those listed in the post-conditions.
 * A length-prefixed list of **post-conditions**, describing properties that must be true of the
   originating account's assets once the transaction finishes executing.  It is encoded as follows:
    * A 4-byte length, indicating the number of post-conditions.
@@ -625,6 +629,7 @@ non-fungible token, with respect to whether or not the particular non-fungible
 token is owned by the account.  It can take the following values:
 * `0x10`: "The account will SEND this non-fungible token"
 * `0x11`: "The account will NOT SEND this non-fungible token"
+* `0x12`: "The account may SEND this non-fungible token"

 Post-conditions are defined in terms of which assets each account sends or
 does not send during the transaction's execution.  To enforce post-conditions,

Related Work

  • This SIP adds to the existing specification for post-conditions in
    SIP-005
  • This SIP will activate in epoch 3.4 together with
    SIP-clarity5
7 Likes

Very cool! Would the user set parameters take place at the app level? Perhaps the Pillar smart wallet @raphasierra.id.block and @friedger are working on would maybe be able to create some parameter settings at the wallet level for users?

2 Likes

Right, these new post-conditions would be set by the app, just like the existing post-conditions. We would need wallets to add support for the new kinds.

3 Likes

Great SIP — Originator mode is a big step forward.

One gap worth flagging for smart wallet / account-abstraction use cases like Pillar: the origin account is the tx broadcaster’s standard address, not the user’s smart wallet contract that actually holds and moves funds. So Originator mode protects the signer’s address, while the smart wallet — the principal we actually want to protect — gets treated as “any other principal” with uncovered transfers allowed.

This means to protect the smart wallet contract, we’re left with Deny mode, which forces us to list every asset flow in the transaction — not just outflows from the smart wallet, but all intermediate movements between contracts. It’s tedious (we’ve managed so far with long if-then-else conditions) but it’s exactly the kind of verbosity this SIP is trying to eliminate.

Would it be worth considering a mode or extension where post-conditions can protect a designated principal (e.g. the smart wallet contract), not just the tx origin? That would make this SIP work seamlessly for account-abstraction patterns where signer ≠ asset holder.

2 Likes

My thought here would be that the smart wallet should protect its own assets using the as-contract? internal post-conditions.

2 Likes

For additional discussion/context from the Feb 27 SIP call (Brice joined the call), check the recap: Weekly SIP Call #164 – Call Recap | Fri, 27 Feb 2026