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: 13/71
Findings: 5
Award: $636.60
🌟 Selected for report: 1
🚀 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/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/FixedPricePassThruGate.sol#L48 https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/FixedPricePassThruGate.sol#L53
If msg.value
exceeds gate.ethCost
, the difference remains permanently stuck in the price gate because the call to the beneficiary is only gate.ethCost
.
Just like the speedBumpGate
, the call to the beneficiary should be msg.value
instead of gate.ethCost
.
(bool sent, bytes memory data) = gate.beneficiary.call{value: msg.value}("");
#0 - illuzen
2022-05-10T09:43:25Z
#1 - gititGoro
2022-06-14T03:20:21Z
A reward pool that is created with a malicious reward token (Eg. one that intentionally always returns false
on only transfer
calls) will brick user withdrawals.
Attacker creates a malicious reward token (eg. MAL
) that works normally for .transferFrom()
calls such that fundPool()
will not revert, but always returns false for .transfer()
Create a reward pool with USDC
as the deposit token and MAL
as the sole reward token
A user deposits into the pool, and after some time, attempts to withdraw his USDC
Withdrawal reverts because
IERC20(pool.rewardTokens[i]).transfer(receipt.owner, transferAmount);
will be false
Create an emergencyWithdraw()
function that allows redemption of receipts for only deposited funds (forego rewards).
#0 - illuzen
2022-05-10T09:43:55Z
#1 - gititGoro
2022-06-14T01:47:49Z
Changing severity because pool isolation and rewards tokens represent leakage not lost funds.
#2 - HickupHH3
2022-06-19T08:36:50Z
Isn't this a closer duplicate of #20 than #34?
#3 - gititGoro
2022-06-23T03:05:16Z
Isn't this a closer duplicate of #20 than #34?
Yes, you're correct. Updating to duplicate of 20. Thank you.
#4 - gititGoro
2022-06-23T03:05:28Z
duplicate of #20
🌟 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/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L193 https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/PermissionlessBasicPoolFactory.sol#L198
Certain ERC20 tokens make modifications to their ERC20's transfer
 or balanceOf
 functions. One type of these tokens is deflationary tokens that charge a certain fee for every transfer()
 or transferFrom()
.
These deflationary tokens are incompatible as a reward token, and more importantly, a deposit token for basic staking because the amount recorded (receipt.amountDepositedWei = amount;
) is less than the actual amount received. Attempts to redeem the receipt may fail because the contract could hold insufficient tokens.
One possible mitigation is to measure the asset change right before and after the asset-transferring calls.
#0 - illuzen
2022-05-10T09:47:20Z
320.671 DAI - $320.67
https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/MerkleResistor.sol#L259 https://github.com/code-423n4/2022-05-factorydao/blob/db415804c06143d8af6880bc4cda7222e5463c0e/contracts/MerkleResistor.sol#L264
It is possible for coinsPerSecond
to be zero. In these cases, the startTime
calculation
uint startTime = block.timestamp + vestingTime - (totalCoins / coinsPerSecond);
will revert from division by zero, preventing initialization, and by extension, withdrawals of vested tokens.
We assume vesting time chosen is the maximum (tree.maxEndTime
) so that totalCoins = maxTotalPayments
. These examples showcase some possibilities for which the calculated coinsPerSecond
can be zero.
pctUpFront = 99
(99% up front)totalCoins = 10_000e6
(10k USDC)vestingTime = 1 year
uint coinsPerSecond = (totalCoins * (uint(100) - tree.pctUpFront)) / (vestingTime * 100); // 10_000e6 * (100 - 99) / (365 * 86400 * 100) // = 0
pctUpFront = 0
totalCoins = 100_000e2
(100k EURS)vestingTime = 180 days
uint coinsPerSecond = (totalCoins * (uint(100) - tree.pctUpFront)) / (vestingTime * 100); // 100_000e2 * 100 / (180 * 86400 * 100) // = 0
Scale up coinsPerSecond
by PRECISION
, then scale down when executing withdrawals. While it isn’t foolproof, the possibility of coinsPerSecond
being zero is reduced significantly.
// L264 uint coinsPerSecond = (totalCoins * (uint(100) - tree.pctUpFront)) * PRECISION / (vestingTime * 100); // L184 currentWithdrawal = (block.timestamp - tranche.lastWithdrawalTime) * tranche.coinsPerSecond / PRECISION;
#0 - illuzen
2022-05-10T09:47:00Z
Example 1 = 3, not 0
Duplicate, but this mitigation is better
#1 - gititGoro
2022-06-15T00:23:50Z
Reducing severity because rewards are not staked user funds.
🌟 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.6564 DAI - $75.66
globalTaxPerCapita
bound is unchecked in constructorThe setGlobalTax()
has an exclusive upper bound of 1000 on the globalTaxPerCapita
value to be set, but this upper bound is not enforced in the constructor.
constructor(address _globalBeneficiary, uint _globalTaxPerCapita) { globalBeneficiary = _globalBeneficiary; require(_globalTaxPerCapita < 1000, 'Tax too high'); globalTaxPerCapita = _globalTaxPerCapita; }
getRewards()
if receipt has been redeemedConsider returning zero amounts if the receipt has already been redeemed.
// after L167 if (receipt.timeWithdrawn != 0) return rewardsLocal;
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/VoterID.sol#L10
Complete the missing description of the contract.
MerkleEligibility.addGate()
The other gates use the prefix operator ++numGates
, should do the same here for consistency.
#0 - illuzen
2022-05-10T15:31:09Z
L1: duplicate L2: valid L3: valid NC01: valid