Skip to Content

Credit Controller Audit

Description

The credit controller is the sole authorized caller for CREDIT token transfers. It wraps credit-token.transfer with reward accounting, ensuring street-rewards state stays synchronized with LP token balances.

Findings

IDFindingSeverityStatus
I-01Single-step ownership transferInformationalBy Design

[I-01] Single-step ownership transfer

  • Severity: Informational
  • Location: set-contract-owner
  • Description: Ownership transfers in a single step with no confirmation from the new owner. If transferred to an incorrect address, ownership is permanently lost.
  • Impact: Low — the contract owner has no economic privileges (cannot mint, burn, or move funds). Ownership only controls the set-contract-owner function itself.
  • Status: Accepted. Consistent pattern across all protocol contracts. Minimal risk given the owner has no financial control.

Checklist Results

#CheckResult
1Access ControlPass — set-contract-owner gated by contract-caller check. transfer enforces caller-is-sender.
2Input ValidationPass — zero-amount check prevents no-op reward state updates.
3ArithmeticN/A — no arithmetic operations.
4Reentrancy / Call OrderingPass — Clarity atomic transactions. If any try! fails, entire transaction reverts.
5Asset SafetyPass — as-contract delegates to credit-token.transfer with controller as authorized caller. Balance pre-check fails early.
6Trait UsageN/A — no trait parameters.
7Authorization ChainsPass — uses contract-caller (not tx-sender) for both owner check and sender identity, preventing intermediary contract abuse.
8State ConsistencyPass — reward decrease-rewards and increase-rewards are both wrapped in try!. Failure at any step reverts all state.
9Denial of ServicePass — no blocking conditions. Any valid holder can transfer at any time.
10Upgrade / MigrationInformational — single-step ownership transfer (see I-01).

Recommendations

No code changes recommended. The single informational finding is a known design choice consistent across the protocol.

Contract Comments

;; Welsh Street Credit Controller ;; errors ;; 4 unique error codes with u91x prefix — all tested, no overlap with other contracts (define-constant ERR_ZERO_AMOUNT (err u911)) (define-constant ERR_NOT_CONTRACT_OWNER (err u912)) (define-constant ERR_NOT_TOKEN_OWNER (err u913)) (define-constant ERR_BALANCE (err u914)) ;; variables ;; initialized to deployer at deployment — transferable via set-contract-owner (define-data-var contract-owner principal tx-sender) ;; public entry point for CREDIT transfers — enforces reward accounting atomically (define-public (transfer (amount uint) (sender principal) (recipient principal) (memo (optional (buff 34))) ) (let ( ;; unwrap-panic is safe here — get-balance always returns (ok uint) for any principal (sender-balance (unwrap-panic (contract-call? .credit-token get-balance sender))) ) (begin ;; prevents zero-amount transfers that would update reward state without economic effect (asserts! (> amount u0) ERR_ZERO_AMOUNT) ;; uses contract-caller (not tx-sender) — prevents intermediary contracts from ;; transferring on behalf of a user without their direct invocation (asserts! (is-eq contract-caller sender) ERR_NOT_TOKEN_OWNER) ;; redundant with credit-token's own balance check but fails early, saving gas (asserts! (>= sender-balance amount) ERR_BALANCE) ;; as-contract delegates to credit-token.transfer — the controller is the authorized caller (try! (as-contract? () (unwrap-panic (contract-call? .credit-token transfer amount sender recipient memo)))) ;; reward state updated after transfer — atomic transaction guarantees consistency (try! (contract-call? .street-rewards decrease-rewards sender amount)) ;; if decrease succeeds but increase fails, transaction reverts entirely (atomic) (try! (contract-call? .street-rewards increase-rewards recipient amount)) ;; memo printed for indexer visibility — no state impact (begin (match memo content (print content) 0x) (ok { amount: amount }) ) ) ) ) ;; single-step ownership transfer — no two-step confirmation pattern (define-public (set-contract-owner (new-owner principal)) (begin ;; contract-caller check — only direct caller, not tx-sender chain (asserts! (is-eq contract-caller (var-get contract-owner)) ERR_NOT_CONTRACT_OWNER) ;; irreversible if transferred to wrong address — no recovery mechanism by design (var-set contract-owner new-owner) (ok true) ) ) ;; read-only — no access control needed, ownership is public information (define-read-only (get-contract-owner) (ok (var-get contract-owner)))
Last updated on