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: 17/96
Findings: 2
Award: $324.23
🌟 Selected for report: 0
🚀 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
https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L653-L661 https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L585-L592
The recoverERC20
function allows the owner to withdraw the ERC20 tokens sent by acceident to the contract but it doesn't allow him to withdraw pldged tokens, the owner though could use the removeRewardToken
function to remove a token used currently in a pledge and then he can use recoverERC20
function to drain all it's balance, and thus the owner could drain any existing pledge balance with this technique.
The recoverERC20
function below contains a check to prevent the owner from withdrawing tokens used in a pledge :
function recoverERC20(address token) external onlyOwner returns(bool) { // @audit check to prevent the withdrawal of pledge tokens 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; }
But using the removeRewardToken
function the owner can eliminate the check and thus he will be able to withdraw all the balance of the pledge token.
function removeRewardToken(address token) external onlyOwner { if(token == address(0)) revert Errors.ZeroAddress(); if(minAmountRewardToken[token] == 0) revert Errors.NotAllowedToken(); // @audit this will invalidate the check in recoverERC20 minAmountRewardToken[token] = 0; emit RemoveRewardToken(token); }
Manual review
To avoid this issue the the contract must use more than just minAmountRewardToken
to track pledged tokens, i recommend to use a Token struct containing the minAmountRewardToken and a boolean variable to check if the token is used in a pledge or not, this boolean value should be checked before removing any token.
#0 - Kogaroshi
2022-10-31T00:52:58Z
Duplicate of #17
#1 - c4-judge
2022-11-09T21:40:24Z
kirk-baird marked the issue as satisfactory
#2 - c4-judge
2022-11-10T06:39:12Z
kirk-baird marked the issue as not a duplicate
#3 - c4-judge
2022-11-10T06:39:19Z
kirk-baird marked the issue as duplicate
#4 - c4-judge
2022-12-06T17:32:42Z
Simon-Busch marked the issue as duplicate of #68
314.3177 USDC - $314.32
https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L387-L388 https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L432-L433
When the pledge creator tries to extend his pledge or to increase the rewardPerVote
of the pledge the old votesDifference
set when creating the pledge is used to calculate the totalRewardAmount
and the feeAmount
, this will force the creator to pay a higher fee to the contract even though the votesDifference
of the pledge has changed.
In the extendPledge
and increasePledgeRewardPerVote
functions the initial votes difference pledgeParams.votesDifference
is used to calculate the fee amount as shown in the code below :
uint256 totalRewardAmount = (pledgeParams.rewardPerVote * pledgeParams.votesDifference * addedDuration) / UNIT; uint256 feeAmount = (totalRewardAmount * protocalFeeRatio) / MAX_PCT ;
uint256 totalRewardAmount = (rewardPerVoteDiff * pledgeParams.votesDifference * remainingDuration) / UNIT; uint256 feeAmount = (totalRewardAmount * protocalFeeRatio) / MAX_PCT ;
The scenario where the pledge creator will be forced to pay a higher fee without reason is the following :
The creator sets the initial votes target equal to 1000 for example (suppose that the creator has 0 votes initially).
At the time the creator decides to extend the pledge (or increase the rewerPerToken) he has already collected 800 votes, so only 200 votes are needed to achieve the target meaning that votesDifference == 200
.
When the contract tries to calculate the totalRewardAmount
and the feeAmount
it will use the votesDifference == 1000
instead votesDifference == 200
and so the creator will pay more in feeAmount but he will not be able to get more than 1000 votes because of the check :
if(delegationBoost.adjusted_balance_of(pledgeParams.receiver) + amount > pledgeParams.targetVotes) revert Errors.TargetVotesOverflow();
Manual review
To avoid this issue both the extendPledge
and increasePledgeRewardPerVote
functions should calculate and update the actual votes difference using the code below :.
pledgeParams.votesDifference = targetVotes - votingEscrow.balanceOf(receiver);
In this way the creator will pay a fee only for the remaining votes in the pledge.
#0 - trust1995
2022-10-30T20:48:23Z
Dup of #234
#1 - Kogaroshi
2022-10-31T00:30:58Z
Duplicate of #163
#2 - Kogaroshi
2022-10-31T00:32:07Z
Also contested on the part based on the received votes, as stated in other issues, the delegated vote should not be counted in those cases, because if the delegation ends before the Pledge ends, then we have a period with missing rewards for users that will try to pledge()
#3 - c4-judge
2022-11-10T23:05:51Z
kirk-baird marked the issue as satisfactory
#4 - c4-judge
2022-11-10T23:05:57Z
kirk-baird marked the issue as not a duplicate
#5 - c4-judge
2022-11-10T23:06:10Z
kirk-baird marked the issue as duplicate of #163