Platform: Code4rena
Start Date: 04/05/2022
Pot Size: $50,000 DAI
Total HM: 24
Participants: 71
Period: 5 days
Judge: Justin Goro
Total Solo HM: 14
Id: 119
League: ETH
Rank: 39/71
Findings: 2
Award: $120.64
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: horsefacts
Also found by: 0x1f8b, 0xYamiDancho, 0xf15ers, 0xkatana, ACai, AlleyCat, Bruhhh, Dravee, Funen, GimelSec, Hawkeye, IllIllI, MaratCerby, PPrieditis, Picodes, Ruhum, TerrierLover, VAD37, berndartmueller, csanuragjain, defsec, delfin454000, eccentricexit, ellahi, fatherOfBlocks, gzeon, hansfriese, hickuphh3, hyh, ilan, joestakey, juicy, kebabsec, oyc_109, rajatbeladiya, reassor, rfa, robee, samruna, simon135, sorrynotsorry, throttle
80.7003 DAI - $80.70
IERC20.sol, IERC721.sol, and IERC721Receiver.sol seem to have very similar implementations with openzeppelin's counterparts.
It is worth using the latest versions if possible.
IERC721.sol in the FactoryDAO project does not implement IERC165. https://github.com/code-423n4/2022-05-factorydao/blob/main/interfaces/IERC721.sol#L8
Here is an implementation of the IERC721 of openzeppelin.
interface IERC721 is IERC165 {
It may be worth adding the support of the interface of ERC165 in this project. Reference
Arguments in the functions used in IERC721 have type variableName
. However, following two parts are exceptions.
https://github.com/code-423n4/2022-05-factorydao/blob/main/interfaces/IERC721.sol#L104
function setApprovalForAll(address operator, bool _approved) external;
https://github.com/code-423n4/2022-05-factorydao/blob/main/interfaces/IERC721.sol#L111
function isApprovedForAll(address, address) external view returns (bool);
If this peoject keeps using the copy of IERC721, it is worth fixing the above style inconsistency as follows.
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
_globalBeneficiary variable does not have any address(0) check.
constructor(address _globalBeneficiary, uint _globalTaxPerCapita) { globalBeneficiary = _globalBeneficiary; globalTaxPerCapita = _globalTaxPerCapita; }
There is no way to update the state variable globalBeneficiary, so it is worth considering adding address(0) check.
constructor(address _globalBeneficiary, uint _globalTaxPerCapita) { require(_globalBeneficiary != address(0), "..."); globalBeneficiary = _globalBeneficiary; globalTaxPerCapita = _globalTaxPerCapita; }
addPool receives rewardsWeiPerSecondPerToken and rewardTokenAddresses array.
function addPool ( uint startTime, uint maxDeposit, uint[] memory rewardsWeiPerSecondPerToken, uint programLengthDays, address depositTokenAddress, address excessBeneficiary, address[] memory rewardTokenAddresses, bytes32 ipfsHash, bytes32 name ) external {
It is possible that user can add empty array on rewardsWeiPerSecondPerToken and rewardTokenAddresses. In this case, the pool does not do anything. If this is not expected, it is worth adding the empty array check at addPool function.
function addPool ( uint startTime, uint maxDeposit, uint[] memory rewardsWeiPerSecondPerToken, uint programLengthDays, address depositTokenAddress, address excessBeneficiary, address[] memory rewardTokenAddresses, bytes32 ipfsHash, bytes32 name ) external { require(rewardsWeiPerSecondPerToken.length != 0, "rewardsWeiPerSecondPerToken cannot be empty");
withdrawTaxes function does not have event emitted while withdrawExcessRewards function does have ExcessRewardsWithdrawn
emission.
For the consistency, withdrawTaxes should have the event emitted.
#0 - illuzen
2022-05-12T09:15:37Z
all duplicates
#1 - gititGoro
2022-06-12T01:26:25Z
For the first item, it's common practice for devs to re-write an interface locally to lock down features and strip out what isn't needed. Openzeppelin often fills up their contracts with code related to their gas savings network that, in the course of a dapp that is not interested in that, is simply bloat which reduces the final deploy size possible for any downstream contracts.
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xYamiDancho, 0xf15ers, 0xkatana, ACai, CertoraInc, Dravee, Funen, GimelSec, Hawkeye, PPrieditis, Picodes, Ruhum, TerrierLover, Tomio, VAD37, Waze, csanuragjain, defsec, delfin454000, eccentricexit, ellahi, fatherOfBlocks, gzeon, hansfriese, horsefacts, ilan, joestakey, juicy, minhquanym, oyc_109, rajatbeladiya, reassor, rfa, robee, samruna, simon135, z3s
39.9359 DAI - $39.94
There are some argument checks at addPool function.
function addPool ( ... ) external { Pool storage pool = pools[++numPools]; ... pool.taxPerCapita = globalTaxPerCapita; require(rewardsWeiPerSecondPerToken.length == rewardTokenAddresses.length, 'Rewards and reward token arrays must be same length');
To reduce the gas cost for the user for the failure case, it is worth adding the check at the start of the function.
function addPool ( ... ) external { require(rewardsWeiPerSecondPerToken.length == rewardTokenAddresses.length, 'Rewards and reward token arrays must be same length'); Pool storage pool = pools[++numPools]; ... pool.taxPerCapita = globalTaxPerCapita;
In the for loop, uint i = 0
is used but this is not needed.
for (uint i = 0; i < rewardTokenAddresses.length; i++) {
In this contract, following places have this behavior. https://github.com/code-423n4/2022-05-factorydao/blob/e22a562c01c533b8765229387894cc0cb9bed116/contracts/PermissionlessBasicPoolFactory.sol#L115
The default value of uint is 0, so removing it can reduce the gas cost like this.
for (uint i; i < rewardTokenAddresses.length; i++) {
success
boolean flag can be optimizedsuccess
boolean flag is used in the for loop. If the transfer fails for any of the token, the operation will be reverted because of require(success, 'Token deposits failed')
.
bool success = true; uint amount; for (uint i = 0; i < pool.rewardFunding.length; i++) { amount = getMaximumRewards(poolId, i); // transfer the tokens from pool-creator to this contract success = success && IERC20(pool.rewardTokens[i]).transferFrom(msg.sender, address(this), amount); // bookkeeping to make sure pools don't share tokens pool.rewardFunding[i] += amount; } require(success, 'Token deposits failed');
If the numbers of loop increase, this can consume gas cost for the failure operations which are not good behavior for users. It can use break
in the for loop if success becomes false to reduce the operations of the for loop like this.
bool success = true; uint amount; for (uint i = 0; i < pool.rewardFunding.length; i++) { amount = getMaximumRewards(poolId, i); // transfer the tokens from pool-creator to this contract success = success && IERC20(pool.rewardTokens[i]).transferFrom(msg.sender, address(this), amount); if (!success) { break; } // bookkeeping to make sure pools don't share tokens pool.rewardFunding[i] += amount; } require(success, 'Token deposits failed');
Following codes have the pattern mentioned above, and worth revisiting.
It is certain that pool.maximumDepositWei - pool.totalDepositsWei
will not be underflown because of require(pool.totalDepositsWei < pool.maximumDepositWei, 'Maximum deposit already reached')
.
require(pool.totalDepositsWei < pool.maximumDepositWei, 'Maximum deposit already reached'); if (pool.totalDepositsWei + amount > pool.maximumDepositWei) { amount = pool.maximumDepositWei - pool.totalDepositsWei; }
This part can be rewritten like this to reduce the gas cost by using unchecked.
require(pool.totalDepositsWei < pool.maximumDepositWei, 'Maximum deposit already reached'); if (pool.totalDepositsWei + amount > pool.maximumDepositWei) { unchecked { amount = pool.maximumDepositWei - pool.totalDepositsWei; } }
#0 - illuzen
2022-05-12T09:15:03Z
all duplicates
#1 - gititGoro
2022-06-05T00:23:46Z
For the success item, why not just require on every transfer?
uint amount; for (uint i = 0; i < pool.rewardFunding.length; i++) { amount = getMaximumRewards(poolId, i); // transfer the tokens from pool-creator to this contract require(IERC20(pool.rewardTokens[i]).transferFrom(msg.sender, address(this), amount),'Token deposits failed'); // bookkeeping to make sure pools don't share tokens pool.rewardFunding[i] += amount; }
Note to sponsor: the existing code assumes the token conforms to the ERC20 standard and returns a boolean. If you'd like to support tokens that don't, consider using a safe transfer approach such as this pattern used in Uniswap V2 (https://github.com/Uniswap/v2-core/blob/8b82b04a0b9e696c0e83f8b2f00e5d7be6888c79/contracts/UniswapV2Pair.sol#L44)