Skip to content

Enforce PostConditionMode.Deny across all contract interactions#252

Merged
Mosas2000 merged 40 commits intomainfrom
fix/enforce-post-condition-deny
Mar 10, 2026
Merged

Enforce PostConditionMode.Deny across all contract interactions#252
Mosas2000 merged 40 commits intomainfrom
fix/enforce-post-condition-deny

Conversation

@Mosas2000
Copy link
Copy Markdown
Owner

@Mosas2000 Mosas2000 commented Mar 10, 2026

Summary

Fixes #223 — All on-chain calls used PostConditionMode.Allow, permitting the contract to transfer arbitrary STX from users without explicit limits.

Core Security Fix

  • Switch PostConditionMode.Allow to PostConditionMode.Deny in test-contract.cjs and all frontend transaction paths
  • Attach explicit STX post conditions with fee-aware ceilings on every openContractCall

Shared Post-Condition Modules

  • scripts/lib/post-conditions.cjs (CJS for CLI scripts)
  • frontend/src/lib/post-conditions.js (ESM for React frontend)
  • Exported helpers: tipPostCondition, maxTransferForTip, feeForTip, totalDeduction, recipientReceives
  • Constants: FEE_BASIS_POINTS, BASIS_POINTS_DIVISOR, FEE_PERCENT, SAFE_POST_CONDITION- Constants: FEE_BASIS_POINTS, BASIS_POINTS_DIVISOR, FEE_PERCENT, SAFE_POST_CONDITION- Constants: FEE_BASIS_POINTS, BASIS_POINTS_DIVISOR, FEE_PERCENT, SAFE_POST_CONDITION- Constants: FEE_BASIncy checks (tip + fee vs wallet balance)
  • Post-condition-specific error messages when transactions are rejected
  • Removed all inline fee math in favor of shared helpers

Linting and CI

  • ESLint no-restricted-properties rule banning PostConditionMode.Allow
  • ESLint no-restricted-syntax rule catching bracket-notation access
  • scripts/audit-post-conditions.sh grep-based audit script
  • New CI job running post-condition audit on every push and PR

Script Improvements

  • Configurable AMOUNT, MESSAGE, DRY_RUN environment variables
  • Mnemonic word-count and recipient format validation
  • Self-tip rejection before wallet derivation
  • Full fee breakdown in console output
  • Error output sanitization to prevent mnemonic leakage

Documentation

  • docs/POST-CONDITION-GUIDE.md — comprehensive enforcement strategy
  • scripts/README.md — full scripts directory documentation
  • Updated .env.example with all supported variables
  • CHANGELOG.md following Keep a Changelog format
  • README security section and project structure updates

Testing

  • 24 unit tests for all shared helpers (constants, edge cases, rounding)
  • 59 integration tests across realistic tip ranges
  • All 83 post-condition tests passing
  • Frontend build verified

Verification

cd frontend && npx vitest run src/test/post-conditions.test.js        # 24 pass
cd frontend && npx vitest run src/test/post-conditions-integration.test.js  # 59 pass
cd frontend && npx vite build                                          # success
bash scripts/audit-post-conditions.sh                                  # pass

Deployment Impact

No changes to Vercel build configuration. All CI-only and docs-only changes are outside the build path. Frontend build verified.

