veRWA - thekmj'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: 24/125

Findings: 3

Award: $186.84

QA:
grade-a
Analysis:
grade-b

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

Labels

bug
3 (High Risk)
primary issue
selected for report
sponsor confirmed
upgraded by judge
H-08

Awards

129.1035 USDC - $129.10

External Links

Lines of code

https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L127-L132 https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L213 https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L241

Vulnerability details

Summary

If governance removes a gauge for any (non-malicious) reason, a user's voting power for that gauge will be completely lost.

Vulnerability details

The GaugeController is a solidity port of Curve DAO's Vyper implementation. Users are to vote for channeling incentives by using the vote_for_gauge_weights() function, and each user can fraction their voting power by $10000$ (that is, defined by BPS).

One modification from the original is that governance can now remove gauges, not allowing users to vote on it. However, any existing individual user's voting power before removal is not reset. Since vote_for_gauge_weights() does not allow voting for removed gauges, the voting power is then forever lost.

Consider the following scenario:

  • Alice has some veRWA, and is now able to vote.
  • She votes on some pool, say, G1, using 100% of her voting power.
  • Pool G1 is removed by governance due to any reason. Perhaps the pool was found to be faulty and liquidity should be migrated, perhaps the market itself has became illiquid and unsafe, perhaps the intended incentives duration for that pool has simply expired.
  • Alice still has 100% of her voting power in that pool, but she cannot remove her vote and claim the voting power back.

It is worth noting that, even if Alice does not use 100% of her voting power on that particular gauge, she would still lose whatever percent vote placed in that pool, and her overall voting power was weakened by said percent.

Impact

Users can lose their voting power.

Proof of concept

We provide the following POC to use on GaugeController tests.

function testPOC() public {
    // prepare
    uint256 v = 10 ether;
    vm.deal(gov, v);
    vm.startPrank(gov);
    ve.createLock{value: v}(v);

    // add gauges
    gc.add_gauge(gauge1);
    gc.change_gauge_weight(gauge1, 100);
    gc.add_gauge(gauge2);
    gc.change_gauge_weight(gauge2, 100);

    // all-in on gauge1
    gc.vote_for_gauge_weights(gauge1, 10000);

    // governance removes gauge1
    gc.remove_gauge(gauge1);

    // cannot vote for gauge2
    vm.expectRevert("Used too much power");
    gc.vote_for_gauge_weights(gauge2, 10000);

    // cannot remove vote for gauge1
    vm.expectRevert("Invalid gauge address"); // @audit remove when mitigate
    gc.vote_for_gauge_weights(gauge1, 0);

    // cannot vote for gauge2 (to demonstrate again that voting power is not removed)
    vm.expectRevert("Used too much power");  // @audit remove when mitigate
    gc.vote_for_gauge_weights(gauge2, 10000);
}

Tools used

Forge, manual review

The simplest way to mitigate this is to allow zero-weight votings on expired pools simply to remove the vote. Modify line 213 as follow:

require(_user_weight == 0 || isValidGauge[_gauge_addr], "Can only vote 0 on non-gauges");

https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L213

The given POC can then be the test case to verify successful mitigation.

As a QA-based recommendation, the sponsor can also provide an external function to remove votes, and/or provide a function to vote for various pools in the same tx. This will allow users to channel their votes directly from removed pools to ongoing pools.

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-08-12T04:39:18Z

141345 marked the issue as primary issue

#1 - c4-sponsor

2023-08-16T12:38:20Z

OpenCoreCH marked the issue as sponsor confirmed

#2 - OpenCoreCH

2023-08-21T11:47:09Z

#3 - c4-judge

2023-08-25T11:09:48Z

alcueca marked the issue as selected for report

#4 - alcueca

2023-08-25T22:43:15Z

Cycling to Medium and then High to make all duplicates High

#5 - c4-judge

2023-08-25T22:43:24Z

alcueca changed the severity to 2 (Med Risk)

#6 - c4-judge

2023-08-25T22:43:38Z

alcueca changed the severity to 3 (High Risk)

#7 - JeffCX

2023-08-28T20:18:42Z

