AgentSkillsCN

Solidity Security Audit

Solidity安全审计

SKILL.md

Skill: SigilBridge Smart Contract Security Audit

Scope: Solidity contracts in contracts/contracts/**/*.sol
When to activate: Any Solidity code review, test writing, or contract changes


Core Architecture Patterns (ENFORCE THESE)

1. ContractRegistry Pattern (ABI + Address)

Rule: NEVER hardcode contract ABIs or addresses.

solidity
// ❌ ANTI-PATTERN: Hardcoded address
address constant SETTLEMENT_ANCHOR = 0x1234...;

// ✅ GOOD: Injected via constructor + role-based access
contract CronosSigilLockbox {
    CronosSettlementAnchor public immutable settlementAnchor;
    
    constructor(address _settlementAnchor) {
        require(_settlementAnchor != address(0), "Invalid anchor");
        settlementAnchor = CronosSettlementAnchor(_settlementAnchor);
    }
}

2. Fee Precision (Scale by 10^18 for SIGIL, 10^6 for stablecoins)

Rule: Use Decimals library or explicit scaling. Never lose precision through division-first-then-multiply.

solidity
// ❌ BAD: Loses precision
uint256 fee = (amount / 1000) * 963; // If amount=500, fee=0

// ✅ GOOD: Multiply first, then divide
uint256 fee = (amount * 963) / 1000; // If amount=500, fee=481.5

// ✅ BEST: Use Fixed-point library or explicit scaling
uint256 constant FEE_BPS = 963; // 0.963% in basis points
uint256 fee = (amount * FEE_BPS) / 100000; // 100000 bps = 100%

Apply to:

  • PaymentRouter.sol: conversionFeeBps = 963 (0.963% in/out)
  • ResolutionExecutor.sol: protocolFeeBps = 1618 (1.618% resolution)
  • StakeVault.sol: reward rate calculations

3. Idempotency via Unique Constraints

Rule: Use mappings with exists flag to prevent double-processing.

solidity
// ✅ GOOD: Idempotent withdrawal verification
mapping(uint256 => WithdrawalClaim) public withdrawalClaims;

struct WithdrawalClaim {
    bool claimed;     // Idempotency flag
    uint256 claimedAt;
    address claimedBy;
}

function withdraw(...) external {
    require(!withdrawalClaims[withdrawalId].claimed, "Already claimed");
    withdrawalClaims[withdrawalId].claimed = true; // Mark FIRST, before external calls
    // ... then do transfers and events
}

Apply to:

  • CronosSettlementAnchor.verifyWithdrawal() ✅ (already has this)
  • Any state-changing function that relies on off-chain data

4. Reentrance Safety (ReentrancyGuard prefix)

Rule: All external functions that transfer tokens MUST use nonReentrant.

solidity
// ✅ GOOD: Guard on all external transfers
function withdraw(...) external nonReentrant { ... }
function deposit(...) external nonReentrant { ... }
function claimRewards(...) external nonReentrant { ... }

Apply to:

  • CronosSigilLockbox.withdraw() ✅ (has guard)
  • CronosSigilLockbox.deposit() ✅ (has guard)
  • PaymentRouter payouts ✅ (verify on review)
  • StakeVault.claimRewards() (verify on review)

5. Role-Based Access Control (AccessControl, not Ownable-only)

Rule: Use explicit roles, not owner-only.

solidity
// ❌ BAD: Only owner can do critical things
onlyOwner

// ✅ GOOD: Explicit roles with clear purpose
bytes32 public constant BRIDGE_ROLE = keccak256("BRIDGE_ROLE");
bytes32 public constant INDEXER_ROLE = keccak256("INDEXER_ROLE");
bytes32 public constant RESOLVER_ROLE = keccak256("RESOLVER_ROLE");

function withdraw(...) external onlyRole(BRIDGE_ROLE) nonReentrant { ... }

Apply to:

  • CronosSigilLockbox ✅ (has BRIDGE_ROLE, ADMIN_ROLE)
  • CronosSettlementAnchor ✅ (has INDEXER_ROLE, ADMIN_ROLE)
  • PaymentRouter (verify roles are granular)
  • ResolutionExecutor (verify RESOLVER_ROLE is distinct)

6. Supply Invariant Guards

Rule: No silent failure on supply mismatch. Log and halt immediately.

solidity
// ❌ BAD: Silently continues with wrong balance
uint256 actualBalance = sigilToken.balanceOf(address(this));
// ... uses totalLocked without checking actualBalance >= totalLocked

// ✅ GOOD: Asserts or requires before proceeding
require(
    sigilToken.balanceOf(address(this)) >= totalLocked,
    "Supply invariant broken: actual < locked"
);

Apply to:

  • CronosSigilLockbox.withdraw(): require(totalLocked >= amount, ...)
  • CronosSettlementAnchor: require(batch.exists, ...)
  • PaymentRouter: verify total distributed <= minted

Security Checklist (Every PR)

Reentrancy

  • All external functions with transfers use nonReentrant
  • Transfers happen AFTER state changes (checks-effects-interactions)
  • No recursive calls without guards
  • SafeERC20 used for all token transfers

Access Control

  • No onlyOwner without role-based alternative
  • All sensitive functions gated by explicit roles
  • Role derivation uses keccak256("ROLE_NAME")
  • Admin revocation of roles tested

Arithmetic

  • No integer overflow/underflow (Solidity 0.8.x has built-in checks, but verify)
  • Division-before-multiply avoided (precision loss)
  • Large numbers (decimals) scaled consistently
  • Fee calculations use basis-point model (BPS = 1/10000)

