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: 32/96
Findings: 3
Award: $41.07
🌟 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
The recoverERC20
function allows the contract owner to transfer arbitrary ERC20 tokens owned by the WardenPledge
contract in order to recover tokens sent by mistake to the contract. In order to protect against withdrawal of deposited reward tokens, it includes a check that the withdrawn token is not currently an approved rewards token:
/** * @notice Recovers ERC2O tokens sent by mistake to the contract * @dev Recovers ERC2O tokens sent by mistake to the contract * @param token Address tof the EC2O token * @return bool: success */ 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; }
However, the contract owner also has the ability to remove rewards tokens, and can easily bypass the check in recoverERC20
by first calling removeRewardToken
to set minAmountRewardToken[token]
to zero, then calling recoverERC20
:
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); }
Impact: A malicious owner can steal all deposited rewards tokens.
Recommendation: Disallow removing rewards tokens associated with active pledges. This will probably require tracking this information in a separately stored data structure.
#0 - Kogaroshi
2022-10-31T00:50:48Z
Duplicate of #17
#1 - c4-judge
2022-11-09T21:26:55Z
kirk-baird marked the issue as satisfactory
#2 - c4-judge
2022-11-10T06:40:11Z
kirk-baird marked the issue as not a duplicate
#3 - c4-judge
2022-11-10T06:40:17Z
kirk-baird marked the issue as duplicate
#4 - c4-judge
2022-12-06T17:32:42Z
Simon-Busch marked the issue as duplicate of #68
🌟 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
The total amount required to fund a pledge is calculated at pledge creation time, then transferred to the WardenPledge
contract:
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);
The total reward amount is then stored, along with the reward rate inside the Pledge
struct:
// Add the total reards as available for the Pledge & write Pledge parameters in storage pledgeAvailableRewardAmounts[vars.newPledgeID] += vars.totalRewardAmount; pledges.push(Pledge( targetVotes, vars.votesDifference, rewardPerVote, rewardToken, safe64(endTimestamp), false, receiver ));
However, if a pledge is created with a fee-on-transfer token, an amount less than the full calculated reward amount will be transferred to the WardenPledge
contract when the pledge is created, but the full amount will be stored for reward calculations. The contract may show that there are sufficient rewards remaining for additional pledges, but revert when a pledger actually attempts to make the pledge due to an insufficient balance.
I consider this low risk, since all rewards tokens must be approved before use, and it is unlikely that rewards would be paid in a fee-on-transfer token, but ensure you qualify all rewards tokens before approval.
The constant WEEK
can be defined using time unit keywords, either 7 days
or 1 weeks
. (These keywords are used elsewhere in the contract).
uint256 public constant WEEK = 7 * 86400;
uint256 public constant WEEK = 1 weeks;
There is an incomplete comment on line 84 of WardenPledge
:
/** @notice Event emitted when xx */
NewPledge
parametersConsider indexing the address parameters of NewPledge
, to allow filtering events by creator
/receiver
/rewardToken
address:
event NewPledge( address creator, address receiver, address rewardToken, uint256 targetVotes, uint256 rewardPerVote, uint256 endTimestamp );
"Protocol fee ratio" is misspelled as "protocal fee ratio":
uint256 oldfee = protocalFeeRatio; protocalFeeRatio = newFee;
#0 - c4-judge
2022-11-12T01:04:42Z
kirk-baird marked the issue as grade-b
🌟 Selected for report: c3phas
Also found by: 0x1f8b, 0xNazgul, 0xRoxas, 0xSmartContract, 0xbepresent, Amithuddar, Awesome, B2, Bnke0x0, Dravee, KoKo, Mathieu, Picodes, RaymondFam, RedOneN, ReyAdmirado, RockingMiles, Ruhum, SadBase, SooYa, Waze, __141345__, adriro, ajtra, ballx, carlitox477, ch0bu, cylzxje, djxploit, durianSausage, emrekocak, erictee, gogo, halden, horsefacts, imare, indijanc, karanctf, leosathya, lukris02, neko_nyaa, oyc_109, peiw, sakman, shark, skyle, tnevler
11.5153 USDC - $11.52
Just passing along two small findings in this report.
The votingEscrow
and delegationBoost
state variables are set once at construction time and cannot change, so you can declare them immutable
:
/** @notice Address of the votingToken to delegate */ IVotingEscrow public votingEscrow; /** @notice Address of the Delegation Boost contract */ IBoostV2 public delegationBoost;
Suggestion:
/** @notice Address of the votingToken to delegate */ IVotingEscrow public immutable votingEscrow; /** @notice Address of the Delegation Boost contract */ IBoostV2 public immutable delegationBoost;
Since the if
statement on line 267 ensures that rewardAmount
is greater than pledgeAvailableRewardAmounts[pledgeId]
,
you may safely perform an unchecked subtraction:
if(rewardAmount > pledgeAvailableRewardAmounts[pledgeId]) revert Errors.RewardsBalanceTooLow(); pledgeAvailableRewardAmounts[pledgeId] -= rewardAmount;
Suggested:
if(rewardAmount > pledgeAvailableRewardAmounts[pledgeId]) revert Errors.RewardsBalanceTooLow(); unchecked { pledgeAvailableRewardAmounts[pledgeId] -= rewardAmount; }
#0 - c4-judge
2022-11-12T01:05:08Z
kirk-baird marked the issue as grade-b