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: 16/71
Findings: 5
Award: $502.05
π 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
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/FixedPricePassThruGate.sol#L48 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/SpeedBumpPriceGate.sol#L67
When a user send ETH via the withdraw()
of MerkleIdentity
, It will call IPriceGate(tree.priceGateAddress).passThruGate{value: msg.value}(tree.priceIndex, msg.sender);
. passThruGate()
checks whether msg.value >= gate.ethCost
holds. If a user accidentally sends more ETH than he has to, the contract just accepts it.
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/FixedPricePassThruGate.sol#L48
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'); } }
vim
Change >=
to ==
or
Add a refund mechanism like
function passThruGate(uint index, address sender) 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'); } // refund if (msg.value > price) { payable(sender).transfer(msg.value - price); } }
#0 - illuzen
2022-05-11T15:01:02Z
Duplicate #49
#1 - gititGoro
2022-06-14T02:54:19Z
Duplicate of #48
#2 - gititGoro
2022-06-14T02:54:31Z
Upgraded severity: lost funds.
π Selected for report: MaratCerby
Also found by: 0x1337, 0x52, 0xYamiDancho, AuditsAreUS, CertoraInc, Dravee, GimelSec, IllIllI, PPrieditis, Ruhum, TrungOre, VAD37, berndartmueller, cccz, csanuragjain, defsec, hickuphh3, horsefacts, hyh, jayjonah8, kenzo, leastwood, mtz, p4st13r4, reassor, throttle, wuwe1, ych18
3.1753 DAI - $3.18
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L146 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleDropFactory.sol#L73 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleVesting.sol#L85 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleResistor.sol#L112
Some tokens have a fee on each transfer
. The protocol doesnβt handle the fee when transferring ERC20 tokens, leading to the inconsistent amount of token actually received in the contract.
The impact is that a pool will not afford enough rewards, but the amount
value is greater than the amount of token actually received, resulting in the overuse of reward tokens from other pools when users call withdraw
.
Also, when the total amount of tokens in the pool is insufficient, withdrawExcessRewards
will also be unable to claim by excessBeneficiary
.
In fundPool
:
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'); }
pool.rewardFunding
should not store amount
directly. Some ERC20 transfers will charge a fee, causing the amount to not match the actual number of tokens received.
Same scenario in MerkleDropFactory, MerkleVesting and MerkleResistor contracts.
vim
Use balanceAfter - balanceBefore
rather than using amount
directly:
function retrieveTokens(address sender, uint256 amount) public { uint256 balanceBefore = deflationaryToken.balanceOf(address(this)); deflationaryToken.transferFrom(sender, address(this), amount); uint256 balanceAfter = deflationaryToken.balanceOf(address(this)); amount = balanceAfter.sub(balanceBefore); totalTokenTransferred += amount; }
Token Transfer Calculation Accuracy
in this article: https://blog.chain.link/defi-security-best-practices/
#0 - illuzen
2022-05-11T15:07:44Z
Duplicate #34
#1 - gititGoro
2022-06-14T02:02:22Z
Changing severity due pool isolation, rewards tokens represent leakage not lost funds and because transfer of deposited funds would fail on deposit.
320.671 DAI - $320.67
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleResistor.sol#L259 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleResistor.sol#L264
If a user tries to claim a few totalCoins
with a long vestingTime
, this user will call the initialize
function failed, and can not withdraw funds.
In MerkleResistor.sol L259:
uint coinsPerSecond = (totalCoins * (uint(100) - tree.pctUpFront)) / (vestingTime * 100);
If a user try to call initialize
function with few totalCoins
(which is limited by maxTotalPayments
) and a long vestingTime
(which is limited the lower bond by tree.minEndTime
), the user may get coinsPerSecond
to 0.
If coinsPerSecond
is 0, it will break L264 due to division by zero:
uint startTime = block.timestamp + vestingTime - (totalCoins / coinsPerSecond);
The user will not be able to initialize and withdraw funds.
vim
Save totalCoins
and coinsPerSecond
with PRECISION
to calculate startTime
to avoid division by zero.
Or check coinsPerSecond
:
if (coinsPerSecond == 0) { startTime = block.timestamp; }
#0 - illuzen
2022-05-11T15:03:40Z
Duplicate #107
#1 - gititGoro
2022-06-15T03:13:13Z
Reducing severity to match original issue.
π 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
75.1651 DAI - $75.17
We list 1 low-critical finding and 1 non-critical finding:
verifyVestingSchedule
will be broken when minEndTime == 0
and someone claim vestingTime = 0
MerkleIdentity.sol
setter functionsverifyVestingSchedule
will be broken when minEndTime == 0
and someone claim vestingTime = 0
If the minEndTime
is set to 0 and a user tries to claim vestingTime
to 0, the user will be reverted due to division by zero.
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleResistor.sol#L259
uint coinsPerSecond = (totalCoins * (uint(100) - tree.pctUpFront)) / (vestingTime * 100);
vim
Check minEndTime != 0
in addMerkleTree
:
require(minEndTime != 0, 'minEndTime should not be 0');
MerkleIdentity.sol
setter functionsItβs better to have events in setter functions.
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleIdentity.sol#L60-L62 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleIdentity.sol#L67-L69 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleIdentity.sol#L75-L78
vim
Add events for setter functions.
#0 - illuzen
2022-05-12T08:31:39Z
first is valid, second is duplicate
π 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.0982 DAI - $39.10
++numGates
instead of numGates += 1
Use ++numGates
instead of numGates += 1
to save gas
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleEligibility.sol#L47
Use ++numGates
instead of numGates += 1
pool.numReceipts
into memory to save gasIf storage is accessed more than once, it should be fetch into memory to save gas. pool.numReceipts
is accessed more than once in deposit()
Fetch pool.numReceipts
into memory
uint numReceipts = pool.numReceipts;
#0 - illuzen
2022-05-12T08:32:08Z
all duplicates