Platform: Code4rena
Start Date: 27/10/2022
Pot Size: $33,500 USDC
Total HM: 8
Participants: 96
Period: 3 days
Judge: kirk-baird
Total Solo HM: 1
Id: 176
League: ETH
Rank: 5/96
Findings: 3
Award: $1,314.86
π Selected for report: 1
π Solo Findings: 0
980.8955 USDC - $980.90
https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L325-L335 https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L259-L268
Pledge may be out of reward due to the decay in veCRV balance. The receiver may lose his reward given to boosters but get nothing in return since her targetVotes is never reached.
According to Curve documentation at https://curve.readthedocs.io/dao-vecrv.html
A userβs veCRV balance decays linearly as the remaining time until the CRV unlock decreases. For example, a balance of 4000 CRV locked for one year provides the same amount of veCRV as 2000 CRV locked for two years, or 1000 CRV locked for four years.
On creation, targetVotes = 100, balance = 20 -> votesDifference = 80 -> reward is allocated for 80 votes
// Get the missing votes for the given receiver to reach the target votes // We ignore any delegated boost here because they might expire during the Pledge duration // (we can have a future version of this contract using adjusted_balance) vars.votesDifference = targetVotes - votingEscrow.balanceOf(receiver); vars.totalRewardAmount = (rewardPerVote * vars.votesDifference * vars.duration) / UNIT; vars.feeAmount = (vars.totalRewardAmount * protocalFeeRatio) / MAX_PCT ; if(vars.totalRewardAmount > maxTotalRewardAmount) revert Errors.IncorrectMaxTotalRewardAmount(); if(vars.feeAmount > maxFeeAmount) revert Errors.IncorrectMaxFeeAmount(); // Pull all the rewards in this contract IERC20(rewardToken).safeTransferFrom(creator, address(this), vars.totalRewardAmount); // And transfer the fees from the Pledge creator to the Chest contract IERC20(rewardToken).safeTransferFrom(creator, chestAddress, vars.feeAmount);
Then 1 week passed, receiver's balance decay to 10
On creation, targetVotes = 100, balance = 10 but votesDifference stays 80, and reward has only allocated for 80 votes.
// Rewards are set in the Pledge as reward/veToken/sec // To find the total amount of veToken delegated through the whole Boost duration // based on the Boost bias & the Boost duration, to take in account that the delegated amount decreases // each second of the Boost duration uint256 totalDelegatedAmount = ((bias * boostDuration) + bias) / 2; // Then we can calculate the total amount of rewards for this Boost uint256 rewardAmount = (totalDelegatedAmount * pledgeParams.rewardPerVote) / UNIT; if(rewardAmount > pledgeAvailableRewardAmounts[pledgeId]) revert Errors.RewardsBalanceTooLow(); pledgeAvailableRewardAmounts[pledgeId] -= rewardAmount;
A booster boosts 80 votes and takes all rewards in the pool. However, only 80 (From booster) + 10 (From receiver) = 90 votes is active. Not 100 votes that receiver promise in the targetVotes.
Then, if another booster tries to boost 10 votes, it will be reverted with RewardsBalanceTooLow since the first booster has taken all reward that is allocated for only 80 votes.
You should provide a way for the creator to provide additional rewards after the pledge creation. Or provide some reward refreshment function that recalculates votesDifference and transfers the required additional reward.
#0 - Kogaroshi
2022-11-02T23:39:13Z
Changed the logic in PR 2, commit Now the whole amount of votes needed for each second of the Pledge duration is calculated, taking in account the receiver potential veCRV balance, and the veCRV decay.
This should allow to add only the exact amount of reward needed to the Pledge reward pool, and have always the correct amount of rewards to achieve the vote target of the Pledge at all times
#1 - Kogaroshi
2022-11-02T23:40:11Z
If possible, a feedback on the new calculation and logic would be appreciated.
#2 - c4-judge
2022-11-10T22:41:20Z
kirk-baird marked the issue as primary issue
#3 - c4-judge
2022-11-10T22:54:24Z
kirk-baird marked the issue as selected for report
314.3177 USDC - $314.32
https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L385-L396 https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L428-L440
extendPledge and increasePledgeRewardPerVote are wasting rewardToken from creator more than necessary. The creator may need to pay more rewards which are temporarily unnecessary locking her fund.
On creation, targetVotes = 100, balance = 20 -> votesDifference = 80 -> reward is allocated for 80 votes
A booster boosts 50 votes -> adjusted balance = 70 votes and the target is 100 votes (Remaining 30 votes)
uint256 addedDuration = newEndTimestamp - oldEndTimestamp; if(addedDuration < minDelegationTime) revert Errors.DurationTooShort(); uint256 totalRewardAmount = (pledgeParams.rewardPerVote * pledgeParams.votesDifference * addedDuration) / UNIT; uint256 feeAmount = (totalRewardAmount * protocalFeeRatio) / MAX_PCT ; if(totalRewardAmount > maxTotalRewardAmount) revert Errors.IncorrectMaxTotalRewardAmount(); if(feeAmount > maxFeeAmount) revert Errors.IncorrectMaxFeeAmount(); // Pull all the rewards in this contract IERC20(pledgeParams.rewardToken).safeTransferFrom(creator, address(this), totalRewardAmount); // And transfer the fees from the Pledge creator to the Chest contract IERC20(pledgeParams.rewardToken).safeTransferFrom(creator, chestAddress, feeAmount);
The creator extends the vote 2 times, he needs to pay a reward for pledgeParams.votesDifference = 80 votes. But actually, only 30 votes remain. Wasting reward for 50 votes.
uint256 oldRewardPerVote = pledgeParams.rewardPerVote; if(newRewardPerVote <= oldRewardPerVote) revert Errors.RewardsPerVotesTooLow(); uint256 remainingDuration = pledgeParams.endTimestamp - block.timestamp; uint256 rewardPerVoteDiff = newRewardPerVote - oldRewardPerVote; uint256 totalRewardAmount = (rewardPerVoteDiff * pledgeParams.votesDifference * remainingDuration) / UNIT; uint256 feeAmount = (totalRewardAmount * protocalFeeRatio) / MAX_PCT ; if(totalRewardAmount > maxTotalRewardAmount) revert Errors.IncorrectMaxTotalRewardAmount(); if(feeAmount > maxFeeAmount) revert Errors.IncorrectMaxFeeAmount(); // Pull all the rewards in this contract IERC20(pledgeParams.rewardToken).safeTransferFrom(creator, address(this), totalRewardAmount); // And transfer the fees from the Pledge creator to the Chest contract IERC20(pledgeParams.rewardToken).safeTransferFrom(creator, chestAddress, feeAmount);
The creator increases the reward per vote 2 times, he needs to pay a reward for pledgeParams.votesDifference = 80 votes. But actually, only 30 votes remain. Wasting reward for 50 votes.
Recalculate votesDifference after each adjustment using adjusted_balance_of
pledgeParams.votesDifference = targetVotes - delegationBoost.adjusted_balance_of(receiver); uint256 addedDuration = newEndTimestamp - oldEndTimestamp; if(addedDuration < minDelegationTime) revert Errors.DurationTooShort(); uint256 totalRewardAmount = (pledgeParams.rewardPerVote * pledgeParams.votesDifference * addedDuration) / UNIT; uint256 feeAmount = (totalRewardAmount * protocalFeeRatio) / MAX_PCT ; if(totalRewardAmount > maxTotalRewardAmount) revert Errors.IncorrectMaxTotalRewardAmount(); if(feeAmount > maxFeeAmount) revert Errors.IncorrectMaxFeeAmount(); // Pull all the rewards in this contract IERC20(pledgeParams.rewardToken).safeTransferFrom(creator, address(this), totalRewardAmount); // And transfer the fees from the Pledge creator to the Chest contract IERC20(pledgeParams.rewardToken).safeTransferFrom(creator, chestAddress, feeAmount);
pledgeParams.votesDifference = targetVotes - delegationBoost.adjusted_balance_of(receiver); uint256 oldRewardPerVote = pledgeParams.rewardPerVote; if(newRewardPerVote <= oldRewardPerVote) revert Errors.RewardsPerVotesTooLow(); uint256 remainingDuration = pledgeParams.endTimestamp - block.timestamp; uint256 rewardPerVoteDiff = newRewardPerVote - oldRewardPerVote; uint256 totalRewardAmount = (rewardPerVoteDiff * pledgeParams.votesDifference * remainingDuration) / UNIT; uint256 feeAmount = (totalRewardAmount * protocalFeeRatio) / MAX_PCT ; if(totalRewardAmount > maxTotalRewardAmount) revert Errors.IncorrectMaxTotalRewardAmount(); if(feeAmount > maxFeeAmount) revert Errors.IncorrectMaxFeeAmount(); // Pull all the rewards in this contract IERC20(pledgeParams.rewardToken).safeTransferFrom(creator, address(this), totalRewardAmount); // And transfer the fees from the Pledge creator to the Chest contract IERC20(pledgeParams.rewardToken).safeTransferFrom(creator, chestAddress, feeAmount);
#0 - Kogaroshi
2022-10-30T23:34:22Z
Duplicate of #61
#1 - Chomtana
2022-10-31T01:45:12Z
Also dup #163
#2 - kirk-baird
2022-11-10T21:56:28Z
Nicely written up and explained however as the sponsor mentions it may be required to use the full amount of tokens transferred if existing pledges end before the desired end timestamp. I'll use Nullify
so the warden is not penalised for this submission.
#3 - c4-judge
2022-11-10T21:56:32Z
kirk-baird marked the issue as nullified
#4 - c4-judge
2022-11-10T22:57:46Z
kirk-baird marked the issue as not nullified
#5 - kirk-baird
2022-11-10T23:00:47Z
I have decided this is actually a valid issue as cached voteDifference
needs to be updated for each call to extendPledge()
and increasePledgeRewards()
. It has two impacts.
a) Users are overpaying the fees
b) Excess funds locked (or not enough)
This is a separate issue to voting balance decaying
Thus I'm going to mark it as a duplicate of #163
#6 - c4-judge
2022-11-10T23:00:52Z
kirk-baird marked the issue as not a duplicate
#7 - c4-judge
2022-11-10T23:01:22Z
kirk-baird marked the issue as duplicate of #163
#8 - c4-judge
2022-11-10T23:01:36Z
kirk-baird marked the issue as satisfactory
#9 - c4-judge
2022-11-10T23:01:40Z
kirk-baird marked the issue as not a duplicate
#10 - c4-judge
2022-11-10T23:01:47Z
kirk-baird marked the issue as duplicate of #163
π Selected for report: robee
Also found by: 0x007, 0x1f8b, 0x52, 0xDjango, 0xNazgul, 0xSmartContract, 8olidity, Awesome, B2, Bnke0x0, Chom, Diana, Dravee, JTJabba, Jeiwan, Josiah, Lambda, Mathieu, Picodes, RaoulSchaffranek, RaymondFam, RedOneN, ReyAdmirado, Rolezn, Ruhum, Sm4rty, Tricko, Trust, Waze, __141345__, a12jmx, adriro, ajtra, brgltd, c3phas, carlitox477, cccz, ch0bu, chaduke, chrisdior4, corerouter, cryptonue, csanuragjain, ctf_sec, cylzxje, delfin454000, dic0de, djxploit, horsefacts, imare, jayphbee, jwood, ktg, ladboy233, leosathya, lukris02, minhtrng, neko_nyaa, oyc_109, pashov, peritoflores, rbserver, rvierdiiev, shark, tnevler, yixxas
19.6449 USDC - $19.64
Although you don't take it into account, the system still has some problems with votes decay / expire during the Pledge duration as stated in my Medium findings. So, you should take adjusted_balance_of into account to get the actual voting difference.
#0 - c4-judge
2022-11-12T00:09:24Z
kirk-baird marked the issue as grade-b