I respect judge's expertise, but I think this is a centralization risk which fits well in analysis report or QA

so I politely but strongly dispute that the severity is QA instead of high severity for this finding

the discussion related to admin related privilege issue is in this thread

https://github.com/code-423n4/org/issues/59

and this comments summarize it well

https://github.com/code-423n4/org/issues/59#issuecomment-1694644749

I did bring it up recently on backstage discord

https://discord.com/channels/810916927919620096/976603323450941440/1145319460014657566

and

the last also suggests that such centralizatoin fits for analysis report

https://github.com/code-423n4/org/issues/59#issuecomment-1694650340

#8 - alcueca

2023-08-28T21:16:03Z

I don't consider this submission to be related to the centralization risks that have been moved to the analysis submissions.

In analysis we accept centralization risks that are inherent to the application design. If there would be a specific function for the governor to take away the votes of a user that would belong in analysis. Wardens could argue whether such a function is necessary and whether there is an alternative to fulfill whatever its purpose that couldn't be abused by permissioned accounts.

In this case, permanently taking away votes from users is not intended. It is an unexpected side effect of removing a gauge that the sponsor didn't intend to implement. As such, it is a valid issue.

The submission correctly specifies that governance doesn't even need to be malicious. If undetected, this submission would have eventually led to lost voting power the first time that a gauge is removed.

#9 - JeffCX

2023-08-29T11:35:28Z

then the severity medium seems approciate

if the admin governance intentionally remove guage to make user lose voting power it is considered as a centralization risk

if the admin remove guage and concerning that user lose voting power, governance can call add_guage to add the guage back any time

also I want to highlight

https://github.com/code-423n4/2023-08-verwa/tree/main#automated-findings--publicly-known-issues

Mistakes by Governance: We assume that all calls that are performed by the governance address are performed with the correct parameters

[L-01] Hardcoding lock duration to 5 years may discourage users from locking.

The original veCRV implementation allows users to lock CRV tokens in exchange for voting power. Each CRV token locked for the maximum duration will yield one unit of voting power, and said voting power decays with time. The same idea is applied here, but with CANTO.

However, while veCRV allows users to specify the lock duration, veRWA does not, and only allows unlocking after 5 years of inactivity. This removes a certain degree of flexibility in the economy game, and may discourage users from locking their tokens, due to the high risk nature of smart contracts and cryptocurrency market.

[L-02] Documentation do not properly reflect the changes from the original VotingEscrow.

There is no such file IVotingEscrow present in the repository.

One should not be relying on FIAT DAO's original IVotingEscrow as the documented source of truth, as the contract veRWA has undergone changes. Several behaviors are important to document for the user to be aware of. For example, increaseAmount also resets their lock to 5 years, or delegate actually does not reset the lock (contrary to the statement at line 18).

[L-03] Events should be emitted for important events

Because changes in voting are equivalent to changes in incentives, it is recommended to emit event for any gauge-related state changes. It will be beneficial to whoever user looking to monitor incentives and its historical data.

We recommend emitting events for vote_for_gauge_weights() whenever a user makes a vote, and _change_gauge_weight for whenever governance changes a pool weight.

Affected functions:

[N-01] Redundant equivalence check for msg.value

It is enough to use msg.value itself as the lock value. There is no need to require it as a function parameter.

[N-02] Voting token metadata should either be all hardcoded or all immutable

https://github.com/code-423n4/2023-08-verwa/blob/main/src/VotingEscrow.sol#L25-L28

In VotingEscrow, the voting token metadata are set to storage.

However, it is known that they will be the native CANTO token, with known immutable metadata.

We suggest setting all of them to a constant, or to immutables. This also has the virtue of gas-saving by reducing storage reads whenever the data are used in a transaction (e.g. decimals).

[N-03] Redundant limit check for voting power

https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L212

In the function vote_for_gauge_weights(), there is an assertion for user voting weight being in range $[0, 10000]$.

require(_user_weight >= 0 && _user_weight <= 10_000, "Invalid user weight");

However, if the user attempts to input more than $10000$, then the call will revert anyway from the following assertion.

