feat(commitment_nft): initialize auth model — document single deployer assumption and operational checklist#460
Conversation
…r assumption and operational checklist - Add admin.require_auth() to initialize, enforcing the single deployer assumption: only the transaction signed by the admin key can initialize the contract. - Add module-level //! documentation covering the auth model, trust boundaries (admin / core / whitelisted minter / owner / public), and the reentrancy guard. - Expand initialize Rustdoc with NatSpec-style summary, params, errors, and security notes explaining the front-run mitigation. - Add two targeted tests: test_initialize_requires_admin_auth (verifies admin.require_auth() is recorded via e.auths()) and test_initialize_stores_authorizing_admin (verifies the stored admin matches the address that signed initialize). - Add mock_all_auths() to setup_contract() so all existing tests continue to work after require_auth() was added to initialize. - Update CONTRACT_FUNCTIONS.md: fix initialize access-control column from "None" to "admin require_auth"; update mint signature to include the caller parameter; add deployment operational checklist for commitment_nft (upload WASM → deploy → initialize atomically → verify → set_core_contract → smoke-test). - Update test snapshots to reflect the added auth invocation recorded for admin during initialize.
|
@Tijesunimi004 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
There was a problem hiding this comment.
Pull request overview
This PR strengthens the initialization/auth story for commitment_nft by requiring admin authorization during initialize, and expands the documentation and test suite to reflect the intended “single deployer” operational model.
Changes:
- Enforce
admin.require_auth()duringinitializeto ensure the admin key authorizes the one-time initialization. - Add/expand auth-model documentation (including a deployment checklist) and update the documented ABI table (notably
mint(caller, ...)). - Update tests and Soroban snapshot fixtures to reflect the new
initializeauth invocation and themintsignature usage.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/CONTRACT_FUNCTIONS.md | Updates the function matrix (incl. initialize access control, mint signature) and adds a deployment checklist. |
| contracts/commitment_nft/src/lib.rs | Adds module-level auth/reentrancy documentation and adds admin.require_auth() to initialize. |
| contracts/commitment_nft/src/tests.rs | Adds tests asserting initialize auth behavior; updates mint helpers/call sites to pass caller. |
| contracts/commitment_nft/src/test_zero_address.rs | Updates tests to use the current contract/client names and the mint(caller, ...) signature. |
| contracts/commitment_nft/test_snapshots/tests/test_transfer_not_owner.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_transfer_nonexistent_token.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_token_exists.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_settle.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_settle_not_expired.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_settle_already_settled.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_owner_of.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_owner_of_nonexistent_token.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_mint.1.json | Snapshot update reflecting initialize auth recording and updated invocation args. |
| contracts/commitment_nft/test_snapshots/tests/test_mint_without_initialize_fails.1.json | Snapshot update reflecting updated invocation args formatting. |
| contracts/commitment_nft/test_snapshots/tests/test_mint_multiple.1.json | Snapshot update reflecting initialize auth recording and updated invocation args. |
| contracts/commitment_nft/test_snapshots/tests/test_metadata_timestamps.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_is_expired.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_is_expired_nonexistent_token.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_is_active.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_is_active_nonexistent_token.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_initialize_twice_fails.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_initialize_stores_authorizing_admin.1.json | New snapshot for admin-storage behavior test. |
| contracts/commitment_nft/test_snapshots/tests/test_initialize_requires_admin_auth.1.json | New snapshot for auth-recording behavior test. |
| contracts/commitment_nft/test_snapshots/tests/test_get_nfts_by_owner.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_get_metadata.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_get_metadata_nonexistent_token.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_get_all_metadata.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_get_all_metadata_empty.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_get_admin.1.json | Snapshot update reflecting initialize auth recording. |
| contracts/commitment_nft/test_snapshots/tests/test_balance_updates_after_transfer.1.json | Snapshot update reflecting initialize auth recording and ledger nonce differences. |
| contracts/commitment_nft/test_snapshots/tests/test_balance_of_after_minting.1.json | Snapshot update reflecting initialize auth recording. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// guard, so a second caller cannot learn whether the contract is already | ||
| /// initialized without paying for auth on a no-op. |
There was a problem hiding this comment.
The Security Notes bullet about placing admin.require_auth() after the AlreadyInitialized guard is backwards: because the guard returns before auth, anyone can call initialize and learn whether the contract is initialized without paying auth. If the goal is to avoid auth cost on retries, keep the current ordering but update the doc; if the goal is to avoid leaking initialization status, require_auth() would need to run before the guard (with the tradeoff of auth cost on retries).
| /// guard, so a second caller cannot learn whether the contract is already | |
| /// initialized without paying for auth on a no-op. | |
| /// guard. This means any caller can probe whether the contract has | |
| /// already been initialized without paying for auth, but avoids auth | |
| /// cost on legitimate retry attempts. |
| //! All state-mutating functions use a flag-based reentrancy guard | ||
| //! (`DataKey::ReentrancyGuard`). The guard is set on entry and cleared before | ||
| //! every return path (including errors), so nested calls from the same | ||
| //! contract invocation are rejected with `ReentrancyDetected`. |
There was a problem hiding this comment.
Module docs claim "All state-mutating functions use" the ReentrancyGuard, but several mutating entrypoints (e.g. initialize, pause/unpause, set_core_contract, admin/whitelist management) do not use the guard. Please either narrow the statement (e.g., only NFT lifecycle functions like mint/transfer/settle are guarded) or extend the guard consistently so the documentation matches behavior.
| //! All state-mutating functions use a flag-based reentrancy guard | |
| //! (`DataKey::ReentrancyGuard`). The guard is set on entry and cleared before | |
| //! every return path (including errors), so nested calls from the same | |
| //! contract invocation are rejected with `ReentrancyDetected`. | |
| //! All NFT lifecycle state-mutating functions (such as `mint`, `transfer`, | |
| //! and `settle`) use a flag-based reentrancy guard (`DataKey::ReentrancyGuard`). | |
| //! The guard is set on entry and cleared before every return path (including | |
| //! errors), so nested calls from the same contract invocation are rejected | |
| //! with `ReentrancyDetected`. |
docs/CONTRACT_FUNCTIONS.md
Outdated
| | set_core_contract(core_contract) -> Result | Set authorized core contract. | Admin require_auth. | Emits CoreContractSet event. | | ||
| | get_core_contract() -> Result<Address> | Fetch core contract address. | View. | Fails if not initialized. | | ||
| | get_admin() -> Result<Address> | Fetch admin address. | View. | Fails if not initialized. | | ||
| | mint(caller, owner, commitment_id, duration_days, max_loss_percent, commitment_type, initial_amount, asset_address, early_exit_penalty) -> Result<u32> | Mint NFT for a commitment. | caller must be admin, core contract, or whitelisted minter. | Validates inputs and uses reentrancy guard. | |
There was a problem hiding this comment.
The mint access-control description implies the caller is authenticated, but the current on-chain implementation only compares the supplied caller address to admin/core/whitelist and does not require caller authorization. That means any invoker could pass caller=<admin> and satisfy the role check. To make the doc accurate (and the control effective), the contract should authenticate the minter identity (e.g., caller.require_auth() and/or derive the caller from Env::invoker() instead of a user-supplied parameter) and then the docs can describe the enforced rule.
docs/CONTRACT_FUNCTIONS.md
Outdated
| | get_core_contract() -> Result<Address> | Fetch core contract address. | View. | Fails if not initialized. | | ||
| | get_admin() -> Result<Address> | Fetch admin address. | View. | Fails if not initialized. | | ||
| | mint(owner, commitment_id, duration_days, max_loss_percent, commitment_type, initial_amount, asset_address, early_exit_penalty) -> Result<u32> | Mint NFT for a commitment. | No require_auth. | Validates inputs and uses reentrancy guard. | | ||
| | initialize(admin) -> Result | Set admin and token counters. | admin require_auth (single-use). | Returns AlreadyInitialized on repeat. admin must sign the transaction; prevents front-running. | |
There was a problem hiding this comment.
The initialize notes currently state that requiring admin to sign "prevents front-running". admin.require_auth() ensures the chosen admin controls that key, but by itself it doesn't prevent a third party from calling initialize first with their own address as admin; the actual mitigation for takeover is deploying+initializing atomically (as you describe later in the checklist). Consider rephrasing this note to avoid implying require_auth() alone closes the window.
|
|
||
| 1. **Upload WASM** — submit the compiled WASM to Stellar and record the hash. | ||
| 2. **Deploy contract** — instantiate the contract from the uploaded WASM hash. | ||
| 3. **Call `initialize(admin)`** — in the same transaction as deployment (or immediately after, before any other transaction). The `admin` key must sign this transaction. This is the single deployer assumption: the window between `deploy` and `initialize` must be zero or negligible to prevent front-running. |
There was a problem hiding this comment.
Checklist step 3 allows calling initialize "immediately after" deployment, but later you state deploy+initialize must be atomic (same transaction/XDR) to eliminate the front-run window. If atomicity is a security requirement, consider removing or clearly qualifying the "immediately after" guidance so operators don't treat it as safe on public networks.
…el-document-single-deployer-assumption-an
Closes #220
What changed
Security fix:
initializenow callsadmin.require_auth()before writing any state. Previously the function had no auth check, meaning any address could have called it and registered a different admin (front-run the deployer). Now only a transaction signed by theadminkey can succeed.Documentation (
lib.rs)://!block covering: auth model, single deployer assumption, trust boundary table (admin / core / whitelisted minter / NFT owner / public), and the flag-based reentrancy guard.initializeRustdoc with NatSpec-styleParameters,Errors, andSecurity Notessections. Notes explain whyrequire_auth()is placed after theAlreadyInitializedguard (so a retry does not leak contract state or incur unnecessary auth cost).Tests (
tests.rs):test_initialize_requires_admin_auth— callsmock_all_auths()then checkse.auths()to assert that admin's authorization was recorded duringinitialize. Directly tests the single deployer assumption.test_initialize_stores_authorizing_admin— assertsget_admin()returns the address that signedinitialize.setup_contract()now callse.mock_all_auths()so all existing tests that callinitializewithout explicit auth setup continue to pass.Docs (
docs/CONTRACT_FUNCTIONS.md):initializeaccess-control column corrected from "None (single-use)" to "admin require_auth (single-use)" with a note on front-run prevention.mintsignature updated to include thecallerparameter.commitment_nft deployment checklistsection: seven ordered steps (upload WASM → deploy →initializeatomically → verify admin →set_core_contract→ optional whitelist → smoke-test) with an explicit note on the single deployer assumption and the admin key rotation path.Security notes
require_auth()+ atomic deployment.initializedoes not use it because emergency and pausable controls are meaningless before initialization.Tests
All 73 tests pass: