Platform: Code4rena
Start Date: 07/08/2023
Pot Size: $36,500 USDC
Total HM: 11
Participants: 125
Period: 3 days
Judge: alcueca
Total Solo HM: 4
Id: 274
League: ETH
Rank: 24/125
Findings: 3
Award: $186.84
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: thekmj
Also found by: 0xCiphky, 0xDetermination, 0xbrett8571, Eeyore, Team_Rocket, Tripathi, bart1e, deadrxsezzz, immeas, ltyu, mert_eren, pep7siup, popular00
129.1035 USDC - $129.10
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L127-L132 https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L213 https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L241
If governance removes a gauge for any (non-malicious) reason, a user's voting power for that gauge will be completely lost.
The GaugeController
is a solidity port of Curve DAO's Vyper implementation. Users are to vote for channeling incentives by using the vote_for_gauge_weights()
function, and each user can fraction their voting power by $10000$ (that is, defined by BPS).
One modification from the original is that governance can now remove gauges, not allowing users to vote on it. However, any existing individual user's voting power before removal is not reset. Since vote_for_gauge_weights()
does not allow voting for removed gauges, the voting power is then forever lost.
Consider the following scenario:
It is worth noting that, even if Alice does not use 100% of her voting power on that particular gauge, she would still lose whatever percent vote placed in that pool, and her overall voting power was weakened by said percent.
Users can lose their voting power.
We provide the following POC to use on GaugeController
tests.
function testPOC() public { // prepare uint256 v = 10 ether; vm.deal(gov, v); vm.startPrank(gov); ve.createLock{value: v}(v); // add gauges gc.add_gauge(gauge1); gc.change_gauge_weight(gauge1, 100); gc.add_gauge(gauge2); gc.change_gauge_weight(gauge2, 100); // all-in on gauge1 gc.vote_for_gauge_weights(gauge1, 10000); // governance removes gauge1 gc.remove_gauge(gauge1); // cannot vote for gauge2 vm.expectRevert("Used too much power"); gc.vote_for_gauge_weights(gauge2, 10000); // cannot remove vote for gauge1 vm.expectRevert("Invalid gauge address"); // @audit remove when mitigate gc.vote_for_gauge_weights(gauge1, 0); // cannot vote for gauge2 (to demonstrate again that voting power is not removed) vm.expectRevert("Used too much power"); // @audit remove when mitigate gc.vote_for_gauge_weights(gauge2, 10000); }
Forge, manual review
The simplest way to mitigate this is to allow zero-weight votings on expired pools simply to remove the vote. Modify line 213 as follow:
require(_user_weight == 0 || isValidGauge[_gauge_addr], "Can only vote 0 on non-gauges");
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L213
The given POC can then be the test case to verify successful mitigation.
As a QA-based recommendation, the sponsor can also provide an external function to remove votes, and/or provide a function to vote for various pools in the same tx. This will allow users to channel their votes directly from removed pools to ongoing pools.
Invalid Validation
#0 - c4-pre-sort
2023-08-12T04:39:18Z
141345 marked the issue as primary issue
#1 - c4-sponsor
2023-08-16T12:38:20Z
OpenCoreCH marked the issue as sponsor confirmed
#2 - OpenCoreCH
2023-08-21T11:47:09Z
#3 - c4-judge
2023-08-25T11:09:48Z
alcueca marked the issue as selected for report
#4 - alcueca
2023-08-25T22:43:15Z
Cycling to Medium and then High to make all duplicates High
#5 - c4-judge
2023-08-25T22:43:24Z
alcueca changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-08-25T22:43:38Z
alcueca changed the severity to 3 (High Risk)
#7 - JeffCX
2023-08-28T20:18:42Z
I respect judge's expertise, but I think this is a centralization risk which fits well in analysis report or QA
so I politely but strongly dispute that the severity is QA instead of high severity for this finding
the discussion related to admin related privilege issue is in this thread
https://github.com/code-423n4/org/issues/59
and this comments summarize it well
https://github.com/code-423n4/org/issues/59#issuecomment-1694644749
I did bring it up recently on backstage discord
https://discord.com/channels/810916927919620096/976603323450941440/1145319460014657566
and
the last also suggests that such centralizatoin fits for analysis report
https://github.com/code-423n4/org/issues/59#issuecomment-1694650340
#8 - alcueca
2023-08-28T21:16:03Z
I don't consider this submission to be related to the centralization risks that have been moved to the analysis
submissions.
In analysis
we accept centralization risks that are inherent to the application design. If there would be a specific function for the governor to take away the votes of a user that would belong in analysis
. Wardens could argue whether such a function is necessary and whether there is an alternative to fulfill whatever its purpose that couldn't be abused by permissioned accounts.
In this case, permanently taking away votes from users is not intended. It is an unexpected side effect of removing a gauge that the sponsor didn't intend to implement. As such, it is a valid issue.
The submission correctly specifies that governance doesn't even need to be malicious. If undetected, this submission would have eventually led to lost voting power the first time that a gauge is removed.
#9 - JeffCX
2023-08-29T11:35:28Z
then the severity medium seems approciate
if the admin governance intentionally remove guage to make user lose voting power it is considered as a centralization risk
if the admin remove guage and concerning that user lose voting power, governance can call add_guage to add the guage back any time
also I want to highlight
https://github.com/code-423n4/2023-08-verwa/tree/main#automated-findings--publicly-known-issues
Mistakes by Governance: We assume that all calls that are performed by the governance address are performed with the correct parameters
🌟 Selected for report: RED-LOTUS-REACH
Also found by: 0x3b, 0x4non, 0xCiphky, 0xDING99YA, 0xDetermination, 0xE1, 0xG0P1, 0xStalin, 0xWaitress, 0xbrett8571, 0xhacksmithh, 0xkazim, 0xmuxyz, 0xweb3boy, 14si2o_Flint, AlexCzm, Alhakista, Bube, Bughunter101, Deekshith99, Eeyore, Giorgio, HChang26, InAllHonesty, JP_Courses, KmanOfficial, MatricksDeCoder, Mike_Bello90, MrPotatoMagic, Naubit, QiuhaoLi, RHaO-sec, Raihan, Rolezn, SUPERMAN_I4G, Shubham, Silverskrrrt, Strausses, T1MOH, Topmark, Tripathi, Watermelon, _eperezok, aakansha, auditsea, audityourcontracts, ayden, carlos__alegre, castle_chain, cducrest, ch0bu, d23e, deadrxsezzz, deth, devival, erebus, fatherOfBlocks, halden, hassan-truscova, hpsb, hunter_w3b, imkapadia, immeas, jat, kaden, kaveyjoe, klau5, koxuan, kutugu, ladboy233, lanrebayode77, leasowillow, lsaudit, markus_ether, matrix_0wl, merlin, nemveer, ni8mare, nonseodion, oakcobalt, owadez, p_crypt0, pipidu83, piyushshukla, popular00, ppetrov, rjs, sandy, sl1, supervrijdag, tay054, thekmj, wahedtalash77, windhustler, zhaojie
9.8204 USDC - $9.82
The original veCRV implementation allows users to lock CRV tokens in exchange for voting power. Each CRV token locked for the maximum duration will yield one unit of voting power, and said voting power decays with time. The same idea is applied here, but with CANTO.
However, while veCRV allows users to specify the lock duration, veRWA does not, and only allows unlocking after 5 years of inactivity. This removes a certain degree of flexibility in the economy game, and may discourage users from locking their tokens, due to the high risk nature of smart contracts and cryptocurrency market.
VotingEscrow
.There is no such file IVotingEscrow
present in the repository.
One should not be relying on FIAT DAO's original IVotingEscrow
as the documented source of truth, as the contract veRWA has undergone changes. Several behaviors are important to document for the user to be aware of. For example, increaseAmount
also resets their lock to 5 years, or delegate
actually does not reset the lock (contrary to the statement at line 18).
Because changes in voting are equivalent to changes in incentives, it is recommended to emit event for any gauge-related state changes. It will be beneficial to whoever user looking to monitor incentives and its historical data.
We recommend emitting events for vote_for_gauge_weights()
whenever a user makes a vote, and _change_gauge_weight
for whenever governance changes a pool weight.
Affected functions:
msg.value
It is enough to use msg.value
itself as the lock value. There is no need to require it as a function parameter.
https://github.com/code-423n4/2023-08-verwa/blob/main/src/VotingEscrow.sol#L25-L28
In VotingEscrow
, the voting token metadata are set to storage.
However, it is known that they will be the native CANTO token, with known immutable metadata.
We suggest setting all of them to a constant, or to immutables. This also has the virtue of gas-saving by reducing storage reads whenever the data are used in a transaction (e.g. decimals).
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L212
In the function vote_for_gauge_weights()
, there is an assertion for user voting weight being in range $[0, 10000]$.
require(_user_weight >= 0 && _user_weight <= 10_000, "Invalid user weight");
However, if the user attempts to input more than $10000$, then the call will revert anyway from the following assertion.
require(power_used >= 0 && power_used <= 10_000, "Used too much power");
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L241C9-L241C81
In the test file GaugeController.t.sol
, user1
is often used as a gauge instead of gauge1
. Using these two variables interchangeably may lead to confusions, especially in future integrations. Thus it is not recommended.
Currently, to edit a vote for a gauge, a user must send one transaction. This means if the user wants to divide their votes across gauges (e.g. if they have multiple positions in multiple pools), then they'd have to send multiple transactions.
We suggest exposing a function for users to update their vote for multiple pools. Validate that the user's total vote after the transaction is less than $10000$ is enough.
#0 - c4-judge
2023-08-22T14:19:44Z
alcueca marked the issue as grade-a
🌟 Selected for report: catellatech
Also found by: 0x73696d616f, 0xSmartContract, 0xbrett8571, MrPotatoMagic, RED-LOTUS-REACH, rjs, thekmj
The project implements a veToken voting-escrow model, similar to veCRV. Users are able to lock their CANTO tokens in exchange for voting power. Users can then vote to channel incentives to different pools, e.g. for pools they have positions in.
The project is a fork of various protocols, with changes.
VotingEscrow
: Fork of FIAT DAO's implementation.GaugeController
: Solidity implementation of Curve's Vyper counterpart.LendingLedger
: For distributing rewards. Is not a fork.For the forked contracts, the major changes are as follow:
VotingEscrow
now uses native token, instead of ERC20 token.GaugeController
: Governance is able to remove gauges.
For the forked contracts, we focus on the changes made, and its implications on existing variables of the contract. Thus we focus mostly on the functionalities and algorithmic aspects. We do not focus on the mathematical specifics, as the math workload are mostly forked, and has already been battle-tested.
For LendingLedger
however, we do focus on the specifics. However, it is still more algorithmic, rather than mathematical, in nature.
We identified and have submitted the following issues and recommendations within the codebase:
No. | Title | Context |
---|---|---|
[H-01] | If governance removes a gauge, user's voting power for that gauge will be lost | |
[M-01] | There is no functionality to extend one's own lock duration | |
[L-01] | Hardcoding lock duration to 5 years may discourage users from locking | 1 |
[L-02] | Documentation do not properly reflect the changes from the original VotingEscrow | 8 |
[L-03] | Events should be emitted for important events | 2 |
[N-01] | Redundant equivalence check for msg.value | 2 |
[N-02] | Voting token metadata should either be all hardcoded or all immutable | 1 |
[N-03] | Redundant limit check for voting power | 1 |
[N-04] | Incorrect variable addresses on testing | |
[R-01] | Recommend allowing users to vote for multiple gauges in one transaction |
There are two contracts holding funds:
VotingEscrow
: Holds locked CANTOLendingLedger
: Holds rewards.In both contracts, we identified no functions for the admin to withdraw funds to themselves.
However, governance is able to override gauge weights using the function GaugeController.change_gauge_weight()
. This is technically a possible path to manipulate rewards distribution.
Governance also has the responsibility to correctly distribute rewards for every epoch (every week), as well as ensuring enough CANTO is locked in the reward Ledger.
External outgoing calls happen only when sending CANTO to the users, either through withdraw()
or claim()
. Both functions either have re-entry guards in place, or follows the CEI pattern.
However, external incoming call also happens in sync_ledger()
. While only whitelisted markets can call this function, it can still affect rewards distribution. It is then the team's responsibility to diligently verify the market was correctly implemented before whitelisting.
LendingLedger
currently only supports rewarding in CANTO/native token. We suggest integrating ERC20 tokens into the mix, as this will enable external partnered protocols to provide incentives, which may boost the growth of the CANTO ecosystem.
16 hours
#0 - c4-judge
2023-08-22T07:12:43Z
alcueca marked the issue as grade-b