require(power_used >= 0 && power_used <= 10_000, "Used too much power");

https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L241C9-L241C81

[N-04] Incorrect variable addresses on testing

In the test file GaugeController.t.sol, user1 is often used as a gauge instead of gauge1. Using these two variables interchangeably may lead to confusions, especially in future integrations. Thus it is not recommended.

[R-01] Recommend allowing users to vote for multiple gauges in one transaction

Currently, to edit a vote for a gauge, a user must send one transaction. This means if the user wants to divide their votes across gauges (e.g. if they have multiple positions in multiple pools), then they'd have to send multiple transactions.

We suggest exposing a function for users to update their vote for multiple pools. Validate that the user's total vote after the transaction is less than $10000$ is enough.

#0 - c4-judge

2023-08-22T14:19:44Z

alcueca marked the issue as grade-a

Findings Information

Labels

analysis-advanced
grade-b
A-07

Awards

47.9153 USDC - $47.92

External Links

Summary of the project

The project implements a veToken voting-escrow model, similar to veCRV. Users are able to lock their CANTO tokens in exchange for voting power. Users can then vote to channel incentives to different pools, e.g. for pools they have positions in.

The project is a fork of various protocols, with changes.

  • VotingEscrow: Fork of FIAT DAO's implementation.
  • GaugeController: Solidity implementation of Curve's Vyper counterpart.
  • LendingLedger: For distributing rewards. Is not a fork.

For the forked contracts, the major changes are as follow:

  • VotingEscrow now uses native token, instead of ERC20 token.
  • In FIAT DAO's Voting Escrow, a user is able to specify their lock duration (not exceeding MAX and ending at valid timestamps). In veRWA, the lock duration is fixed at 5 years (maximum possible).
  • GaugeController: Governance is able to remove gauges.
    • Additionally, governance does not have to specify gauge weight upon addition.

Approach taken in evaluating the codebase

For the forked contracts, we focus on the changes made, and its implications on existing variables of the contract. Thus we focus mostly on the functionalities and algorithmic aspects. We do not focus on the mathematical specifics, as the math workload are mostly forked, and has already been battle-tested.

For LendingLedger however, we do focus on the specifics. However, it is still more algorithmic, rather than mathematical, in nature.

We identified and have submitted the following issues and recommendations within the codebase:

No.TitleContext
[H-01]If governance removes a gauge, user's voting power for that gauge will be lost
[M-01]There is no functionality to extend one's own lock duration
[L-01]Hardcoding lock duration to 5 years may discourage users from locking1
[L-02]Documentation do not properly reflect the changes from the original VotingEscrow8
[L-03]Events should be emitted for important events2
[N-01]Redundant equivalence check for msg.value2
[N-02]Voting token metadata should either be all hardcoded or all immutable1
[N-03]Redundant limit check for voting power1
[N-04]Incorrect variable addresses on testing
[R-01]Recommend allowing users to vote for multiple gauges in one transaction

Centralization risks

There are two contracts holding funds:

  • VotingEscrow: Holds locked CANTO
  • LendingLedger: Holds rewards.

In both contracts, we identified no functions for the admin to withdraw funds to themselves.

However, governance is able to override gauge weights using the function GaugeController.change_gauge_weight(). This is technically a possible path to manipulate rewards distribution.

Governance also has the responsibility to correctly distribute rewards for every epoch (every week), as well as ensuring enough CANTO is locked in the reward Ledger.

Systemic risks

External outgoing calls happen only when sending CANTO to the users, either through withdraw() or claim(). Both functions either have re-entry guards in place, or follows the CEI pattern.

However, external incoming call also happens in sync_ledger(). While only whitelisted markets can call this function, it can still affect rewards distribution. It is then the team's responsibility to diligently verify the market was correctly implemented before whitelisting.

Architecture recommendations

LendingLedger currently only supports rewarding in CANTO/native token. We suggest integrating ERC20 tokens into the mix, as this will enable external partnered protocols to provide incentives, which may boost the growth of the CANTO ecosystem.

Time spent:

16 hours

#0 - c4-judge

2023-08-22T07:12:43Z

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