Bring in makeStandardSTXPostCondition and FungibleConditionCode
so we can build explicit STX transfer limits for the transaction.
Replace the permissive Allow mode with Deny and attach an
explicit STX post condition that caps the transfer at the tip
amount plus the maximum fee.  This prevents the transaction
from moving more funds than intended.
Define FEE_BASIS_POINTS and BASIS_POINTS_DIVISOR as named
constants mirroring the on-chain values so the post-condition
calculation is self-documenting and easy to update.
Read AMOUNT and MESSAGE from environment variables with safe
defaults.  Validate the minimum tip amount matches the contract
requirement of 1000 uSTX.
Set DRY_RUN=1 to build and inspect the transaction, post
conditions, and payload size without sending it to the network.
Useful for verifying post-condition setup before committing
real funds.
Compare derived sender address against the recipient and fail
fast with a clear message, matching the contract-level
self-tip rejection.
Centralize fee calculation, max-transfer computation, and
post-condition building so all CLI scripts enforce consistent
safety limits.
Replace inline post-condition building with the centralized
tipPostCondition helper and SAFE_POST_CONDITION_MODE constant.
Reduces duplication and ensures consistency across scripts.
Provide tipPostCondition, maxTransferForTip, and the
SAFE_POST_CONDITION_MODE constant for React components to use
when building contract calls.
Replace inline Pc builder and PostConditionMode import with
tipPostCondition and SAFE_POST_CONDITION_MODE from the shared
module.  The post condition now accounts for the fee ceiling.
Replace the inline Pc builder and PostConditionMode import in
the tip-a-tip flow with the centralized tipPostCondition and
SAFE_POST_CONDITION_MODE.
Cover maxTransferForTip with edge cases (minimum tip, large
amounts, non-round values, custom fees, zero fee, max fee)
and verify tipPostCondition returns valid objects.
Flag any usage of the permissive Allow mode as an error so
developers are directed to use Deny with explicit conditions
from the shared module.
…g to SendTip

