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.
// ❌ 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.
// ❌ 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.
// ✅ 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.
// ✅ 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.
// ❌ 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.
// ❌ 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
externalfunctions with transfers usenonReentrant - • Transfers happen AFTER state changes (checks-effects-interactions)
- • No recursive calls without guards
- • SafeERC20 used for all token transfers
Access Control
- • No
onlyOwnerwithout 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
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()incrementstotalLocked✅ - •
withdraw()decrementstotalLocked✅ - •
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
withdrawalClaimsmapping ✅ - • 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
| Vulnerability | Example | Fix |
|---|---|---|
| Silent math | uint x = a/b*c (wrong order) | Use (a*c)/b |
| Overflow abuse | Send huge amount, overflow happens | Use Solidity 0.8+ checks |
| Reentrancy | external_call() before state update | Use nonReentrant guard |
| Flashloan | Oracle price wrong in block | Use time-weighted avg |
| Front-running | MEV on fee discovery | Use commit-reveal or batch |
| Unguarded init | initialize() callable twice | Use 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:
function invariant_supplyConservation() public {
assert(
sigilLocked + sigilBridged == sigilCirculating
);
}
References
- •Solidity Security: https://solidity.readthedocs.io/en/latest/security-considerations.html
- •OpenZeppelin: https://docs.openzeppelin.com/contracts/
- •Rekt: https://rekt.news/ (Learn from failures)
- •Echidna Fuzzing: https://github.com/crytic/echidna