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: 8/96
Findings: 4
Award: $1,192.69
π Selected for report: 1
π Solo Findings: 0
π Selected for report: ladboy233
Also found by: 0x52, 0xDecorativePineapple, 0xhunter, Aymen0909, Bnke0x0, Dravee, JTJabba, Jeiwan, Lambda, Nyx, Picodes, Trust, cccz, cryptonue, csanuragjain, dic0de, hansfriese, horsefacts, imare, minhtrng, pashov, peritoflores, rbserver, rvierdiiev, wagmi, yixxas
9.9073 USDC - $9.91
Owner can steal pending pledge rewards at any time. Even though there's an attempt to restrict this owner's ability, it can be easily bypassed.
The recoverERC20
function allows owner to withdraw ERC20 tokens mistakenly sent to the contract (WardenPledge.sol#L653):
function recoverERC20(address token) external onlyOwner returns(bool) { if(minAmountRewardToken[token] != 0) revert Errors.CannotRecoverToken(); uint256 amount = IERC20(token).balanceOf(address(this)); if(amount == 0) revert Errors.NullValue(); IERC20(token).safeTransfer(owner(), amount); return true; }
Since pledge rewards are ERC20 tokens sent to the contract (WardenPledge.sol#L333), owner can withdraw them at any time. This check can be easily bypassed by owner:
if(minAmountRewardToken[token] != 0) revert Errors.CannotRecoverToken();
Because minAmountRewardToken
is controlled by owner (WardenPledge.sol#L570-L592):
function removeRewardToken(address token) external onlyOwner { if(token == address(0)) revert Errors.ZeroAddress(); if(minAmountRewardToken[token] == 0) revert Errors.NotAllowedToken(); minAmountRewardToken[token] = 0; emit RemoveRewardToken(token); }
Owner calls removeRewardToken
for each of the currently supported reward tokens, which will set minAmountRewardToken
to 0 for them. Owner calls recoverERC20
for each of the tokens and removes all pending pledge reward from the contract.
Manual review
Consider disallowing owner transferring any ERC20 tokens out of the contract. Alternatively, in the recoverERC20
function, ensure no open pledge has remaining reward in the token being recovered.
#0 - Kogaroshi
2022-10-31T00:15:12Z
Duplicate of #17
#1 - c4-judge
2022-11-10T07:11:28Z
kirk-baird marked the issue as not a duplicate
#2 - c4-judge
2022-11-10T07:11:36Z
kirk-baird marked the issue as duplicate
#3 - c4-judge
2022-11-10T21:21:01Z
kirk-baird marked the issue as satisfactory
#4 - c4-judge
2022-11-10T21:21:06Z
kirk-baird marked the issue as not a duplicate
#5 - c4-judge
2022-11-10T21:21:11Z
kirk-baird marked the issue as duplicate of #17
#6 - c4-judge
2022-12-06T17:32:42Z
Simon-Busch marked the issue as duplicate of #68
754.535 USDC - $754.53
A pledge target can be fully reached only when a pledge is made every block, which is not realistic on Ethereum. Every pledge will have some amount of leftover reward tokens and target vote amounts won't be fully maintained during desired durations.
A pledge target consists of two parameters:
Thus, the total reward of a pledge is calculated as reward per token * number of votes desired * duration
(WardenPledge.sol#L327):
vars.votesDifference = targetVotes - votingEscrow.balanceOf(receiver); vars.totalRewardAmount = (rewardPerVote * vars.votesDifference * vars.duration) / UNIT;
Thus, pledge creator expects to have a desired amount of votes maintained during the entire duration. However, it's almost not possible to maintain a specific number of votes during a specific duration for two reasons:
Thus, a pledge target can only be reached at the current timestamp, but, in the next block, the amount of delegated votes will decrease and a new pledge will be required to fill the gap.
Manual review
The only reasonable workaround for this problem seems to be setting a higher target, which means overpaying. And it still doesn't allow to maintain a desired amount of votes during a desired duration.
#0 - Kogaroshi
2022-10-31T00:26:22Z
Duplicate of #91
#1 - c4-judge
2022-11-10T22:53:40Z
kirk-baird marked the issue as satisfactory
#2 - c4-judge
2022-11-10T22:53:46Z
kirk-baird marked the issue as not a duplicate
#3 - c4-judge
2022-11-10T22:53:58Z
kirk-baird marked the issue as duplicate of #91
408.613 USDC - $408.61
https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L387 https://github.com/code-423n4/2022-10-paladin/blob/d6d0c0e57ad80f15e9691086c9c7270d4ccfe0e6/contracts/WardenPledge.sol#L432
Total reward amount in extendPledge
and increasePledgeRewardPerVote
can be calculated incorrectly due to cached pledgeParams.votesDifference
, which can lead to two outcomes:
When a pledge is created, the creator chooses the targetβthe total amount of votes they want to reach with the pledge. Based on a target, the number of missing votes is calculated, which is then used to calculated the total reward amount (WardenPledge.sol#L325-L327):
function createPledge( address receiver, address rewardToken, uint256 targetVotes, uint256 rewardPerVote, // reward/veToken/second uint256 endTimestamp, uint256 maxTotalRewardAmount, uint256 maxFeeAmount ) external whenNotPaused nonReentrant returns(uint256){ ... // 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; ... }
When extending a pledge or increasing a pledge reward per vote, current veToken balance of the pledge's receiver (votingEscrow.balanceOf(receiver)
) can be different from the one it had when the pledge was created (e.g. the receiver managed to lock more CRV or some of locked tokens have expired). However pledgeParams.votesDifference
is not recalculated (WardenPledge.sol#L387, WardenPledge.sol#L432):
function extendPledge( uint256 pledgeId, uint256 newEndTimestamp, uint256 maxTotalRewardAmount, uint256 maxFeeAmount ) external whenNotPaused nonReentrant { ... Pledge storage pledgeParams = pledges[pledgeId]; ... uint256 totalRewardAmount = (pledgeParams.rewardPerVote * pledgeParams.votesDifference * addedDuration) / UNIT; ... } function increasePledgeRewardPerVote( uint256 pledgeId, uint256 newRewardPerVote, uint256 maxTotalRewardAmount, uint256 maxFeeAmount ) external whenNotPaused nonReentrant { ... Pledge storage pledgeParams = pledges[pledgeId]; ... uint256 totalRewardAmount = (rewardPerVoteDiff * pledgeParams.votesDifference * remainingDuration) / UNIT; ... }
This can lead to two consequences:
votesDifference
got in fact smaller), pledge creator will overpay for pledge extension and pledge reward per vote increase. This extra reward cannot be received by pledgers because a receiver cannot get more votes than pledgeParams.targetVotes
(which is not updated when modifying a pledge):
function _pledge(uint256 pledgeId, address user, uint256 amount, uint256 endTimestamp) internal { ... // Check that this will not go over the Pledge target of votes if(delegationBoost.adjusted_balance_of(pledgeParams.receiver) + amount > pledgeParams.targetVotes) revert Errors.TargetVotesOverflow(); ... }
votesDifference
got in fact bigger), the pledge target cannot be reached because the reward amount was underpaid in extendPledge
/increasePledgeRewardPerVote
.Manual review
Consider updating votesDifference
when extending a pledge or increasing a pledge reward per vote.
#0 - Kogaroshi
2022-11-02T23:42:10Z
As stated in #91, new method for needed votes & needed reward calculations is introduced in this commit, allowing to get the exact amount of reward token the Pledge creator should pay when extending the Pledge or increasing the rewardPerVote
#1 - c4-judge
2022-11-10T22:57:28Z
kirk-baird marked the issue as primary issue
#2 - c4-judge
2022-11-10T23:04:49Z
kirk-baird marked the issue as selected for report
#3 - c4-judge
2022-11-10T23:08:40Z
kirk-baird marked the issue as satisfactory
π 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
Pledged
event is emitted with a wrong amount of delegated votesThe actual amount of delegated votes is bias
, not amount
(WardenPledge.sol#L255-L263):
uint256 slope = amount / boostDuration; uint256 bias = slope * boostDuration; // 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;
Manual review
Consider using bias
instead of amount
.
#0 - c4-judge
2022-11-12T00:36:21Z
kirk-baird marked the issue as grade-b