1- # ClipCash NFT Contract — Security Audit Preparation
1+ # ClipCash NFT Contract � Security Audit Preparation
22
3- > ** Contract:** ` clips-nft-contract ` v1.0 .0
3+ > ** Contract:** ` clips-nft-contract ` v1.1 .0
44> ** Chain:** Stellar / Soroban (SDK 22.0.0)
55> ** Prepared:** 2026-07-29
66> ** Status:** ?? Ready for External Audit (findings documented below)
@@ -46,17 +46,17 @@ The backend's interaction surface with this contract is documented in Section 11
4646
4747The ` ClipsNftContract ` is a Soroban-native NFT contract implementing:
4848
49- - ** Soulbound flag** — tokens marked ` is_soulbound = true ` are non-transferable and cannot be approved for delegation.
50- - ** Admin-gated minting** — only the stored ` admin ` address may call ` mint ` .
51- - ** Single-approval model** — one approved spender per token; approval is consumed on ` transfer_from ` .
52- - ** Royalty BPS** — configurable default royalty (0– 10 000 BPS) stored in instance storage.
53- - ** Creator provenance** — ` creator ` is set to the initial ` to ` address at mint and never changes.
49+ - ** Soulbound flag** � tokens marked ` is_soulbound = true ` are non-transferable and cannot be approved for delegation.
50+ - ** Admin-gated minting** � only the stored ` admin ` address may call ` mint ` .
51+ - ** Single-approval model** � one approved spender per token; approval is consumed on ` transfer_from ` .
52+ - ** Royalty BPS** � configurable default royalty (0� 10 000 BPS) stored in instance storage.
53+ - ** Creator provenance** � ` creator ` is set to the initial ` to ` address at mint and never changes.
5454
5555### Public Entry Points
5656
5757| Function | Caller Auth | Admin-gated |
5858| ---| ---| ---|
59- | ` initialize(admin) ` | none (first-caller wins) | — |
59+ | ` initialize(admin) ` | none (first-caller wins) | � |
6060| ` mint(to, token_id, clip_id, content_uri, is_soulbound) ` | admin | YES |
6161| ` transfer(from, to, token_id) ` | token owner (` from ` ) | NO |
6262| ` transfer_from(spender, from, to, token_id) ` | approved spender | NO |
@@ -93,7 +93,7 @@ pub fn initialize(env: Env, admin: Address) -> Result<(), Error> {
9393
9494** Finding AC-01 (Medium):** ` initialize ` does ** not** call ` admin.require_auth() ` . Any address can call
9595` initialize ` with any ` admin ` value ** before** the real deployer does. On Soroban this is a race condition
96- — whoever calls ` initialize ` first controls the contract.
96+ � whoever calls ` initialize ` first controls the contract.
9797
9898See Section 9.1 for the full finding and recommended fix.
9999
@@ -109,7 +109,7 @@ admin.require_auth();
109109```
110110
111111** Assessment:** PASS. Only the stored admin can invoke ` mint ` . Soroban's ` require_auth() ` enforces
112- on-ledger authorization — no off-chain bypass is possible.
112+ on-ledger authorization � no off-chain bypass is possible.
113113
114114---
115115
@@ -124,7 +124,7 @@ if token_data.owner != from { return Err(Error::Unauthorized); }
124124```
125125
126126** Assessment:** PASS. Dual check: (1) ` from ` must authorize the call, (2) ` from ` must be the stored owner.
127- Both are required — passing auth but wrong owner, or right owner without auth, both fail.
127+ Both are required � passing auth but wrong owner, or right owner without auth, both fail.
128128
129129---
130130
@@ -177,7 +177,7 @@ overflow-checks = true
177177```
178178
179179** Assessment:** PASS. All integer arithmetic in the release binary is compiled with overflow panics.
180- Soroban's ` panic = "abort" ` means overflows abort the transaction cleanly — no undefined behaviour.
180+ Soroban's ` panic = "abort" ` means overflows abort the transaction cleanly � no undefined behaviour.
181181
182182---
183183
@@ -192,7 +192,7 @@ pub fn increment_total_supply(env: &Env) {
192192}
193193```
194194
195- ** Assessment:** PASS. ` u64 ` with ` overflow-checks = true ` . Overflow would abort at 2^64 - 1 tokens —
195+ ** Assessment:** PASS. ` u64 ` with ` overflow-checks = true ` . Overflow would abort at 2^64 - 1 tokens �
196196practically unreachable.
197197
198198---
@@ -212,7 +212,7 @@ Off-chain royalty calculation `price * bps / 10_000` is performed in the NestJS
212212not in the contract itself.
213213
214214** Assessment:** PASS. Bounds checked. Royalty application arithmetic is not enforced on-chain
215- (by design — Soroban does not natively execute royalty enforcement during transfers).
215+ (by design � Soroban does not natively execute royalty enforcement during transfers).
216216
217217---
218218
@@ -228,6 +228,33 @@ storage::get_owner_tokens(&env, &owner).len() as u64
228228
229229---
230230
231+ ### 4.5 Royalty Multiplication Overflow (Issue #689 )
232+
233+ ** Location:** ` src/lib.rs ` — ` calculate_fractional_royalty ` , ` transfer_with_royalty ` , ` pay_royalty_with_asset ` .
234+
235+ Three call sites multiply an unbounded, caller-supplied ` sale_price ` / ` amount ` by ` royalty_bps `
236+ before dividing by ` ROYALTY_BPS_MAX ` . ` overflow-checks = true ` (4.1) would abort the whole
237+ transaction on overflow in a release build, but that's an opaque panic rather than a typed error,
238+ and the previous ` pay_royalty_with_asset ` used ` saturating_mul ` , which does * not* panic — it
239+ silently clamps to the type's max value and would have paid out a nonsensical royalty amount.
240+ All three sites now use ` checked_mul ` and return ` Error::RoyaltyOverflow ` explicitly:
241+
242+ | Function | Arithmetic type | Overflows only when ` sale_price ` / ` amount ` exceeds |
243+ | ---| ---| ---|
244+ | ` calculate_fractional_royalty ` | ` u128 ` | ` u128::MAX / 10_000 ` (~ 3.4 × 10³⁴) |
245+ | ` transfer_with_royalty ` | ` u64 ` | ` u64::MAX / 10_000 ` (~ 1.84 × 10¹⁵ stroops, ~ 184M XLM) |
246+ | ` pay_royalty_with_asset ` | ` i128 ` | ` i128::MAX / 10_000 ` (~ 1.7 × 10³⁴) |
247+
248+ ` royalty_bps ` is bounds-checked to ` ROYALTY_BPS_MAX ` (10 000) before the multiplication in all
249+ three functions, so these are the only overflow conditions possible.
250+
251+ ** Assessment:** PASS. Extreme values are rejected with a typed ` Error::RoyaltyOverflow ` instead of
252+ an opaque panic or a silently-clamped payout. Covered by
253+ ` test_fractional_royalty_overflow_is_rejected ` , ` test_transfer_with_royalty_overflow_is_rejected ` ,
254+ and ` test_pay_royalty_with_asset_overflow_is_rejected ` in ` src/test.rs ` .
255+
256+ ---
257+
231258## 5. Reentrancy Risk Review
232259
233260### 5.1 Soroban Execution Model
@@ -269,8 +296,8 @@ highest-risk entry points and should be the focus of access control review durin
269296
270297### Privileged Deployment Sequence
271298
272- 1 . ` stellar contract deploy ` — obtains ` CONTRACT_ID `
273- 2 . ` stellar contract invoke initialize --admin <ADMIN_ADDRESS> ` — ** must be called atomically/immediately post-deploy**
299+ 1 . ` stellar contract deploy ` � obtains ` CONTRACT_ID `
300+ 2 . ` stellar contract invoke initialize --admin <ADMIN_ADDRESS> ` � ** must be called atomically/immediately post-deploy**
2743013 . Off-chain backend reads ` CONTRACT_ID ` from environment and routes mint calls through the admin key
275302
276303> ** FINDING AC-01 (Medium):** The gap between deploy and ` initialize ` is a frontrunning window.
@@ -323,7 +350,7 @@ bare `token_id` key for token data. No collision risk identified.
323350| ` mint ` | ` ("mint", to) ` | ` (token_id, is_soulbound) ` | PASS |
324351| ` transfer ` | ` ("transfer", from, to) ` | ` token_id ` | PASS |
325352| ` approve ` | ` ("approve", owner, spender) ` | ` token_id ` | PASS |
326- | ` set_default_royalty_bps ` | — | — | MISSING |
353+ | ` set_default_royalty_bps ` | � | � | MISSING |
327354
328355** FINDING EV-01 (Low):** ` set_default_royalty_bps ` does not emit an event. Off-chain indexers
329356cannot detect royalty changes without polling. A ` royalty_updated ` event should be added.
@@ -338,7 +365,7 @@ cannot detect royalty changes without polling. A `royalty_updated` event should
338365observes the deploy transaction can immediately call ` initialize ` with a malicious admin address
339366before the legitimate deployer does.
340367
341- ** Impact:** Total contract takeover — malicious admin controls minting.
368+ ** Impact:** Total contract takeover � malicious admin controls minting.
342369
343370** Likelihood:** Low in practice (requires monitoring the mempool and acting faster than the deployer),
344371but non-zero on high-traffic networks.
@@ -358,7 +385,7 @@ pub fn initialize(env: Env, admin: Address) -> Result<(), Error> {
358385
359386Alternatively, bundle deploy + initialize in one transaction using ` stellar contract deploy --invoke-args ` .
360387
361- ** Status:** OPEN — requires code change before mainnet deployment.
388+ ** Status:** OPEN � requires code change before mainnet deployment.
362389
363390---
364391
@@ -381,7 +408,7 @@ pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), Error> {
381408}
382409```
383410
384- ** Status:** DEFERRED — acceptable for v1 if admin key is a hardware-secured multisig.
411+ ** Status:** DEFERRED � acceptable for v1 if admin key is a hardware-secured multisig.
385412
386413---
387414
@@ -399,7 +426,7 @@ entries may be archived, making the contract non-functional until restored.
399426- Add ` env.storage().persistent().extend_ttl(&key, MIN_TTL, MAX_TTL) ` on every ` set_token ` and ` set_owner_token ` .
400427- Operate a keepalive bot that periodically calls a no-op function to extend the instance TTL.
401428
402- ** Status:** OPEN — critical for long-term mainnet operation.
429+ ** Status:** OPEN � critical for long-term mainnet operation.
403430
404431---
405432
@@ -420,7 +447,7 @@ pub fn emit_royalty_updated(env: &Env, old_bps: u32, new_bps: u32) {
420447}
421448```
422449
423- ** Status:** OPEN — low risk, should be resolved before mainnet for transparency.
450+ ** Status:** OPEN � low risk, should be resolved before mainnet for transparency.
424451
425452---
426453
@@ -432,7 +459,7 @@ with no format validation.
432459** Impact:** An admin could mint tokens with empty URIs or malformed URIs. Integrity depends entirely
433460on the off-chain backend enforcing URI format.
434461
435- ** Status:** INFORMATIONAL — acceptable for v1 with trusted admin.
462+ ** Status:** INFORMATIONAL � acceptable for v1 with trusted admin.
436463
437464---
438465
@@ -445,7 +472,7 @@ a marketplace escrow address.
445472** Impact:** Royalty recipient attribution could be incorrect if the off-chain system mints to a
446473non-creator wallet.
447474
448- ** Status:** INFORMATIONAL — a design decision. The backend must always pass the actual creator's
475+ ** Status:** INFORMATIONAL � a design decision. The backend must always pass the actual creator's
449476wallet as ` to ` . Consider adding a separate ` creator ` parameter to ` mint ` for future flexibility.
450477
451478---
@@ -478,20 +505,20 @@ The test suite (`src/test.rs`) covers **30 test cases** across the following sce
478505The NestJS backend (` src/nft/ ` ) interacts with this contract via the Stellar Soroban RPC.
479506Key trust assumptions auditors should be aware of:
480507
481- 1 . ** Admin key custody** — The private key corresponding to the ` admin ` address used in ` initialize `
508+ 1 . ** Admin key custody** � The private key corresponding to the ` admin ` address used in ` initialize `
482509 is held by the backend operator. It is never transmitted over the network; the backend signs
483510 transactions server-side. Auditors should verify ` STELLAR_SECRET_KEY ` is loaded from secrets
484511 management (not hardcoded in source or ` .env ` ).
485512
486- 2 . ** ` mint ` invocation** — The backend's ` NftService.mintClip() ` calls ` mint ` after validating
513+ 2 . ** ` mint ` invocation** � The backend's ` NftService.mintClip() ` calls ` mint ` after validating
487514 clip ownership and status via ` NftMintGuard ` . The guard is enforced at the HTTP layer, not
488515 on-chain. On-chain, only the admin auth check applies.
489516
490- 3 . ** Signature verification for ` prepare-mint ` ** — ` MintSignatureVerificationService ` verifies an
517+ 3 . ** Signature verification for ` prepare-mint ` ** � ` MintSignatureVerificationService ` verifies an
491518 Ed25519 wallet signature before building the XDR. This prevents unauthorized users from
492519 preparing mint transactions on behalf of others but does ** not** substitute for on-chain auth.
493520
494- 4 . ** Royalty enforcement** — Royalty BPS is configured on-chain but royalty collection occurs
521+ 4 . ** Royalty enforcement** � Royalty BPS is configured on-chain but royalty collection occurs
495522 off-chain. Secondary marketplaces are not forced by the contract to honour royalties.
496523
497524### Open Questions for Auditors
@@ -508,7 +535,7 @@ Key trust assumptions auditors should be aware of:
508535
509536## 12. Audit Checklist
510537
511- ### Pre-Audit (Internal — track before sending to auditors)
538+ ### Pre-Audit (Internal � track before sending to auditors)
512539
513540- [x] Access control review complete
514541- [x] Overflow handling review complete
@@ -518,17 +545,17 @@ Key trust assumptions auditors should be aware of:
518545- [x] Event emission review complete
519546- [x] Findings documented with severity ratings
520547- [x] Test coverage mapped
521- - [ ] ** AC-01 fix merged** — ` admin.require_auth() ` added to ` initialize `
522- - [ ] ** ST-01/ST-02 fix merged** — ` extend_ttl ` calls added to write functions
523- - [ ] ** EV-01 fix merged** — ` royalty_updated ` event added to ` set_default_royalty_bps `
548+ - [ ] ** AC-01 fix merged** � ` admin.require_auth() ` added to ` initialize `
549+ - [ ] ** ST-01/ST-02 fix merged** � ` extend_ttl ` calls added to write functions
550+ - [ ] ** EV-01 fix merged** � ` royalty_updated ` event added to ` set_default_royalty_bps `
524551- [ ] Deploy scripts reviewed for atomic initialize pattern
525552- [ ] Admin private key stored in secrets manager (not committed ` .env ` file)
526553- [ ] Soroban SDK dependency pinned to exact version (no range specifiers)
527554- [ ] Contract WASM size within Soroban deployment limits verified
528555
529556### Auditor Review (complete during external audit)
530557
531- - [ ] Access control — all ` require_auth() ` call sites verified
558+ - [ ] Access control � all ` require_auth() ` call sites verified
532559- [ ] Storage key collision analysis complete
533560- [ ] Event completeness verified
534561- [ ] Arithmetic safety confirmed
0 commit comments