Platform: Code4rena
Start Date: 04/11/2021
Pot Size: $50,000 USDC
Total HM: 20
Participants: 28
Period: 7 days
Judge: 0xean
Total Solo HM: 11
Id: 51
League: ETH
Rank: 3/28
Findings: 5
Award: $3,959.10
🌟 Selected for report: 17
🚀 Solo Findings: 2
🌟 Selected for report: pants
872.285 USDC - $872.28
pants
The function MainToken.set_mint_multisig()
doesn't check that _minting_multisig
doesn't equal zero before it sets it as the new minting_multisig
.
This function can be invoked by mistake with the zero address as _minting_multisig
, causing the system to lose its minting_multisig
forever, without the option to set a new minting_multisig
.
Manual code review.
Check that _minting_multisig
doesn't equal zero before setting it as the new minting_multisig
.
🌟 Selected for report: pants
872.285 USDC - $872.28
pants
The function LPToken.set_minter()
doesn't check that _minter
doesn't equal zero before it sets it as the new minter.
This function can be invoked by mistake with the zero address as _minter
, causing the system to lose its minter forever, without the option to set a new minter.
Manual code review.
Check that _minter
doesn't equal zero before setting it as the new minter.
872.285 USDC - $872.28
pants
The function MainToken.set_admin()
doesn't check that _admin
doesn't equal zero before it sets it as the new admin.
This function can be invoked by mistake with the zero address as _admin
, causing the system to lose its admin forever, without the option to set a new admin.
Manual code review.
Check that _admin
doesn't equal zero before setting it as the new admin.
#0 - chickenpie347
2021-11-16T14:11:01Z
Addressed in #90.
#1 - 0xean
2022-01-08T02:49:21Z
dupe of #35
130.8427 USDC - $130.84
pants
An admin can (by mistake maybe) addInvestor with address that already exists. This way its funds are locked in the system and cannot be withdrawn, even by the admin.
🌟 Selected for report: pants
290.7617 USDC - $290.76
pants
The functions ETHPoolDelegator.balances()
and ETHPoolDelegator.coins()
accept an argument called i
and use it as an index to determine which element in the _balances
/ _coins
array should be loaded and returned. However, these functions don't check that the index they receive as an argument actually fits the bounds of the array.
If the index exceeds the array length, there will be a revert with no informative error message. The user wouldn't know what caused the revert.
Manual code review.
Add an appropriate require statement to each of these functions to validate that the given argument fits the _balances
/ _coins
array bounds.
🌟 Selected for report: pants
290.7617 USDC - $290.76
pants
The functions USDPoolDelegator.balances()
, USDPoolDelegator.coins()
and USDPoolDelegator.underlying_coins()
accept an argument called i
and use it as an index to determine which element in the _balances
/ _coins
/ _underlying_coins
array should be loaded and returned. However, these functions don't check that the index they receive as an argument actually fits the bounds of the array.
If the index exceeds the array length, there will be a revert with no informative error message. The user wouldn't know what caused the revert.
Manual code review.
Add an appropriate require statement to each of these functions to validate that the given argument fits the _balances
/ _coins
/ _underlying_coins
array bounds.
🌟 Selected for report: TomFrenchBlockchain
Also found by: PranavG, Reigada, WatchPug, jah, nathaniel, pants, pauliax, pmerkleplant
2.147 USDC - $2.15
pants
At LPToken.sol the state variable swap could be set immutable. swap is defined in line 20.
#0 - 0xean
2022-01-08T23:13:21Z
duplicate of #1
8.1808 USDC - $8.18
pants
optimizing for loops by using unchecked{++i} over i++
For example in line 535 of SwapUtils.sol you have
for (uint256 i = 0; i < numTokens; i++) {
While a more effiient looping would be:
for (uint256 i = 0; i < numTokens; unchecked{++i}) {
One cannot reach uint256 maximal value so it's overflow safe.
#0 - 0xean
2022-01-08T23:11:17Z
duplicate of #261
pants
Avoiding initialization of loop index to save gas since it's already initialized to 0. For example in line 116 of vesting.sol.
This is also happening in many other places (>10) of the code.
#0 - chickenpie347
2022-01-03T23:24:38Z
Duplicate of #5
🌟 Selected for report: pants
290.7617 USDC - $290.76
pants
The functions BTCPoolDelegator.balances()
and BTCPoolDelegator.coins()
accept an argument called i
and use it as an index to determine which element in the _balances
/ _coins
array should be loaded and returned. However, these functions don't check that the index they receive as an argument actually fits the bounds of the array.
If the index exceed the array length, there will be a revert with no informative error message. The user wouldn't know what caused the revert.
Manual code review.
Add an appropriate require statement to each of these functions to validate that the given argument fits the _balances
/ _coins
array bounds.
4.4176 USDC - $4.42
pants
Some function of USDPoolDelegator that declared public could be set external since there are no in-contract calls to them. For example the 'balances' function.
#0 - 0xean
2022-01-08T23:18:18Z
duplicate of #121
🌟 Selected for report: pants
44.8876 USDC - $44.89
pants
In BTCPoolDelegator contract, address state variables future_owner
in lines 51 and bool state variable is_killed
in line 55 should be placed one after another.
solidity keep storage in 32 bytes slot and can optimize multiple variables that are less than 32 bytes.
address is 20 bytes and bool is 1 byte, so it can be placed in one storage slot instead of two.
Tested it on Remix, saves 50 gas per transaction
change
50 uint256 public future_admin_fee; 51 address public future_owner; 52 53 uint256 kill_deadline; 54 uint256 constant kill_deadline_dt = 2 * 30 * 86400; 54 bool is_killed;
to
50 uint256 public future_admin_fee; 51 uint256 constant kill_deadline_dt = 2 * 30 * 86400; 52 53 uint256 kill_deadline; 54 address public future_owner; 54 bool is_killed;
8.1808 USDC - $8.18
pants
When double reading a variable from memory the gas efficient way is to cache it and use the cached value.
For example in line 537, 538 of SwapUtils.sol you have two accesses to xp[i]. You can save xp[i] as local variable and then use it instead at the second time.
🌟 Selected for report: pants
44.8876 USDC - $44.89
pants
The function PoolGauge.withdraw()
doesn't have to execute these lines of code when _value
equals zero:
_balance: uint256 = self.balanceOf[msg.sender] - _value _supply: uint256 = self.totalSupply - _value self.balanceOf[msg.sender] = _balance self.totalSupply = _supply self._update_liquidity_limit(msg.sender, _balance, _supply) assert ERC20(self.lp_token).transfer(msg.sender, _value)
There is no reason to execute these lines of code if _value
equals zero because they won't affect the system. An identical optimization is already implemented in PoolGauge.deposit()
.
Manual code review.
Execute these lines of code only if _value
doesn't equal zero.
3.4079 USDC - $3.41
pants
In InvestorDistribution at line 19 you have
using SafeMath for uint256;
although you use solidity >0.8.0 therefore you don't need to use safeMath for uint256
🌟 Selected for report: pants
44.8876 USDC - $44.89
pants
In InvestorDistribution you generally use uint256 for every quantity, although 256 bits are much more than necessary. For example for Investor structure you can change all to uint128, and for every normal use it will not affect and save gas.
12.1196 USDC - $12.12
pants
Consider the following require statement:
// condition is boolean // str is a string require(condition, str) The string str is split into 32-byte sized chunks and then stored in memory using mstore, then the memory offsets are provided to revert(offset, length). For chunks shorter than 32 bytes, and for low --optimize-runs value (usually even the default value of 200), instead of push32 val, where val is the 32 byte hexadecimal representation of the string with 0 padding on the least significant bits, the solidity compiler replaces it by shl(value, short-value)). Where short-value does not have any 0 padding. This saves the total bytes in the deploy code and therefore saves deploy time cost, at the expense of extra 6 gas during runtime. This means that shorter revert strings saves deploy time costs of the contract. Note that this kind of saving is not relevant for high values of --optimize-runs as push32 value will not be replaced by a shl(..., ...) equivalent by the Solidity compiler.
Going back, each 32 byte chunk of the string requires an extra mstore. That is, additional cost for mstore, memory expansion costs, as well as stack operations. Note that, this runtime cost is only relevant when the revert condition is met.
Overall, shorter revert strings can save deploy time as well as runtime costs.
Note that if your contracts already allow using at least Solidity 0.8.4, then consider using Custom errors. This is more gas efficient, while allowing the developer to describe the errors in detail using NatSpec. A disadvantage to this approach is that, some tooling may not have proper support for this, as well as block explorers like Etherscan.
Examples Here is a non-exhaustive list of lengthy revert strings that could be shortened:
Vesting.sol line 197 Swap.sol line 149
#0 - chickenpie347
2022-01-03T23:27:32Z
Duplicate of #23
12.1196 USDC - $12.12
pants
Removing unused named return variables can reduce gas usage and improve code clarity.
For example, FairSideDAO.sol line 275 You both use named return and actual return statement. We recommend to use only one of them as explained.
Look at this issue: https://github.com/code-423n4/2021-10-tempus-findings/issues/4
#0 - chickenpie347
2022-01-03T23:28:46Z
Duplicate of #25 , think you also mixed up projects you are reviewing.
20.1994 USDC - $20.20
pants
You import "import "./hardhat/console.sol";" and all uses are commented. You should also comment the import. SwapUtils line 9
20.1994 USDC - $20.20
pants
In InvestorDistribution line 103 you read investors[_investor] multiple times (only at this function investors[_investor] is read 6 times). You could cache the value and call the cached value instead. Investors x = investors[_investor]; And then use x.amount, x.claimed, etc.
12.1196 USDC - $12.12
pants
On L158 of swap.sol, you use a uint8 as the for loop variable:
Due to how the EVM natively works on 256 numbers, using a 8 bit number here introduces additional costs as the EVM has to properly enforce the limits of this smaller type.
See the warning at this link: https://docs.soliditylang.org/en/v0.8.0/internals/layout_in_storage.html#layout-of-state-variables-in-storage
🌟 Selected for report: pants
44.8876 USDC - $44.89
pants
Since no contract inherent from SwapUtils all internal functions could be set private. For example getD could be set private to save gas.
8.1808 USDC - $8.18
pants
optimizing for loops by caching memory array length, instead of calling it every time.
For example at Swap.sol at time 158 you should have
uint8 len = _pooledTokens.length and in the next line define the forloop with stop condition of i<len
This appears in many places in the code. At some places you did cached the array length correctly.
🌟 Selected for report: pants
44.8876 USDC - $44.89
pants
If data can fit into 32 bytes, then you should use bytes32 datatype rather than bytes or strings as it is much cheaper in solidity. Basically, Any fixed size variable in solidity is cheaper than variable size. On the MarketPlace.sol contract, string memory variable can be replaced with bytes32 array. That will save gas on the contract.
An example is revert messages. For example look at line 32 of PublicSale.sol.