Replace the simple breakdown panel with a detailed fee preview that shows
the platform fee percentage from shared constants, the total amount that
will leave the sender's wallet (tip plus fee), recipient net amount, and
the post-condition ceiling enforced on-chain.  Import maxTransferForTip
from the shared module so the displayed ceiling matches the actual
post-condition attached to the transaction.
Before this change the confirm dialog only showed the tip amount and
recipient.  Users had no visibility into the platform fee or total
deduction at the moment of final confirmation.  Add fee percentage,
fee amount, and total-from-wallet rows so the user sees the exact
same numbers shown in the fee preview panel before clicking Send.
The previous check compared only the tip amount against the wallet
balance.  A user with exactly 1 STX could enter 1 STX and pass
validation, but the on-chain transaction would fail because the
contract also deducts a 0.5% fee.  Calculate the total deduction
(tip plus fee) before checking the balance so users get an immediate
inline error when their balance cannot cover the full transaction.
The inline error on the amount input was already updated to include
the fee, but the submit-button guard still compared the raw tip
amount to the wallet balance.  Align validateAndConfirm with the
same total-deduction logic so neither code path can let an
underfunded transaction reach the wallet signer.
Extract the repeated fee math from SendTip into reusable functions
in the shared post-conditions module.  feeForTip computes the
platform fee using Math.ceil to match on-chain rounding.
totalDeduction returns tip-plus-fee.  recipientReceives returns
the net after splitting.  All three accept optional feeBps for
testing different fee tiers.
Every place that computed fee, total deduction, or recipient-receives
inline (handleAmountChange, validateAndConfirm, fee preview, and
confirm dialog) now calls feeForTip, totalDeduction, or
recipientReceives from the shared module.  This eliminates four
separate copies of the same Math.ceil / Math.floor expressions and
ensures the UI stays in sync with the on-chain post-condition logic
if the fee formula ever changes.
Mirror the same helper functions added to the ESM frontend module in
the CJS scripts module so both codebases use identical fee logic.
Export all three new functions alongside the existing helpers.
Cover the three new shared functions with 12 additional test cases
including round amounts, non-round amounts, zero-fee edge cases,
the relationship between totalDeduction and maxTransferForTip
(differ by exactly 1), and the ceil-vs-floor rounding divergence
between feeForTip and recipientReceives.  All 23 tests pass.
The test-contract.cjs script now supports configurable tip amount,
message text, and a dry-run mode, but these options were not
documented in the example file.  Add each variable with a brief
explanation and sensible defaults so contributors can discover the
full set of options without reading the script source.
Create a comprehensive developer guide explaining the post-condition
strategy: why Deny mode is required, how the fee model works, what
the shared modules export, how the ESLint rule catches violations,
how to add new contract calls safely, and where the unit tests live.
Intended as the canonical reference for anyone adding new on-chain
interactions to TipStream.
Grep-search all JS/TS source files in frontend/src, scripts, and
chainhook for direct usage of PostConditionMode.Allow.  Exclude
test files and comment lines to avoid false positives from JSDoc
and ESLint rule messages.  Exit non-zero if any match is found,
making it suitable for CI gate checks.
Run scripts/audit-post-conditions.sh as a standalone job on every
push and pull request targeting main.  The job has no dependencies
on other jobs and runs in under ten seconds, catching any accidental
re-introduction of PostConditionMode.Allow before code is merged.
Document every script in the scripts directory including environment
variables for test-contract.cjs, the shared post-conditions module,
the audit script, deploy helper, and hook setup.  Link to the full
post-condition guide for contributors who need the strategy details.
Add a JSDoc header comment explaining the purpose, environment
variables, usage examples, and security model so new contributors
can understand the script without reading the full source.
When a transaction error message mentions 'post-condition', display
a specific toast explaining that the post-condition check protected
the user's funds rather than the generic 'Failed to send tip' message.
This helps users understand that their wallet was never debited.
Apply the same post-condition-aware error detection used in SendTip
to the tip-a-tip catch block.  If the error message mentions
post-condition, explain that the user's funds are safe instead of
showing a generic failure toast.
Add two early checks before the async wallet derivation: verify that
the MNEMONIC has exactly 12 or 24 words (BIP-39 standard), and that
the RECIPIENT matches the SP... mainnet address pattern.  These
catch simple copy-paste errors before the script attempts expensive
wallet generation or network calls.
Test all helper functions against nine realistic tip amounts from
0.001 STX to 10000 STX and fourteen edge-case micro-amounts.
Verify that maxTransferForTip always exceeds totalDeduction by
exactly one, recipientReceives is always less than the tip, fee
plus net accounts for the full amount within rounding tolerance,
and the effective fee rate stays within expected bounds.  59 tests.
Replace the single 'Amount' log line with a detailed breakdown that
shows fee, total deduction, post-condition ceiling, mode, and
message.  Import feeForTip and totalDeduction from the shared module.
Also fix the STX conversion display to use the actual amount instead
of a hardcoded 0.001.
The existing no-restricted-properties rule catches dot-notation
PostConditionMode.Allow but not bracket-notation like
PostConditionMode['Allow'].  Add a complementary no-restricted-syntax
rule with an AST selector to close the bracket-notation loophole.
Document all security, added, and changed items from this branch
following the Keep a Changelog format.  List the mode switch to
Deny, new shared modules and helpers, UI changes, ESLint rules,
CI integration, tests, and documentation.
Add a @module tag and @see reference to the post-condition guide.
Export FEE_PERCENT as a precomputed human-readable value (0.5) so
UI components can display it directly without repeating the basis
point math.
Replace inline FEE_BASIS_POINTS / BASIS_POINTS_DIVISOR * 100
calculations in both the fee preview and confirm dialog with the
precomputed FEE_PERCENT constant.  Remove FEE_BASIS_POINTS and
BASIS_POINTS_DIVISOR from the import since they are no longer
referenced directly in this component.
Import FEE_PERCENT in the test suite and verify it equals 0.5 and
matches the computation from FEE_BASIS_POINTS / BASIS_POINTS_DIVISOR.
All 24 unit tests pass.
Highlight PostConditionMode.Deny as the first listed security
measure.  Mention the shared modules, ESLint rules, and CI pipeline
that enforce the policy.  Link to the new post-condition guide.
Add scripts/lib, scripts/audit-post-conditions.sh,
scripts/test-contract.cjs, docs/POST-CONDITION-GUIDE.md, and
frontend/src/test to the structure tree.  Update the lib directory
description to mention post-condition helpers.
If an error message happens to contain the mnemonic (e.g. from a
wallet-sdk stack trace), redact it before printing to stderr.  Also
exit with code 1 on error so CI pipelines correctly detect failures.
@Mosas2000 Mosas2000 merged commit 5d42596 into main Mar 10, 2026
3 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🟠 HIGH: test-contract.cjs uses PostConditionMode.Allow on mainnet transactions

1 participant