State Invariants

  • Supply invariant: locked + available = totalBalance
  • merkle roots immutable once set
  • Withdrawal idempotency enforced
  • No intermediate state leaks via events

Merkle Proofs

  • Proof format versioned (enum, not magic bytes)
  • Leaf hash matches execution-chain format (cross-platform compatibility)
  • Root stored immutably (ok for now; document PQC migration path)
  • Proof verification BEFORE state changes

Contract Interactions

  • All external calls documented (what can they do?)
  • Multi-contract sequences have guards against reordering
  • Error handling doesn't swallow revert reasons
  • Function ordering: query → state checks → state updates → transfers → events

Known Risks & Mitigations

Risk 1: Merkle Proof Forgery (Quantum Era)

Impact: HIGH (proofs become invalid post-quantum)
Mitigation:

  • Document PQC migration path in comments
  • Design dual-merkle strategy (Keccak + PQC later)
  • Use NIST PQC finalist (SPHINCS+) when ready
  • Phase 3+: Implement hybrid verification

Risk 2: Creator Fee Griefing (Economic)

Impact: MEDIUM (if creator sets fee=0.3%, house gets only 1.318%)
Mitigation:

  • Designer controlled via governance (Min/Max fee)
  • Fees soft-capped at 0.3% per architecture docs
  • Governance can adjust ceilings post-deployment
  • Fee changes use timelock (7 days minimum)

Risk 3: Missed Batch (Relayer Offline)

Impact: MEDIUM (batches skip, withdrawals pending)
Mitigation:

  • nextBatchId is sequential (no gaps allowed)
  • Missing batch blocks relayer from progressing
  • Emergency pause + recovery mode (doc in PHASE_2B)
  • Monitor batch gaps; alert on skips

Risk 4: Stale Oracle Prices

Impact: LOW-MEDIUM (wrong conversion rates)
Mitigation:

  • PaymentRouter prices must be updated frequently
  • Add timestamp check: require(priceUpdatedAt + 1 hours >= now, ...)
  • Implement Chainlink oracle fallback (Phase 3+)
  • Pause conversions if prices stale > 1 hour

Code Review Workflow

code
1. READ CONTRACT TOP-TO-BOTTOM
   - Understand role: Bridge custody? Fee collection? Settlement?
   - Identify dependencies: What does it call? What calls it?
   - Flag external calls: Any unsafe interactions?

2. CHECK SECURITY CHECKLIST
   - Reentrancy guards? ✅/⚠️/❌
   - Access control explicit? ✅/⚠️/❌
   - Arithmetic safe? ✅/⚠️/❌
   - Invariants guarded? ✅/⚠️/❌

3. RUN FUZZER (if applicable)
   - Generate random inputs
   - Check invariants hold
   - Report counterexample if broken

4. ESCALATE IF:
   - Immutable crypto params (no upgrade path)
   - New signature scheme (needs PQC specialist)
   - Cross-contract ordering issues
   - Fee precision edge cases

Contract-Specific Patterns

CronosSigilLockbox

Purpose: Custody of canonical SIGIL
Critical: Supply invariant must hold always
Review:

  • deposit() increments totalLocked
  • withdraw() decrements totalLocked
  • withdraw() checks proof before state change ✅
  • Emergency pause works ✅

CronosSettlementAnchor

Purpose: Merkle root storage + proof verification
Critical: Proof verification must be idempotent
Review:

  • verifyWithdrawal() marks claimed BEFORE transfer ✅
  • Double-spend prevented by withdrawalClaims mapping ✅
  • Merkle proof verification correct ✅
  • Batch ID sequential ⚠️ (monitor for gaps)

PaymentRouter

Purpose: Fee collection + conversion
Critical: Conversion fee precision (0.963%)
Review:

  • Fee calculated as (amount * 963) / 100000
  • Price oracle freshness checked
  • Supported tokens whitelist enforced
  • Admin can't steal user funds

ResolutionExecutor

Purpose: Payout distribution
Critical: Fee calculation (1.618% resolution)
Review:

  • Resolution fee is (payout * 1618) / 100000
  • Creator fee is (resolutionFee * creatorFeeBps) / 1000 (0-300 bps)
  • House gets remainder
  • Dispute resolution wired correctly

StakeVault

Purpose: Outcome pools + reward distribution
Critical: Reward rate never exceeds 8% APR
Review:

  • APR capped at 800 bps (8%)
  • Reward accrual is block-by-block
  • Lock-up period enforced (30 days)
  • Claim expiry prevents zombie funds

Common Vulnerabilities to Flag

VulnerabilityExampleFix
Silent mathuint x = a/b*c (wrong order)Use (a*c)/b
Overflow abuseSend huge amount, overflow happensUse Solidity 0.8+ checks
Reentrancyexternal_call() before state updateUse nonReentrant guard
FlashloanOracle price wrong in blockUse time-weighted avg
Front-runningMEV on fee discoveryUse commit-reveal or batch
Unguarded initinitialize() callable twiceUse initializer modifier

Testing Standards

Every contract must have:

  • Unit tests for happy path
  • Fuzz tests for invariants
  • Adversarial tests (can attacker steal funds?)
  • Integration tests (multi-contract sequences)
  • Gas cost benchmarks

Example Fuzz Invariant:

solidity
function invariant_supplyConservation() public {
    assert(
        sigilLocked + sigilBridged == sigilCirculating
    );
}

References