Security Audit

Internal audit by Opus (Claude Opus 4.6)

Scope: All contracts in src/core/, src/governance/, src/identity/, src/oracle/, src/libraries/ · Solidity 0.8.28 · OpenZeppelin v5.x

4
HIGH
9
MEDIUM
7
LOW
4/4
HIGH FIXED
20 total findings5 fixed · 4 acknowledged · 11 open

Trust Model

Non-Upgradeable Contracts

All contracts are immutable by design. Bugs cannot be patched post-deployment, but the system cannot be rugged by a malicious admin upgrade.

3-Oracle Median Consensus

Basket and ETH prices require 3+ oracles to agree via median consensus. A single compromised oracle cannot manipulate pricing.

Biometric Sybil Defense

Citizenship is gated by biometric verification (off-chain ZK proof, on-chain hash attestation). Each biometric identity maps to exactly one citizen.

Ratchet Principle

Floors can only increase, never decrease. Once a UBC minimum is set, it cannot be lowered even by governance. ConstitutionalGuard enforces this on-chain.

2-Day Execution Timelock

All governance proposals require a 2-day delay between passing and execution, giving citizens time to react to malicious proposals.

7-Day Minimum Voting Period

Governor proposals run for a minimum of 7 days, preventing flash loan governance attacks and ensuring broad participation.

Findings by Severity

High Severity

HIGH4 findings
H-1

Unverified vote weights

FIXED

Governor.sol

castVote(proposalId, support, stakeAmount) accepts stakeAmount from the caller without verifying it against any staking/conviction contract. Any address can pass arbitrary vote weight.

Fix: Now verifies on-chain via QuadraticVoting/ConvictionWeighting lookup.

H-2

Single-oracle ETH price manipulation

FIXED

ComputeBasketOracle.sol

submitETHPrice() allowed a single oracle to set the ETH price used by OneWayBridge.depositETH(), unlike basket prices which require 3-oracle median consensus. A compromised oracle could set ETH price to $1M, making ETH deposits mint disproportionate UCU.

Fix: ETH price now uses the same multi-oracle median consensus as basket prices.

H-3

Missing reentrancy guard

FIXED

Bonfire.sol

finalizeEpoch() makes an external call via _burnUCU() (low-level .call) and reportRevenue() calls safeTransferFrom(). Neither had ReentrancyGuard protection. Cross-function reentrancy could manipulate epoch accounting.

Fix: Added ReentrancyGuard inheritance and nonReentrant to reportRevenue() and finalizeEpoch().

H-4

Low-level call to non-contract succeeds silently

FIXED

Bonfire.sol

_burnUCU() used address(ucu).call(...) which succeeds silently when the target is an EOA (not a contract). If ucu were set to a non-contract address, burns would appear successful without actually burning tokens, inflating supply.

Fix: Added extcodesize check and return data validation.

Medium Severity

MEDIUM9 findings
M-1

No proposal threshold

OPEN

Governor.sol

Anyone can create proposals with no minimum stake or citizen requirement. This enables spam proposals that waste gas and create governance noise.

Mitigation: Add a minimum stake requirement or restrict to verified citizens.

M-2

Unbounded paramIds array (DoS vector)

OPEN

ConstitutionalGuard.sol

validateProposal() iterates all paramIds for each target in a proposal (O(targets * paramIds)). As protections accumulate, gas costs increase, eventually making execution impossible.

Mitigation: Cap paramIds.length or use a mapping-based lookup by (target, selector).

M-3

Permissionless enrollment

OPEN

UBCDistributor.sol

enrollCitizen() can be called by anyone for any verified citizen. A griefer could enroll citizens who don't want UBC, consuming capacity slots or adding them to the waitlist without consent.

Mitigation: Restrict to msg.sender == citizen or add an opt-in mechanism.

M-4

Stake lock only tracks last proposal

OPEN

QuadraticVoting.sol

lockedForProposal stores only the single most recent proposal the user voted on. If a citizen votes on proposal A (ends day 30) then votes on proposal B (ends day 10), after day 10 they can unstake even though proposal A is still active.

Mitigation: Track all active locks or use the latest-ending proposal.

M-5

Distribute loop with external calls

