-
Notifications
You must be signed in to change notification settings - Fork 99
feat(commitment_nft): initialize auth model — document single deployer assumption and operational checklist #460
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,41 @@ | ||||||||||||
| //! # Commitment NFT Contract | ||||||||||||
| //! | ||||||||||||
| //! Mints and manages Soroban NFTs that represent user commitments. Each NFT | ||||||||||||
| //! records commitment parameters (duration, max loss, type, amount) and tracks | ||||||||||||
| //! whether the underlying commitment is still active. | ||||||||||||
| //! | ||||||||||||
| //! ## Auth Model and Single Deployer Assumption | ||||||||||||
| //! | ||||||||||||
| //! `initialize` must be called exactly once, immediately after deployment, | ||||||||||||
| //! by the entity that holds the private key of the `admin` address it | ||||||||||||
| //! registers. This is the **single deployer assumption**: the transaction that | ||||||||||||
| //! deploys the contract should also invoke `initialize` in the same | ||||||||||||
| //! transaction or atomically thereafter, so no third party can front-run it | ||||||||||||
| //! with a different admin. `admin.require_auth()` is called inside | ||||||||||||
| //! `initialize` to enforce this — the transaction must be signed by the admin | ||||||||||||
| //! key. | ||||||||||||
| //! | ||||||||||||
| //! Subsequent admin operations (pausing, whitelisting minters, upgrading) | ||||||||||||
| //! require the same `admin.require_auth()` check via the `require_admin` | ||||||||||||
| //! helper. | ||||||||||||
| //! | ||||||||||||
| //! ## Trust Boundaries | ||||||||||||
| //! | ||||||||||||
| //! | Caller role | What they can do | | ||||||||||||
| //! |-------------|-----------------| | ||||||||||||
| //! | Admin | Initialize, pause/unpause, set core contract, manage minter whitelist, upgrade, migrate, emergency mode | | ||||||||||||
| //! | Core contract (`set_core_contract`) | Call `mint` as an authorized minter | | ||||||||||||
| //! | Whitelisted minter (`add_authorized_contract`) | Call `mint` | | ||||||||||||
| //! | NFT owner | `transfer` (inactive NFTs only) | | ||||||||||||
| //! | Anyone | All view functions (`get_metadata`, `owner_of`, etc.) | | ||||||||||||
| //! | ||||||||||||
| //! ## Reentrancy | ||||||||||||
| //! | ||||||||||||
| //! 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`. | ||||||||||||
|
|
||||||||||||
| #![no_std] | ||||||||||||
| //! Commitment NFT contract. | ||||||||||||
| //! | ||||||||||||
|
|
@@ -180,7 +218,42 @@ pub struct CommitmentNFTContract; | |||||||||||
|
|
||||||||||||
| #[contractimpl] | ||||||||||||
| impl CommitmentNFTContract { | ||||||||||||
| /// Initialize the NFT contract | ||||||||||||
| /// Initialize the commitment NFT contract. | ||||||||||||
| /// | ||||||||||||
| /// # Single Deployer Assumption | ||||||||||||
| /// | ||||||||||||
| /// This function enforces the single deployer assumption: the caller must | ||||||||||||
| /// control the `admin` address being registered. `admin.require_auth()` is | ||||||||||||
| /// called before any state is written, ensuring the transaction must be | ||||||||||||
| /// signed by the admin key. This prevents front-running attacks where a | ||||||||||||
| /// third party deploys the contract and calls `initialize` with a different | ||||||||||||
| /// admin before the legitimate deployer acts. | ||||||||||||
| /// | ||||||||||||
| /// The recommended deployment pattern is to invoke `initialize` in the same | ||||||||||||
| /// transaction as the contract upload, leaving no window for front-running. | ||||||||||||
| /// | ||||||||||||
| /// This function may only be called once. Any subsequent call returns | ||||||||||||
| /// [`ContractError::AlreadyInitialized`]. | ||||||||||||
| /// | ||||||||||||
| /// # Parameters | ||||||||||||
| /// | ||||||||||||
| /// - `admin`: Address that will hold admin authority over the contract. | ||||||||||||
| /// Must authorize this transaction via `require_auth`. | ||||||||||||
| /// | ||||||||||||
| /// # Errors | ||||||||||||
| /// | ||||||||||||
| /// - [`ContractError::AlreadyInitialized`] — contract has already been | ||||||||||||
| /// initialized. | ||||||||||||
| /// | ||||||||||||
| /// # Security Notes | ||||||||||||
| /// | ||||||||||||
| /// - `admin.require_auth()` is invoked after the `AlreadyInitialized` | ||||||||||||
| /// guard, so a second caller cannot learn whether the contract is already | ||||||||||||
| /// initialized without paying for auth on a no-op. | ||||||||||||
|
Comment on lines
+251
to
+252
|
||||||||||||
| /// 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 likemint/transfer/settleare guarded) or extend the guard consistently so the documentation matches behavior.