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: 14/71
Findings: 5
Award: $594.45
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: cccz
Also found by: 0x52, 0xYamiDancho, GimelSec, IllIllI, PPrieditis, WatchPug, csanuragjain, danb, gzeon, hickuphh3, horsefacts, hyh, kenzo, leastwood, reassor, unforgiven
63.9296 DAI - $63.93
In passThruGate
function, msg.value
is checked to be greater than the required cost, but the excess amount is not returned to the sender.
function passThruGate(uint index, address) override external payable { Gate memory gate = gates[index]; require(msg.value >= gate.ethCost, 'Please send more ETH'); // pass thru ether if (msg.value > 0) { // use .call so we can send to contracts, for example gnosis safe, re-entrance is not a threat here (bool sent, bytes memory data) = gate.beneficiary.call{value: gate.ethCost}(""); require(sent, 'ETH transfer failed'); } }
function passThruGate(uint index, address) override external payable { uint price = getCost(index); require(msg.value >= price, 'Please send more ETH'); // bump up the price Gate storage gate = gates[index]; // multiply by the price increase factor gate.lastPrice = (price * gate.priceIncreaseFactor) / gate.priceIncreaseDenominator; // move up the reference gate.lastPurchaseBlock = block.number; // pass thru the ether if (msg.value > 0) { // use .call so we can send to contracts, for example gnosis safe, re-entrance is not a threat here (bool sent, bytes memory data) = gate.beneficiary.call{value: msg.value}(""); require(sent, 'ETH transfer failed'); } }
Return excess ETH to sender or make the require strict equal.
#0 - illuzen
2022-05-12T05:15:47Z
Duplicate #49
#1 - gititGoro
2022-06-14T02:54:50Z
Duplicate of #48
#2 - gititGoro
2022-06-14T02:54:58Z
Upgraded severity: lost funds.
🌟 Selected for report: MaratCerby
Also found by: CertoraInc, Ruhum, VAD37, berndartmueller, broccolirob, cryptphi, danb, gzeon, horsefacts, hyh, joestakey, leastwood, pedroais, peritoflores, throttle, wuwe1, z3s
19.1789 DAI - $19.18
Some ERC20.transfer does not revert upon failure, MerkleVesting
contract does not check the return value of the ERC20.transfer
and user will not be able to claim again due to currentWithdrawal
is subtracted from tranche.currentCoins
.
IERC20(tree.tokenAddress).transfer(destination, currentWithdrawal);
tranche.currentCoins -= currentWithdrawal;
Use safeTransfer or otherwise check the return value of ERC20.transfer()
#0 - illuzen
2022-05-12T04:42:55Z
Duplicate #27
#1 - gititGoro
2022-06-15T03:30:45Z
Duplicate of #22
coinsPerSecond
is totalCoins(in token's decimals) divided by the length of vesting period in seconds. For low decimal token such as USDC, coinsPerSecond
might have significant precision loss that will make user received less than the amount entitled.
uint coinsPerSecond = totalCoins / (endTime - startTime);
For example if 0.01 WBTC is vested over 1 year coinsPerSecond = 0.01 * 10e8 / (365*86400) = 0 which the user will not be able to claim any of the balance
Multiply coinsPerSecond
by for example 10e18
#0 - illuzen
2022-05-12T04:45:58Z
Duplicate #107
🌟 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
74.7635 DAI - $74.76
PermissionlessBasicPoolFactory use 0.8.12 while other file use 0.8.9 https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L3-L4
pragma solidity 0.8.12;
function setGlobalTax(uint newTaxPerCapita) external { require(msg.sender == globalBeneficiary, 'Only globalBeneficiary can set tax'); require(newTaxPerCapita < 1000, 'Tax too high'); globalTaxPerCapita = newTaxPerCapita; }
constructor(address _mgmt) { management = _mgmt; treeAdder = _mgmt; } /// @notice Change the management key /// @dev Only the current management key can change this /// @param newMgmt the new management key function setManagement(address newMgmt) external managementOnly { management = newMgmt; } /// @notice Change the treeAdder key /// @dev Only the current management key can call this /// @param newAdder new addres that will be able to add trees, old address will not be able to function setTreeAdder(address newAdder) external managementOnly { treeAdder = newAdder; }
uint paymentSlope = (maxTotalPayments - minTotalPayments) * PRECISION / (tree.maxEndTime - tree.minEndTime); // y = mx + b = paymentSlope * (x - x0) + y0 // divide by precision factor here since we have completed the rounding error sensitive operations totalCoins = (paymentSlope * (vestingTime - tree.minEndTime) / PRECISION) + minTotalPayments;
require(block.timestamp > pool.startTime, 'Cannot deposit before pool start');
#0 - illuzen
2022-05-12T08:35:46Z
all duplicates except mul-after-div
🌟 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
115.9068 DAI - $115.91
++numGates
instead of numGates += 1
numGates += 1;
There are some instance that there is an id
field and some address field. Normally id won't need full 256 bit and can be packed into the same slot as the address fields.
https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L15-L39
struct Receipt { uint id; // primary key uint amountDepositedWei; // amount of tokens originally deposited uint timeDeposited; // the time the deposit was made uint timeWithdrawn; // the time the deposit was withdrawn, or 0 if not withdrawn yet address owner; // the owner of the deposit } // this represents a single staking pool with >= 1 reward tokens struct Pool { uint id; // primary key uint[] rewardsWeiPerSecondPerToken; // array of reward rates, this number gets multiplied by time and tokens (not wei) to determine rewards uint[] rewardsWeiClaimed; // bookkeeping of how many rewards have been paid out for each token uint[] rewardFunding; // bookkeeping of how many rewards have been supplied for each token uint maximumDepositWei; // the size of the pool, maximum sum of all deposits uint totalDepositsWei; // current sum of all deposits uint numReceipts; // number of receipts issued uint startTime; // the time that the pool begins uint endTime; // time that the pool ends uint taxPerCapita; // portion of rewards that go to the contract creator address depositToken; // token that user deposits (stakes) address excessBeneficiary; // address that is able to reclaim unused rewards address[] rewardTokens; // array of token contract addresses that stakers will receive as rewards mapping (uint => Receipt) receipts; // mapping of receipt ids to receipt structs }
The increment of numTrees
and numPools
will never overflow an uint256, we can wrap them in unchecked block.
contracts/PermissionlessBasicPoolFactory.sol:103: Pool storage pool = pools[++numPools]; contracts/MerkleResistor.sol:86: merkleTrees[++numTrees] = MerkleTree( contracts/MerkleVesting.sol:64: merkleTrees[++numTrees] = MerkleTree( contracts/FixedPricePassThruGate.sol:30: Gate storage gate = gates[++numGates]; contracts/MerkleIdentity.sol:98: MerkleTree storage tree = merkleTrees[++numTrees]; contracts/SpeedBumpPriceGate.sol:39: Gate storage gate = gates[++numGates]; contracts/MerkleDropFactory.sol:51: merkleTrees[++numTrees] = MerkleTree(
Some variable can be marked as immutable to save gas https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L51-L54
address public globalBeneficiary;
pool.endTime can be calculated without using pool.startTime https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L106-L107
pool.startTime = startTime > block.timestamp ? startTime : block.timestamp; pool.endTime = (startTime > block.timestamp ? startTime : block.timestamp) + (programLengthDays * 1 days);
Instead of &&
the success flag in each iteration, we can have require(success, 'Token deposits failed');
inside the for loop.
https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L137-L149
function fundPool(uint poolId) internal { Pool storage pool = pools[poolId]; 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'); }
for (uint i = 0; i < rewards.length; i++) { pool.rewardsWeiClaimed[i] += rewards[i]; pool.rewardFunding[i] -= rewards[i]; uint tax = (pool.taxPerCapita * rewards[i]) / 1000; uint transferAmount = rewards[i] - tax; taxes[poolId][i] += tax; success = success && IERC20(pool.rewardTokens[i]).transfer(receipt.owner, transferAmount); } success = success && IERC20(pool.depositToken).transfer(receipt.owner, receipt.amountDepositedWei); require(success, 'Token transfer failed');
for (uint i = 0; i < pool.rewardTokens.length; i++) { uint rewards = pool.rewardFunding[i]; pool.rewardFunding[i] = 0; success = success && IERC20(pool.rewardTokens[i]).transfer(pool.excessBeneficiary, rewards); } require(success, 'Token transfer failed');
for (uint i = 0; i < pool.rewardTokens.length; i++) { uint tax = taxes[poolId][i]; taxes[poolId][i] = 0; success = success && IERC20(pool.rewardTokens[i]).transfer(globalBeneficiary, tax); } require(success, 'Token transfer failed');
for(uint256 i; i < length; ){ ... unchecked{ ++i; } }
Solidity ^0.8.4 allow the use of custom errors to optimize gas usage. https://blog.soliditylang.org/2021/04/21/custom-errors/
require(pool.id == poolId, 'Uninitialized pool');
#0 - illuzen
2022-05-12T08:35:03Z
all duplicates