Enforce PostConditionMode.Deny across all contract interactions#252
Merged
Enforce PostConditionMode.Deny across all contract interactions#252
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
PostConditionMode.AllowtoPostConditionMode.Denyin test-contract.cjs and all frontend transaction pathsopenContractCallShared Post-Condition Modules
scripts/lib/post-conditions.cjs(CJS for CLI scripts)frontend/src/lib/post-conditions.js(ESM for React frontend)tipPostCondition,maxTransferForTip,feeForTip,totalDeduction,recipientReceivesFEE_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)Linting and CI
no-restricted-propertiesrule banningPostConditionMode.Allowno-restricted-syntaxrule catching bracket-notation accessscripts/audit-post-conditions.shgrep-based audit scriptScript Improvements
Documentation
docs/POST-CONDITION-GUIDE.md— comprehensive enforcement strategyscripts/README.md— full scripts directory documentation.env.examplewith all supported variablesTesting
Verification
Deployment Impact
No changes to Vercel build configuration. All CI-only and docs-only changes are outside the build path. Frontend build verified.