code-423n4/2024-07-loopfi-validation

Lack of Vote Decay in GaugeV3 Contract

Closed this issue · 1 comments

Lines of code

https://github.com/code-423n4/2024-07-loopfi/blob/57871f64bdea450c1f04c9a53dc1a78223719164/src/quotas/GaugeV3.sol#L152-L158

Vulnerability details

Impact

GaugeV3 contract allows for long-term rate manipulation through a single large vote. This exploit can lead to unfair distribution of rewards, potential economic attacks, and compromise the integrity of the voting system.

Proof of Concept

The issue allows Bob to cast an extremely large vote that overwhelms all other votes and persists indefinitely.

function _vote

	if (lpSide) {
		qp.totalVotesLpSide += votes; // U:[GA-12]
		uv.votesLpSide += votes; // U:[GA-12]
	} else {
		qp.totalVotesCaSide += votes; // U:[GA-12]
		uv.votesCaSide += votes; // U:[GA-12]
	}

Attack Senario:

  1. Bob identifies a token in the GaugeV3 contract.
  2. Bob casts a large vote (100,000,000) for this token on the LP side.
  3. This vote immediately pushes the rate to the maximum allowed value (100).
  4. Alice, a normal user, attempts to counteract with a significant but much smaller vote (1,000,000) on the CA side.
  5. Alice's vote has negligible impact, barely moving the rate from 100.
  6. 30 days pass, simulating multiple epochs.
  7. The system updates the epoch, but the rate remains unchanged at 100.
  8. The manipulated rate persists, demonstrating the long-term impact of Bob's single large vote.

Please add this code to CDPVault.t.sol.

function testRateManipulationCaseA() public {
    console.log("Starting rate manipulation test");

    CDPVault vault = createCDPVault(token, 150 ether, 0, 1.25 ether, 1.0 ether, 0);
    createGaugeAndSetGauge(address(vault));

    // create position
    token.mint(address(this), 200 ether);
    token.approve(address(vault), 200 ether);

    address attacker = address(0x1);
    address normalUser = address(0x2);

    // Initial state
    address[] memory tokens = new address[](1);
    tokens[0] = address(token);
    uint16[] memory initialRates = gauge.getRates(tokens);
    console.log("Initial rate:");
    console.logUint(initialRates[0]);

    // Attacker casts a large vote
    uint96 largeVote = 100000000; // type(uint96).max;
    bytes memory voteData = abi.encode(address(token), true); // vote for LP side
    vm.prank(address(voter));
    gauge.vote(attacker, largeVote, voteData);

    // Check rates after attacker's vote
    uint16[] memory ratesAfterAttack = gauge.getRates(tokens);
    console.log("Rate after attacker's vote:");
    console.logUint(ratesAfterAttack[0]);

    // Normal user tries to counteract
    uint96 normalVote = 1000000; // A significant but much smaller vote
    voteData = abi.encode(address(token), false); // vote for CA side
    vm.prank(address(voter));
    gauge.vote(normalUser, normalVote, voteData);

    // Check rates after normal user's vote
    uint16[] memory ratesAfterNormalVote = gauge.getRates(tokens);
    console.log("Rate after normal user's vote:");
    console.logUint(ratesAfterNormalVote[0]);

    // Simulate time passing (multiple epochs)
    vm.warp(block.timestamp + 30 days);
    gauge.updateEpoch();

    // Check rates after time has passed
    uint16[] memory ratesAfterTime = gauge.getRates(tokens);
    console.log("Rate after time has passed:");
    console.logUint(ratesAfterTime[0]);

    // Assertions to confirm the vulnerability
    assertEq(ratesAfterAttack[0], 100, "Rate should be at maximum after attacker's vote");
    assertNotEq(ratesAfterNormalVote[0], ratesAfterAttack[0], "Normal user's vote should have an impact");
    assertEq(ratesAfterTime[0], ratesAfterNormalVote[0], "Rate should remain unchanged after time passes");

    console.log("Rate manipulation test completed");
}

Output

[PASS] testRateManipulationCaseA() (gas: 3508736)
Logs:
  Starting rate manipulation test
  Initial rate:
  10
  Rate after attacker's vote:
  100
  Rate after normal user's vote:
  99
  Rate after time has passed:
  99
  Rate manipulation test completed

Case B:

The vote is not brought to the middle exactly, even though both users voted with the same amount.

Please add this code to CDPVault.t.sol.

function testRateManipulationCaseB() public {

    CDPVault vault = createCDPVault(token, 150 ether, 0, 1.25 ether, 1.0 ether, 0);
    createGaugeAndSetGauge(address(vault));

    // create position
    token.mint(address(this), 200 ether);
    token.approve(address(vault), 200 ether);

    address attacker = address(0x1);
    address normalUser = address(0x2);

    // Initial state
    address[] memory tokens = new address[](1);
    tokens[0] = address(token);
    uint16[] memory initialRates = gauge.getRates(tokens);
    console.log("Initial rate:");
    console.logUint(initialRates[0]);

   

    // Normal user tries to vote
    uint96 normalVote = 1000000; // A significant but much smaller vote
    bytes memory  voteData = abi.encode(address(token), false); // vote for CA side
    vm.prank(address(voter));
    gauge.vote(normalUser, normalVote, voteData);

    // Check rates after normal user's vote
    uint16[] memory ratesAfterNormalVote = gauge.getRates(tokens);
    console.log("Rate after first vote:");
    console.logUint(ratesAfterNormalVote[0]);

// another user
 uint96 largeVote = 1000000; 
  voteData = abi.encode(address(token), true); // vote for LP side
 vm.prank(address(voter));
 gauge.vote(attacker, largeVote, voteData);

 // Check rates after user's vote
 uint16[] memory ratesAfterAttack = gauge.getRates(tokens);
 console.log("Rate after second vote:");
 console.logUint(ratesAfterAttack[0]);


    // Simulate time passing (multiple epochs)
    vm.warp(block.timestamp + 30 days);
    gauge.updateEpoch();

    // Check rates after time has passed
    uint16[] memory ratesAfterTime = gauge.getRates(tokens);
    console.log("Rate after time has passed:");
    console.logUint(ratesAfterTime[0]);

}

Output

Logs:
  Initial rate:
  10
  Rate after first vote:
  10
  Rate after second vote:
  55
  Rate after time has passed:
  55

Tools Used

Manual Review

Recommended Mitigation Steps

Implement a vote decay mechanism where votes lose influence over time. This can be achieved by introducing a decay factor that reduces vote power based on the time elapsed since the vote was cast. Additionally, consider implementing a maximum vote power limit per user to prevent any single voter from having an overly dominant influence on the rates.

Assessed type

Other

evokid commented

Hey @koolexcrypto , thank you for the judging efforts. i think this issue was missed in the validation repo, there is a
POC already provided, you can test it. thank you.