OPEN

SupportClasses.sol

distribute() makes safeTransfer calls inside a loop over recipients. If any recipient is a contract that reverts on receive, the entire distribution fails (one bad recipient blocks all).

Mitigation: Use a pull-based pattern or try/catch around individual transfers.

M-6

Oracle staleness affects deposit pricing

OPEN

OneWayBridge.sol

If oracle reports expire (> MAX_PRICE_AGE), getBasketPriceUSD() reverts, completely blocking deposits rather than gracefully degrading. Coordinated oracle downtime could freeze the bridge.

Mitigation: Consider a cached fallback price with an age warning.

M-7

Votes not recorded on-chain for tally

OPEN

ConvictionWeighting.sol

voteWithConviction() emits events but does not update any on-chain vote tally. The effective votes are only in events, making the contract informational only.

Mitigation: Wire ConvictionWeighting to Governor.castVote or implement its own tally.

M-8

netSupplyChange() overflow risk

ACKNOWLEDGED

UCUToken.sol

int256(totalMinted) - int256(totalBurned) will revert if totalMinted exceeds type(int256).max (~5.7e76). Practically unreachable but theoretically possible for an uncapped supply token.

Mitigation: Add a safe cast or document the theoretical limit.

M-9

sqrt overflow for large inputs

FIXED

FixedPointMath.sol

x * WAD in sqrt() overflows for x > type(uint256).max / 1e18 (~1.15e59). While token amounts shouldn't reach this, the function is a public library.

Fix: Added explicit overflow check with require.

Low Severity

LOW7 findings
L-1

Birth rate period reset drift

OPEN

CitizenRegistry.sol

_enforceBirthRate() sets currentPeriodStart = block.timestamp instead of incrementing by agentBirthPeriod. Over time, periods drift forward if called late.

Mitigation: Use currentPeriodStart += agentBirthPeriod to prevent drift.

L-2

Waitlist array grows forever

OPEN

UBCDistributor.sol

The waitlist array only grows via push() and is never compacted. waitlistHead advances but old entries remain. Over years, this wastes storage.

Mitigation: Periodically compact or use a linked-list pattern.

L-3

Largest recipient tracking is imprecise

ACKNOWLEDGED

SovereigntyIndex.sol

If a citizen transfers to A (100 UCU) then to B (150 UCU), largestRecipient updates to B. But if A later receives another 200 UCU (total 300), the contract misses that A surpassed B.

Mitigation: Maintain a proper max-heap or accept the approximation.

L-4

Daily rate limit uses block.timestamp / 1 days

ACKNOWLEDGED

OneWayBridge.sol

Miners can slightly manipulate timestamps. A deposit near midnight could be placed into a favorable day window. Impact is minimal (max ~15 seconds of manipulation).

Mitigation: Acceptable for current design.

L-5

No upper bound on proposal targets

OPEN

Governor.sol

A proposal can have hundreds of targets/calldatas, consuming excessive gas on execution.

Mitigation: Add a MAX_ACTIONS_PER_PROPOSAL constant.

L-6

Grant proposals never expire

OPEN

CreatorGrantOracle.sol

Once submitted, a ProposalData lives forever. Stale proposals from years ago can still be verified and approved by oracles.

Mitigation: Add an expiry timestamp to proposals.

L-7

Supporter refund on expiry triggered by anyone

ACKNOWLEDGED

SupportClasses.sol

When distribute() is called on an expired class, the refund goes to sc.supporter. A third party could front-run the supporter's own call. Not harmful but creates unexpected behavior.

Mitigation: Document this as expected behavior.

Pre-Mainnet Recommendations

  1. Wire Governor to QuadraticVoting/ConvictionWeighting for on-chain vote weight verification
  2. Add flash loan protection with minimum stake duration before voting power accrues
  3. Restrict proposal creation to citizens with minimum stake
  4. Add MAX_ACTIONS_PER_PROPOSAL to prevent gas DoS on execution
  5. Implement pull-based distribution for SupportClasses
  6. Deploy with multisig/DAO as admin for all admin-controlled contracts
  7. Formal verification of FixedPointMath library (ln/exp/log10)
  8. Professional external audit before handling real value