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: 36/71
Findings: 4
Award: $125.46
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: MaratCerby
Also found by: CertoraInc, Ruhum, VAD37, berndartmueller, broccolirob, cryptphi, danb, gzeon, horsefacts, hyh, joestakey, leastwood, pedroais, peritoflores, throttle, wuwe1, z3s
0 USDC - $0.00
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleVesting.sol#L89
Throughout your contracts, you expect every ERC20 to return a boolean value on transfers, e.g.
bool success = IERC20(pool.depositToken).transferFrom(msg.sender, address(this), amount); require(success, 'Token transfer failed');
While it's specified like that in the EIP, there are contracts that don't follow that standard. One prominent example is the USDT token: https://etherscan.io/address/0xdac17f958d2ee523a2206206994597c13d831ec7#code#L85
Because of that, there is the SafeERC20 library. There the transfer function only verifies the return value if there is one, see https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol#L82-L98
There are two transfer functions you call in your contracts:
In the case of USDT, neither of the functions returns a bool. That token will simply not be usable with your contracts. For example, in PermissionlessBasicPoolFactory
the creator of a pool won't be able to deposit the rewards tokens if one of them is USDT. But, if the return value is only missing in the token's transfer()
function you have a real problem. Users would be able to deposit their tokens since that uses transferFrom()
but won't be able to get them back from the contracts.
Here's a small test contract using foundry that highlights the issue:
pragma solidity ^0.8.12; import "forge-std/Test.sol"; import "src/IERC20.sol"; contract BrokenERC20 { function transfer(address to, uint amount) external {} } contract TestContract is Test { function testB() external { BrokenERC20 t = new BrokenERC20(); require(IERC20(address(t)).transfer(address(this), 1)); } }
testB()
will always fail because IERC20(address(t)).transfer(address(this), 1)
will always return the zero value of a bool which is false
.
Here are all the instances where the transfer functions have to be updated:
./contracts/PermissionlessBasicPoolFactory.sol:255: success = success && IERC20(pool.rewardTokens[i]).transfer(receipt.owner, transferAmount); ./contracts/PermissionlessBasicPoolFactory.sol:258: success = success && IERC20(pool.depositToken).transfer(receipt.owner, receipt.amountDepositedWei); ./contracts/PermissionlessBasicPoolFactory.sol:278: success = success && IERC20(pool.rewardTokens[i]).transfer(pool.excessBeneficiary, rewards); ./contracts/PermissionlessBasicPoolFactory.sol:296: success = success && IERC20(pool.rewardTokens[i]).transfer(globalBeneficiary, tax); ./contracts/MerkleDropFactory.sol:110: require(IERC20(tree.tokenAddress).transfer(destination, value), "ERC20 transfer failed"); ./contracts/MerkleResistor.sol:206: require(IERC20(tree.tokenAddress).transfer(destination, currentWithdrawal), 'Token transfer failed'); ./contracts/MerkleVesting.sol:173: IERC20(tree.tokenAddress).transfer(destination, currentWithdrawal); ./contracts/PermissionlessBasicPoolFactory.sol:146: success = success && IERC20(pool.rewardTokens[i]).transferFrom(msg.sender, address(this), amount); ./contracts/PermissionlessBasicPoolFactory.sol:213: bool success = IERC20(pool.depositToken).transferFrom(msg.sender, address(this), amount); ./contracts/MerkleDropFactory.sol:79: require(IERC20(merkleTree.tokenAddress).transferFrom(msg.sender, address(this), value), "ERC20 transfer failed"); ./contracts/MerkleResistor.sol:123: require(IERC20(merkleTree.tokenAddress).transferFrom(msg.sender, address(this), value), "ERC20 transfer failed"); ./contracts/MerkleVesting.sol:89: require(IERC20(merkleTree.tokenAddress).transferFrom(msg.sender, address(this), value), "ERC20 transfer failed");
none
Use SafeERC20's transfer functions
#0 - illuzen
2022-05-10T07:54:34Z
#1 - gititGoro
2022-06-14T01:43:45Z
#2 - gititGoro
2022-06-14T01:44:45Z
Changing severity due pool isolation, rewards tokens represent leakage not lost funds and because transfer of deposited funds would fail on deposit.
🌟 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#L194-L198 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L144-L146 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleDropFactory.sol#L73-L77 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleVesting.sol#L85-L89
PermissionlessBasicPoolFactory contract not handling fee-on-transfer tokens properly can cause a loss of funds for users.
Since anybody can create a pool and decide which tokens are supposed to be used for the deposits and the reward, there's the possibility of someone creating one with a token that takes fees on transfer. In that case, the internal balance of the contract would be wrong. It would think that it had more tokens than it actually does. When users start withdrawing their deposits & rewards, the person who withdraws last will not be able to withdraw their deposit & reward.
Because of that, I'd rate this issue HIGH. Especially considering the fact that the time gap between deposits and the first withdrawal will be quite large. The pool could fill up before users realize that the contract can't handle fee-on-transfer tokens.
NOTE: the same problem exists for all the other contracts transferring ERC20 tokens. But, in those cases, the user should be able to save funds by modifying the Merkle tree.
When depositing tokens, the token doesn't verify that it received the correct amount: https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L194-L198
When withdrawing, it uses the value stored when the user deposited: https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L233
If it's a fee-on-transfer token that might be more than the contract actually owns.
Basic example:
amountDepositedWei
. The transaction fails because the contract doesn't have 1e18 tokens. Alice can't withdraw her funds.none
Store the actual token amount the contract receives, e.g.:
uint prevBalance = IERC20(pool.depositToken).balanceOf(address(this)); IERC20(pool.depositToken).transferFrom(msg.sender, address(this), amount); uint postBalance = IERC20(pool.depositToken).balanceOf(address(this)); receipt.amountDepositedWei = postBalance - prevBalance;
#0 - illuzen
2022-05-10T07:53:48Z
#1 - gititGoro
2022-06-14T01:41:58Z
Reducing severity as this is a funds leakage issue and because pools are isolated.
🌟 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
83.247 DAI - $83.25
Whenever you make a change to the contract's configuration there should be an event.
Here are a couple functions where that's missing:
Consider using a two-step process for transferring the ownership of a contract. While it costs a little more gas, it's safer than transferring directly.
Here's an example from the Compound Timelock contract: https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol#L45-L58
The contract isn't fully compliant with the ERC721-specs.
balanceOf()
should throw if an invalid address is passed.
tokenURI()
should throw if an invalid tokenID is passed.
#0 - illuzen
2022-05-10T07:57:51Z
Configuration events are highly debatable. I have not ever used them, even once, on my or others contracts. Two step is also debatable, incorrectly entering function arguments is not in scope, unless it's considered code style.
ERC721 spec issue is inconsequential, but valid.
🌟 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.0273 DAI - $39.03
for (uint i = 0; i < pool.rewardTokens.length; i++) { // ... } // change to uint length = pool.rewardTokens.length; for (uint i = 0; i < length; i++) { // ... }
#0 - illuzen
2022-05-10T07:58:10Z
Duplicate