
Loading...
Loading
Loading...
LoadingLoading audit report...

WalletGuard.ai, powered by Gestalt Labs
Forge fork-validation ran but no findings met the threshold for PoC inclusion. See the per-finding "Forge validated" badges in the report below for individual results.
The analyzed contracts implement a Curve-style gauge voting and lending reward distribution system consisting of three core contracts: VotingEscrow (vote-escrowed CANTO locking), GaugeController (gauge weight voting), and LendingLedger (reward distribution to lending markets). The analysis identified 1 critical, 5 high, 8 medium, 5 low, and 2 informational findings across 25 total findings from 9 specialist agents. The single most dangerous pattern is a fundamental epoch unit mismatch between LendingLedger (block-number-based epochs) and GaugeController (timestamp-based epochs), which renders the entire reward distribution mechanism non-functional and, if patched incorrectly, could enable reward manipulation. Combined with missing reentrancy protection on LendingLedger.claim() and the absence of vote cooldown enforcement, the protocol faces systemic risks to both reward integrity and fund safety. The overall risk level is high; the contracts should not be deployed in their current state without addressing the critical epoch mismatch and multiple high-severity logic errors.
The LendingLedger contract passes block numbers (like 100,000 or 200,000) to the GaugeController as if they were timestamps (like 1,700,000,000). The GaugeController interprets these as dates from January 1970, returning zero for all gauge weights. This means no CANTO rewards are ever distributed to any lender, regardless of how users vote. The entire reward system is completely non-functional.
Users are supposed to be locked into their gauge weight votes for 10 days to prevent manipulation. However, the cooldown is never actually checked; it is only recorded. This means a user can vote, then immediately re-vote with different weights in the same transaction. An attacker can rapidly shift all their voting power between gauges to maximize rewards for gauges they control, undermining the fairness of the entire reward distribution system.
The claim() function sends native CANTO to users before fully securing all related state. While the direct re-entry into claim() for the same market is neutralized, a malicious whitelisted lending market can exploit the callback during the ETH transfer to inflate the user's balance via sync_ledger(), then trigger another claim for additional rewards. Since there is no reentrancy guard on claim(), the contract's entire CANTO balance could be drained by a compromised or malicious whitelisted market.
4 centralization points identified
Governance transfer is a privileged operation with no two-step confirmation, representing a centralization and operational risk.
setGovernance()Only governance can call setRewards, and the DoS only affects the governance transaction itself. This is an operational risk for the admin role.
setRewards()Governance-only function that can retroactively set rewards. This is an admin capability that should be documented as a trust assumption.
setRewards()Governance can delist a market, permanently locking user rewards. This is a centralization risk where governance action directly harms users.
whiteListLendingMarket()A malicious whitelisted lending market exploits the missing reentrancy guard on LendingLedger.claim(). Attack flow: (1) Attacker deploys a contract that gets whitelisted by governance. (2) The attacker's market calls sync_ledger(attacker, large_positive_delta) to inflate the attacker's user.amount. (3) Attacker calls claim(market) which sends ETH. (4) In the receive() fallback, the attacker's market calls sync_ledger again to further inflate user.amount. (5) The attacker calls claim() again from the fallback. Since rewardDebt was set based on the old (smaller) amount, the new accumulatedCanto based on the inflated amount exceeds rewardDebt, extracting additional CANTO. (6) Repeat until the contract is drained. Finding 1 (reentrancy in claim) combines with Finding 3/10 (sync_ledger trust model) to create a fund drain.
An attacker exploits the missing vote cooldown (Finding 2) to rapidly allocate maximum weight to a gauge, then governance removes that gauge (Finding 16). The removal zeroes out changes_weight for the gauge but does not clear user votes. When the attacker later calls vote_for_gauge_weights with weight 0 on the removed gauge to 'free' their power, the old slope changes in changes_sum are double-subtracted (once by _remove_gauge_weight, once by the vote update). This permanently corrupts the total weight sum, inflating relative weights for all remaining gauges, causing disproportionate reward distribution that benefits the attacker's other gauge positions.
The epoch mismatch (Finding 21) means cantoReward is always 0, so accCantoPerShare never increases. If governance eventually fixes the epoch system (e.g., via contract upgrade), rewards begin accruing. If governance then delists a market (Finding 22) while users have accrued rewards, those rewards become permanently inaccessible because claim() calls update_market() which requires the whitelist check. The combination of the epoch fix enabling actual reward accrual followed by delisting creates a larger pool of permanently locked funds than either issue alone.
If governance is set to address(0) (Finding 17), no new gauges can be added and no gauges can be removed. If _remove_gauge_weight encounters an underflow condition (Finding 7/15) before governance is lost, gauges become permanently unremovable. Combined, the protocol enters a state where the gauge set is permanently frozen with potentially corrupted weights, and no administrative action can recover the system.
| Agent | Status | Findings | Severity | Confidence | Duration | Coverage |
|---|---|---|---|---|---|---|
| reentrancy | success | 8 | 70% | 1.7m | Cross-function reentrancy in LendingLedger.claim() with sync_ledger(), VotingEscrow reentrancy via withdraw() ETH transfer, GaugeController vote_for_gauge_weights formula correctness and Math.max misuse, Missing cooldown in vote_for_gauge_weights, Epoch unit mismatch between LendingLedger (block-based) and GaugeController (time-based), Access control on sync_ledger and claim, Integer overflow/underflow in _remove_gauge_weight, CEI pattern analysis across all ETH-transferring functions, VotingEscrow delegation logic in increaseAmount, GaugeController add_gauge/remove_gauge governance controls, LendingLedger setRewards and whiteListLendingMarket admin functions, ERC-777/ERC-1155 callback patterns (not applicable - no ERC20 tokens used, native ETH only), Flash loan callback patterns, Signature replay (no signatures used in these contracts), Read-only reentrancy in gauge_relative_weight view functions | |
| access control | success | 9 | 72% | 1.8m | GaugeController access control (onlyGovernance modifier), LendingLedger access control (onlyGovernance, whitelisted market checks), VotingEscrow lock management and withdrawal logic, Single-step governance transfer in both GaugeController and LendingLedger, Reentrancy in LendingLedger.claim() ETH transfer, sync_ledger trust model and potential for whitelisted market abuse, vote_for_gauge_weights cooldown enforcement (last_user_vote never read), Signature auth - no signatures used, not applicable, Delegatecall usage - none present, Initializer protection - no upgradeable proxies, Flash loan governance capture - VotingEscrow uses locked balances not spot balances, Gauge removal and re-addition consistency, Block vs timestamp epoch alignment mismatch between LendingLedger and GaugeController, Integer overflow/underflow in int256 arithmetic for reward debt, setRewards loop gas limit vulnerability, VotingEscrow delegated increaseAmount checkpoint correctness, Historical weight corruption after gauge removal | |
| economic | success | 9 | 70% | 2.0m | Flash loan attack vectors on VotingEscrow (native CANTO, no flash loan risk), Oracle manipulation (no price feeds used, not applicable), Governance capture via flash loans (VotingEscrow uses native CANTO deposits, not ERC20 balance), ERC-4626 inflation attack (not applicable, no ERC-4626 vault), Reentrancy in LendingLedger.claim() ETH transfer, Reentrancy in VotingEscrow.withdraw() ETH transfer (protected by nonReentrant), vote_for_gauge_weights logic including bias calculation with Math.max, Missing cooldown check in vote_for_gauge_weights, sync_ledger access control and arbitrary amount crediting, update_market arithmetic overflow potential, Delegation logic correctness in VotingEscrow.increaseAmount, time_weight update logic in _get_weight and _get_sum, governance address zero-address risk, Integer overflow in accCantoPerShare uint128 storage, Chainlink/Pyth oracle usage (none present), Sandwich/MEV exposure (no swaps or liquidations), Reward manipulation via gauge weight gaming, _remove_gauge_weight correctness for slope change cancellation, Turnstile interface (external registration, no security impact), VotingEscrow createLock/withdraw flow including checkpoint correctness, LendingLedger setRewards epoch alignment checks | |
| logic validation | success | 10 | 72% | 1.7m | GaugeController vote_for_gauge_weights bias and slope accounting, GaugeController gauge weight sum invariant maintenance, GaugeController cooldown enforcement, LendingLedger update_market epoch boundary calculations, LendingLedger uint128 overflow in accCantoPerShare, LendingLedger claim reentrancy via ETH send, LendingLedger sync_ledger access control (msg.sender as lending market), VotingEscrow createLock/increaseAmount/withdraw math, VotingEscrow checkpoint slope and bias correctness, VotingEscrow delegation logic, Governance address zero-address protection, EIP-712 signature patterns (not applicable - no signatures), Cross-contract integration between GaugeController and LendingLedger, Flash loan attack vectors on gauge weight manipulation, Loop bounds and gas DoS vectors | |
| code quality | success | 11 | 73% | 1.8m | GaugeController voting logic and power accounting, LendingLedger reward calculation and accumulator overflow, VotingEscrow lock/unlock/delegation mechanics, Access control on governance functions, uint128/uint64 downcast safety, Reentrancy in claim() and withdraw(), Cross-contract call patterns between LendingLedger and GaugeController, Signature replay (EIP-712) - not applicable, no signatures, Flash loan attack vectors on voting, Off-by-one in loop bounds, Missing validation in add_gauge/remove_gauge, Vote cooldown enforcement, Power accounting after gauge removal | |
| compiler bugs | success | 6 | 65% | 1.5m | GaugeController.vote_for_gauge_weights - bias/slope update arithmetic, GaugeController._get_sum and _get_weight - historical weight filling, GaugeController._change_gauge_weight and _remove_gauge_weight - weight management, LendingLedger.update_market - epoch calculation and gauge weight query, LendingLedger.sync_ledger - access control and delta handling, LendingLedger.claim - reentrancy and reward calculation, LendingLedger.setRewards - epoch validation, VotingEscrow.createLock, increaseAmount, withdraw - lock management and ETH handling, VotingEscrow._checkpoint - global and user point history, Cross-contract calls between LendingLedger and GaugeController (epoch mismatch), Integer overflow/underflow in Solidity 0.8.x (auto-checked), Signature replay patterns (none present - no EIP-712), Flash loan attack vectors on gauge weight manipulation, Governance centralization risks | |
| assembly safety | success | 8 | 73% | 1.6m | Full source code scan for non-ASCII characters (codepoint-by-codepoint) - no non-ASCII found, Assembly blocks - none present in the source, Reentrancy patterns in LendingLedger.claim() and VotingEscrow.withdraw(), Access control on governance functions in GaugeController and LendingLedger, EIP-712 signature patterns - none used (no off-chain signatures), Integer overflow/underflow in reward calculations, Epoch alignment between LendingLedger (block-based) and GaugeController (time-based), Vote manipulation via missing cooldown in vote_for_gauge_weights, Gauge weight arithmetic in _change_gauge_weight and _remove_gauge_weight, VotingEscrow checkpoint logic for delegated and non-delegated locks, Flash loan attack vectors on governance weight manipulation, sync_ledger trust model for whitelisted markets, Slope change accounting in VotingEscrow._checkpoint, Known vulnerability patterns from the checklist (signature replay, public function griefing, strategy migration) | |
| l2 specific | success | 11 | 73% | 2.3m | GaugeController vote_for_gauge_weights power accounting and slope manipulation, GaugeController _change_gauge_weight and _remove_gauge_weight arithmetic safety, GaugeController _get_sum and _get_weight checkpoint loops, GaugeController gauge removal and re-vote interaction, LendingLedger claim reentrancy and CEI pattern, LendingLedger update_market gas costs and epoch loop, LendingLedger sync_ledger access control and parameter validation, LendingLedger whitelist removal impact on user funds, LendingLedger setRewards epoch validation, LendingLedger uint128 overflow in accCantoPerShare, VotingEscrow createLock, increaseAmount, withdraw reentrancy (protected by nonReentrant), VotingEscrow _checkpoint slope and bias calculation correctness, VotingEscrow delegation mechanics, Cross-contract interaction between LendingLedger and GaugeController, Block epoch vs time epoch alignment between LendingLedger and GaugeController, Signature replay patterns (no signatures used in this protocol), L2-specific patterns (no L2 imports or predeploy addresses detected in code), Turnstile interface usage (only interface defined, not called from main contracts) | |
| math verification | success | 8 | 73% | 2.0m | GaugeController vote_for_gauge_weights formula correctness (Math.max pattern), GaugeController vote cooldown enforcement, GaugeController _change_gauge_weight and _remove_gauge_weight underflow scenarios, GaugeController _get_sum and _get_weight checkpoint logic, LendingLedger update_market epoch unit mismatch (blocks vs timestamps), LendingLedger precision in accCantoPerShare and secRewardsPerShare calculations, LendingLedger claim() reentrancy (CEI pattern - rewardDebt is updated before send), LendingLedger sync_ledger access control (only whitelisted markets via update_market check), LendingLedger cantoPerBlock epoch alignment, VotingEscrow createLock, increaseAmount, withdraw flows, VotingEscrow _checkpoint slope and bias calculations, VotingEscrow delegation mechanics in increaseAmount, VotingEscrow withdraw reentrancy (nonReentrant guard present), Integer overflow/underflow in Solidity 0.8+ (built-in protection), Access control on governance functions (onlyGovernance modifier), EIP-712 signature patterns (not present in this contract) |
This automated audit has inherent limitations. The following areas are not covered.
This report is an automated point-in-time assessment and does not guarantee protection against all possible attacks. It does not cover off-chain components, economic modeling, or business logic correctness unless explicitly noted. Changes to the contract after the audit commit are not reviewed. This is not financial or legal advice. WalletGuard, powered by Gestalt Labs, provides this analysis as-is with no warranty of completeness.
[](https://walletguard.ai/audit/8165c8ef-3a30-4257-aa9d-4c32494daa42)
<a href="https://walletguard.ai/audit/8165c8ef-3a30-4257-aa9d-4c32494daa42"> <img src="https://walletguard.ai/api/badge/8165c8ef-3a30-4257-aa9d-4c32494daa42" alt="WalletGuard Audit Badge" /> </a>