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: 33/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
https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L653-L661
Users can lose all the rewards to the malicious/compromised owner.
https://github.com/code-423n4/2022-10-paladin/blob/main/contracts/WardenPledge.sol#L653-L661
' 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; }'
Change to:
' function recoverERC20(address token, address to, uint256 amount) 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; }'
#0 - Kogaroshi
2022-10-30T23:31:50Z
Duplicate of #17
#1 - c4-judge
2022-11-10T07:13:18Z
kirk-baird marked the issue as not a duplicate
#2 - c4-judge
2022-11-10T07:13:24Z
kirk-baird marked the issue as duplicate
#3 - c4-judge
2022-11-10T21:23:24Z
kirk-baird marked the issue as satisfactory
#4 - c4-judge
2022-11-10T21:23:29Z
kirk-baird marked the issue as not a duplicate
#5 - c4-judge
2022-11-10T21:23:34Z
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
🌟 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
address(0x0)
when assigning values to address
state variables2022-10-paladin/contracts/WardenPledge.sol::140 => chestAddress = _chestAddress; 2022-10-paladin/contracts/WardenPledge.sol::142 => minTargetVotes = _minTargetVotes;
2022-10-paladin/contracts/WardenPledge.sol::173 => return pledges;
2022-10-paladin/contracts/WardenPledge.sol::24 => uint256 public constant WEEK = 7 * 86400;
Use a solidity version of at least 0.8.13 to get the ability to use using for with a list of free functions
2022-10-paladin/contracts/WardenPledge.sol::2 => pragma solidity 0.8.10;
indexed
fieldsEach event should use three indexed fields if there are three or more fields
2022-10-paladin/contracts/WardenPledge.sol::94 => event ExtendPledgeDuration(uint256 indexed pledgeId, uint256 oldEndTimestamp, uint256 newEndTimestamp); 2022-10-paladin/contracts/WardenPledge.sol::96 => event IncreasePledgeTargetVotes(uint256 indexed pledgeId, uint256 oldTargetVotes, uint256 newTargetVotes); 2022-10-paladin/contracts/WardenPledge.sol::98 => event IncreasePledgeRewardPerVote(uint256 indexed pledgeId, uint256 oldRewardPerVote, uint256 newRewardPerVote); 2022-10-paladin/contracts/WardenPledge.sol::102 => event RetrievedPledgeRewards(uint256 indexed pledgeId, address receiver, uint256 amount); 2022-10-paladin/contracts/WardenPledge.sol::105 => event Pledged(uint256 indexed pledgeId, address indexed user, uint256 amount, uint256 endTimestamp); 2022-10-paladin/contracts/WardenPledge.sol::108 => event NewRewardToken(address indexed token, uint256 minRewardPerSecond); 2022-10-paladin/contracts/WardenPledge.sol::110 => event UpdateRewardToken(address indexed token, uint256 minRewardPerSecond); 2022-10-paladin/contracts/WardenPledge.sol::112 => event RemoveRewardToken(address indexed token); 2022-10-paladin/contracts/WardenPledge.sol::115 => event ChestUpdated(address oldChest, address newChest); 2022-10-paladin/contracts/WardenPledge.sol::117 => event PlatformFeeUpdated(uint256 oldfee, uint256 newFee); 2022-10-paladin/contracts/WardenPledge.sol::119 => event MinTargetUpdated(uint256 oldMinTarget, uint256 newMinTargetVotes);
2022-10-paladin/contracts/WardenPledge.sol::43 => bool closed;
2022-10-paladin/contracts/WardenPledge.sol::1 => // SPDX-License-Identifier: MIT
public
functions not called by the contract should be declared external
insteadContracts are allowed to override their parents’ functions and change the visibility from public to external .
2022-10-paladin/contracts/WardenPledge.sol::153 => function pledgesIndex() public view returns(uint256){ 2022-10-paladin/contracts/WardenPledge.sol::163 => function getUserPledges(address user) external view returns(uint256[] memory){ 2022-10-paladin/contracts/WardenPledge.sol::172 => function getAllPledges() external view returns(Pledge[] memory){ 2022-10-paladin/contracts/WardenPledge.sol::181 => function _getRoundedTimestamp(uint256 timestamp) internal pure returns(uint256) { 2022-10-paladin/contracts/WardenPledge.sol::195 => function pledge(uint256 pledgeId, uint256 amount, uint256 endTimestamp) external whenNotPaused nonReentrant { 2022-10-paladin/contracts/WardenPledge.sol::206 => function pledgePercent(uint256 pledgeId, uint256 percent, uint256 endTimestamp) external whenNotPaused nonReentrant { 2022-10-paladin/contracts/WardenPledge.sol::222 => function _pledge(uint256 pledgeId, address user, uint256 amount, uint256 endTimestamp) internal { 2022-10-paladin/contracts/WardenPledge.sol::299 => function createPledge( 2022-10-paladin/contracts/WardenPledge.sol::368 => function extendPledge( 2022-10-paladin/contracts/WardenPledge.sol::414 => function increasePledgeRewardPerVote( 2022-10-paladin/contracts/WardenPledge.sol::456 => function retrievePledgeRewards(uint256 pledgeId, address receiver) external whenNotPaused nonReentrant { 2022-10-paladin/contracts/WardenPledge.sol::488 => function closePledge(uint256 pledgeId, address receiver) external whenNotPaused nonReentrant { 2022-10-paladin/contracts/WardenPledge.sol::525 => function _addRewardToken(address token, uint256 minRewardPerSecond) internal { 2022-10-paladin/contracts/WardenPledge.sol::541 => function addMultipleRewardToken(address[] calldata tokens, uint256[] calldata minRewardsPerSecond) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::560 => function addRewardToken(address token, uint256 minRewardPerSecond) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::570 => function updateRewardToken(address token, uint256 minRewardPerSecond) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::585 => function removeRewardToken(address token) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::599 => function updateChest(address chest) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::612 => function updateMinTargetVotes(uint256 newMinTargetVotes) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::625 => function updatePlatformFee(uint256 newFee) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::636 => function pause() external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::643 => function unpause() external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::653 => function recoverERC20(address token) external onlyOwner returns(bool) { 2022-10-paladin/contracts/WardenPledge.sol::665 => function safe64(uint256 n) internal pure returns (uint64) {
There are units for seconds, minutes, hours, days, and weeks
2022-10-paladin/contracts/WardenPledge.sol::34 => // Price per vote per second, set by the owner 2022-10-paladin/contracts/WardenPledge.sol::79 => uint256 public minDelegationTime = 1 weeks; 2022-10-paladin/contracts/WardenPledge.sol::262 => // each second of the Boost duration 2022-10-paladin/contracts/WardenPledge.sol::303 => uint256 rewardPerVote, // reward/veToken/second
#0 - c4-judge
2022-11-11T23:40:24Z
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
immutable
Avoids a Gsset (20000 gas)
2022-10-paladin/contracts/WardenPledge.sol::47 => Pledge[] public pledges; 2022-10-paladin/contracts/WardenPledge.sol::60 => IVotingEscrow public votingEscrow; 2022-10-paladin/contracts/WardenPledge.sol::62 => IBoostV2 public delegationBoost; 2022-10-paladin/contracts/WardenPledge.sol::73 => address public chestAddress; 2022-10-paladin/contracts/WardenPledge.sol::76 => uint256 public minTargetVotes;
If variables occupying the same slot are both written the same function or by the constructor avoids a separate Gsset (20000 gas). Reads of the variables are also cheaper
2022-10-paladin/contracts/WardenPledge.sol::73 => address public chestAddress;
<array>.length
should not be looked up in every loop of a for
loopEven memory arrays incur the overhead of bit tests and bit shifts to calculate the array length. Storage array length checks incur an extra Gwarmaccess (100 gas) PER-LOOP.
2022-10-paladin/contracts/WardenPledge.sol::547 => for(uint256 i = 0; i < length;){
> 0
costs more gas than != 0
when used on a uint
in a require()
statement2022-10-paladin/contracts/WardenPledge.sol::471 => if(remainingAmount > 0) { 2022-10-paladin/contracts/WardenPledge.sol::504 => if(remainingAmount > 0) {
2022-10-paladin/contracts/WardenPledge.sol::473 => pledgeAvailableRewardAmounts[pledgeId] = 0; 2022-10-paladin/contracts/WardenPledge.sol::506 => pledgeAvailableRewardAmounts[pledgeId] = 0; 2022-10-paladin/contracts/WardenPledge.sol::547 => for(uint256 i = 0; i < length;){ 2022-10-paladin/contracts/WardenPledge.sol::589 => minAmountRewardToken[token] = 0;
payable
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided.
2022-10-paladin/contracts/WardenPledge.sol::541 => function addMultipleRewardToken(address[] calldata tokens, uint256[] calldata minRewardsPerSecond) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::560 => function addRewardToken(address token, uint256 minRewardPerSecond) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::570 => function updateRewardToken(address token, uint256 minRewardPerSecond) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::585 => function removeRewardToken(address token) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::599 => function updateChest(address chest) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::612 => function updateMinTargetVotes(uint256 newMinTargetVotes) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::625 => function updatePlatformFee(uint256 newFee) external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::636 => function pause() external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::643 => function unpause() external onlyOwner { 2022-10-paladin/contracts/WardenPledge.sol::653 => function recoverERC20(address token) external onlyOwner returns(bool) {
Use a solidity version of at least 0.8.13 to have external calls skip contract existence checks if the external call has a return value
2022-10-paladin/contracts/WardenPledge.sol::2 => pragma solidity 0.8.10;
When multiplying or dividing by a power of two, it is cheaper to bitshift than to use standard math operations.
2022-10-paladin/contracts/WardenPledge.sol::263 => uint256 totalDelegatedAmount = ((bias * boostDuration) + bias) / 2;
The comparison operators >=
and <=
use more gas than >
,
<
, or ==
. Replacing the >=
and ≤
operators with a comparison
operator that has an opcode in the EVM saves gas.
2022-10-paladin/contracts/WardenPledge.sol::223 => if(pledgeId >= pledgesIndex()) revert Errors.InvalidPledgeID(); 2022-10-paladin/contracts/WardenPledge.sol::229 => if(pledgeParams.endTimestamp <= block.timestamp) revert Errors.ExpiredPledge(); 2022-10-paladin/contracts/WardenPledge.sol::374 => if(pledgeId >= pledgesIndex()) revert Errors.InvalidPledgeID(); 2022-10-paladin/contracts/WardenPledge.sol::380 => if(pledgeParams.endTimestamp <= block.timestamp) revert Errors.ExpiredPledge(); 2022-10-paladin/contracts/WardenPledge.sol::420 => if(pledgeId >= pledgesIndex()) revert Errors.InvalidPledgeID(); 2022-10-paladin/contracts/WardenPledge.sol::426 => if(pledgeParams.endTimestamp <= block.timestamp) revert Errors.ExpiredPledge(); 2022-10-paladin/contracts/WardenPledge.sol::429 => if(newRewardPerVote <= oldRewardPerVote) revert Errors.RewardsPerVotesTooLow(); 2022-10-paladin/contracts/WardenPledge.sol::457 => if(pledgeId >= pledgesIndex()) revert Errors.InvalidPledgeID(); 2022-10-paladin/contracts/WardenPledge.sol::489 => if(pledgeId >= pledgesIndex()) revert Errors.InvalidPledgeID(); 2022-10-paladin/contracts/WardenPledge.sol::496 => if(pledgeParams.endTimestamp <= block.timestamp) revert Errors.ExpiredPledge();
Replace the comparison operator and reverse the logic to save gas using the suggestions above.
Checking non-zero transfer values can avoid an expensive external call and save gas.
While this is done in some places, it’s not consistently done in the solution.
2022-10-paladin/contracts/WardenPledge.sol::271 => IERC20(pledgeParams.rewardToken).safeTransfer(user, rewardAmount); 2022-10-paladin/contracts/WardenPledge.sol::475 => IERC20(pledgeParams.rewardToken).safeTransfer(receiver, remainingAmount); 2022-10-paladin/contracts/WardenPledge.sol::508 => IERC20(pledgeParams.rewardToken).safeTransfer(receiver, remainingAmount); 2022-10-paladin/contracts/WardenPledge.sol::658 => IERC20(token).safeTransfer(owner(), amount);
private
rather than public
for constants, saves gasIf needed, the value can be read from the verified contract source code. Savings are due to the compiler not having to create non-payable getter functions for deployment call data, and not adding another entry to the method ID table
2022-10-paladin/contracts/WardenPledge.sol::22 => uint256 public constant UNIT = 1e18; 2022-10-paladin/contracts/WardenPledge.sol::23 => uint256 public constant MAX_PCT = 10000; 2022-10-paladin/contracts/WardenPledge.sol::24 => uint256 public constant WEEK = 7 * 86400;
Saves a storage slot. If the variable is assigned a non-zero value, saves Gsset (20000 gas). If it’s assigned a zero value, saves Gsreset (2900 gas). If the variable remains unassigned, there is no gas savings unless the variable is public, in which case the compiler-generated non-payable getter deployment cost is saved. If the state variable is overriding an interface’s public function, mark the variable as constant or immutable so that it does not use a storage slot
2022-10-paladin/contracts/WardenPledge.sol::52 => mapping(address => uint256[]) public ownerPledges;
<x> * 2 is equivalent to <x> << 1 and <x> / 2 is the same as <x> >> 1. The MUL and DIV opcodes cost 5 gas, whereas SHL and SHR only cost 3 gas
2022-10-paladin/contracts/WardenPledge.sol::263 => uint256 totalDelegatedAmount = ((bias * boostDuration) + bias) / 2;
#0 - kirk-baird
2022-11-09T21:34:19Z
Some issues are already included in the C4 public disclose for this contest found here. Specifically
#1 - c4-judge
2022-11-09T21:34:23Z
kirk-baird marked the issue as grade-b