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: 27/125
Findings: 3
Award: $151.46
π Selected for report: 0
π 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
99.3104 USDC - $99.31
When a gauge is removed through the remove_gauge
function, you adjust the gauge weight to 0
without checking if there are active users who have already voted for this gauge. This might lead to unexpected results if users have already voted for the gauge before it's removed.
function remove_gauge(address _gauge) external onlyGovernance { require(isValidGauge[_gauge], "Invalid gauge address"); isValidGauge[_gauge] = false; _change_gauge_weight(_gauge, 0); // This sets gauge weight to 0 emit GaugeRemoved(_gauge); }
The impact of this vulnerability can be severe:
Active users who have voted for the gauge being removed will lose their voting power associated with that gauge. This loss of incentives could lead to dissatisfaction among users who have actively participated in the system.
The lack of proper redistribution of voting power can lead to unexpected results in the governance and incentivization mechanisms of the contract. Users may receive fewer rewards than expected, disrupting the intended balance of the system.
Once the gauge is removed, it might become difficult to recover lost incentives and properly reallocate voting power to affected users, as the contract may not have mechanisms to retroactively address this issue.
Illustration of the potential issue with the remove_gauge
function and the lack of consideration for users who have already voted for the gauge:
pragma solidity ^0.8.0; contract GaugeController { mapping(address => bool) public isValidGauge; mapping(address => mapping(address => uint256)) public vote_user_power; function add_gauge(address _gauge) external { isValidGauge[_gauge] = true; } function remove_gauge(address _gauge) external { require(isValidGauge[_gauge], "Invalid gauge address"); isValidGauge[_gauge] = false; // No logic to handle users who have voted for this gauge } function vote_for_gauge_weights(address _gauge_addr, uint256 _user_weight) external { require(_user_weight >= 0 && _user_weight <= 10_000, "Invalid user weight"); require(isValidGauge[_gauge_addr], "Invalid gauge address"); // Assume slope_ is calculated correctly // Calculate power_used correctly // Record user's vote vote_user_power[msg.sender][_gauge_addr] = _user_weight; } } contract User { GaugeController public gaugeController; constructor(address _gaugeController) { gaugeController = GaugeController(_gaugeController); } function voteForGauge(address _gauge, uint256 _weight) external { gaugeController.vote_for_gauge_weights(_gauge, _weight); } } contract Main { GaugeController public gaugeController; User public user; constructor() { gaugeController = new GaugeController(); user = new User(address(gaugeController)); } function simulateScenario() external { address gaugeA = address(0x123); address gaugeB = address(0x456); // Add gauges gaugeController.add_gauge(gaugeA); gaugeController.add_gauge(gaugeB); // User votes for Gauge A user.voteForGauge(gaugeA, 5000); // User votes for Gauge B user.voteForGauge(gaugeB, 3000); // Remove Gauge A without handling user votes gaugeController.remove_gauge(gaugeA); // Now, user's voting power for Gauge A is lost and not redistributed // This could lead to unexpected behavior and loss of incentives } }
The simulateScenario
function illustrates the issue. The user votes for both Gauge A and Gauge B. However, when Gauge A is removed through the remove_gauge
function, there is no logic to handle the user's voting power for Gauge A. As a result, the user's voting power for Gauge A becomes lost or not properly redistributed, which could lead to unexpected behavior and loss of incentives.
VSCODE
The logic error could be fixed by adding a check to the remove_gauge function to see if there are any active users who have already voted for the gauge. If there are any active users who have voted for the gauge, the function should not remove the gauge and should instead emit a warning to the users that their votes will no longer be counted.
function remove_gauge(address _gauge) external onlyGovernance { require(isValidGauge[_gauge], "Invalid gauge address"); isValidGauge[_gauge] = false; bool hasVoters = false; for (uint256 i; i < voters[_gauge].length; ++i) { if (voters[_gauge][i].active) { hasVoters = true; break; } } if (hasVoters) { emit GaugeRemovedWarning(_gauge); return; } _change_gauge_weight(_gauge, 0); emit GaugeRemoved(_gauge); }
First checks to see if there are any active voters for the gauge. If there are no active voters, the function proceeds to remove the gauge and emit a "GaugeRemoved" event. If there are active voters, the function emits a "GaugeRemovedWarning" event and does not remove the gauge.
This would prevent the gauge from being removed if there are active users who have already voted for it. It would also warn the users that their votes will no longer be counted.
Governance
#0 - c4-pre-sort
2023-08-12T08:39:16Z
141345 marked the issue as duplicate of #62
#1 - c4-judge
2023-08-25T11:11:05Z
alcueca marked the issue as satisfactory
#2 - c4-judge
2023-08-25T22:43:22Z
alcueca changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-08-25T22:43:36Z
alcueca changed the severity to 3 (High Risk)
π 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
4.2289 USDC - $4.23
https://github.com/code-423n4/2023-08-verwa/blob/9a2e7be003bc1a77b3b87db31f3d5a1bcb48ed32/src/GaugeController.sol#L225 https://github.com/code-423n4/2023-08-verwa/blob/9a2e7be003bc1a77b3b87db31f3d5a1bcb48ed32/src/GaugeController.sol#L211
Lack of a buffer in the lock duration checking condition can lead to transaction failures for users whose lock duration is very close to the current epoch. This can result in a poor user experience and unnecessary gas expenses for failed transactions.
The condition require(lock_end > next_time, "Lock expires too soon"); in the vote_for_gauge_weights() function ensures that the user's lock doesn't expire too soon. However, you might want to consider adding a buffer to this condition to prevent users from submitting transactions that could fail due to small timing discrepancies.
The lock duration checking occurs at GaugeController.sol#L225
require(lock_end > next_time, "Lock expires too soon");
However, as mentioned, there is no buffer added to account for potential timing discrepancies, which could lead to failed transactions due to small variations in block timestamps.
Here's a scenario to illustrate the potential issue with the lock duration checking condition:
vote_for_gauge_weights()
function.vote_for_gauge_weights()
function checks if Alice's lock duration (specified by lock_end
) is greater than the current epoch (represented by next_time
).lock_end
is exactly equal to the next_time
(no buffer added).Issue:
Due to the lack of buffer in the lock duration checking condition, even if Alice's lock end is just a fraction of a second after the next_time
, the condition lock_end > next_time
would evaluate to false. This could lead to Alice's transaction failing with the error message "Lock expires too soon," even though her lock duration is effectively greater than the current epoch.
Blockchain transactions are processed in discrete blocks, and each block has a timestamp. If Alice's transaction is included in a block with a timestamp that falls after the next_time
, even by a very small margin, the condition would evaluate to false, causing the transaction to fail. This is because the condition only checks if the lock end is strictly greater than the next_time
.
VSCODE
Consider adding a small buffer to the condition to account for timing discrepancies. For example, the condition can be modified to lock_end >= next_time + BUFFER
, where BUFFER
is a small duration (e.g., a few seconds) added to the next_time
. This would provide a safety margin and ensure that users like Alice won't face transaction failures due to minor timing differences.
require(lock_end >= next_time + BUFFER, "Lock expires too soon");
Here, BUFFER
represents a small duration (e.g., a few seconds) added to the next_time
.
Solmate
#0 - 141345
2023-08-12T08:35:29Z
gas and user experience improvement
QA might be more appropriate.
#1 - c4-sponsor
2023-08-16T14:29:41Z
OpenCoreCH marked the issue as disagree with severity
#2 - c4-sponsor
2023-08-16T14:29:46Z
OpenCoreCH marked the issue as sponsor acknowledged
#3 - OpenCoreCH
2023-08-16T14:31:14Z
UX improvement, but not even sure if it would improve the UX. I think it is pretty easy to communicate that the lock must be longer than the epoch end. If we start introducing some time buffer, we have the same problem at the buffer boundaries and it would still be possible that the end timestamp is just a few seconds too late
#4 - alcueca
2023-08-24T06:17:07Z
Valid QA that users need to take responsibility for timing discrepancies themselves when specifying a lock, so that their transactions don't revert unexpectedly.
#5 - c4-judge
2023-08-24T06:17:16Z
alcueca changed the severity to QA (Quality Assurance)
#6 - c4-judge
2023-08-24T06:17:24Z
alcueca marked the issue as grade-b
π Selected for report: catellatech
Also found by: 0x73696d616f, 0xSmartContract, 0xbrett8571, MrPotatoMagic, RED-LOTUS-REACH, rjs, thekmj
The veRWA, Incentivization Primitive for Real World Assets on Canto is a well-designed and implemented system for incentivizing liquidity providers and voters on the Canto network. The system is based on a voting-escrow model, where users can lock up their CANTO tokens for five years in exchange for veCANTO tokens. veCANTO tokens can then be used to vote for lending markets, which are rewarded with CANTO tokens based on the amount of votes they receive. Users who provide liquidity to lending markets can also claim CANTO tokens based on their share of the liquidity in the market.
The veRWA system has a number of strengths, including:
The veRWA system also has a few weaknesses, including:
Overall, the veRWA system is a well-designed and implemented system for incentivizing liquidity providers and voters on the Canto network. The system has a number of strengths, but it also has a few weaknesses. It will be important to monitor the system closely in the future to ensure that it continues to operate as intended.
Here are some additional comments for the judge to contextualize my findings:
22 hours
#0 - c4-judge
2023-08-22T07:15:10Z
alcueca marked the issue as grade-b