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: 3/71
Findings: 5
Award: $2,984.74
π 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/main/contracts/FixedPricePassThruGate.sol#L48 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/FixedPricePassThruGate.sol#L53
The FPPT gate allows receiving more than gate.ethCost
.
But when forwarding the funds to the beneficiary, it is sending only gate.ethCost
.
There is no other way to retrieve funds from the contract and so msg.value-gate.ethCost
will be locked.
Note: the SpeedBumpPriceGate does send msg.value
to the beneficiary and not just the price.
Excess ether can be sent to the contract, but it will be locked. Beneficiary will lose funds.
The gate receives ether and requires that it is enough:
require(msg.value >= gate.ethCost, 'Please send more ETH');
And it then sends gate.ethCost
, and not the full msg.value
:
(bool sent, bytes memory data) = gate.beneficiary.call{value: gate.ethCost}("");
Therefore, msg.value-gate.ethCost
will be locked in the contract.
As I mentioned above, the SpeedBump gate does send the whole msg.value
.
Send the whole msg.value
to the beneficary.
#0 - illuzen
2022-05-10T15:32:13Z
#1 - gititGoro
2022-06-14T03:20:31Z
#2 - gititGoro
2022-06-14T03:21:06Z
Upgrading severity as user funds lost
π Selected for report: kenzo
2639.2674 DAI - $2,639.27
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleVesting.sol#L173
Across the codebase, the protocol is usually checking that ERC20 transfers have succeeded by checking their return value.
This check is missing in MerkleVesting's withdraw
function.
If for some reason the ERC20 transfer is temporarily failing, the user would totally lose his allocation and funds.
All the state variables would already have been updated at this stage, so he can't call withdraw
again.
There is no way to withdraw these locked tokens.
At the last point of withdraw
, the function is sending the funds to the user, and does not check the return value - whether it has succeeded:
IERC20(tree.tokenAddress).transfer(destination, currentWithdrawal);
Note that this is (nicely and rightfully) done after all the state variables have been updated. As the return value of the external call is not checked, if it has failed, the contract wouldn't know about it, and the function will finish "successfully".
As done throughout the rest of the protocol, add a check that verifies that the transfer has succeeded.
#0 - illuzen
2022-05-10T15:49:06Z
Debatable, since requiring successful transfer means we can't do non-standard tokens like USDT. Also, tokens could be malicious and simply lie about the success.
#1 - gititGoro
2022-06-15T00:46:11Z
Regarding the non standard tokens that don't return bools, the common approach to performing a low level call with
(bool success, _) = address(token).call(//etc
allows for transfers to be validated for USDT.
Severity will stand because this function represents user funds.
https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L230 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L233 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L252 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L269
In PermissionlessBasicPoolFactory, the withdrawal functions only succeed if the transfer has succeeded in all tokens.
If a reward token's transfer has not succeeded for some reason, no other tokens can be withdrawn either. This does not only mean that rewards will be lost; At worst, this means a user's deposit will be locked - as the withdrawal of it is dependent on the withdrawal of the reward tokens.
The withdraw
function is iterating over all tokens in a loop:
for (uint i = 0; i < rewards.length; i++) { ... 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');
By looking at the success
variable and condition, we can see that the function will succeed, and deposit sent back to the user, only if all token transfers have been successful.
Therefore, one bad apple will spoil the bunch, and at worst make users funds locked.
A similar situation is happening in withdrawExcessRewards
and withdrawTaxes
.
Consider moving to a system where as a fallback, the user can choose which specific rewards/deposit to withdraw. This only needs 1 additional storage slot - you can use 1 uint to remember which rewards have already been withdrawn (rewardTokens[0] is the first bit in the uint, rewardTokens[1] the second, etc').
#0 - illuzen
2022-05-11T09:20:17Z
Duplicate #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/main/contracts/MerkleDropFactory.sol#L77 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleDropFactory.sol#L107 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleResistor.sol#L121 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleResistor.sol#L204 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/MerkleVesting.sol#L89 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L144 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L198 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L230 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L233 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L252 https://github.com/code-423n4/2022-05-factorydao/blob/main/contracts/PermissionlessBasicPoolFactory.sol#L269
The protocol checks that ERC20 transfers have succeeded using require(success)
.
But some tokens (like USDT) do not return true on transfer success.
The protocol, which is supposed to be general purpose, will not work with these tokens. While it seems unlikely that somebody will decide to reward stakers with USDT, it might still be an option, which is not working at the moment.
Consider using SafeERC20 for IERC20
.
#0 - illuzen
2022-05-10T15:32:31Z
The reward calculation functions implicitly assume that the staking token has 18 decimals.
Incorrect amount of rewards given. As the FactoryDAO suite is supposed to be a general solution, this will cause incompatibility with certain tokens.
The staking pool calculates the amount of rewards in the following way:
rewardsLocal[i] = (secondsDiff * pool.rewardsWeiPerSecondPerToken[i] * receipt.amountDepositedWei) / 1e18;
Let's take an example:
He would have set rewardsWeiPerSecondPerToken
to be 1e18 (=1 REWARD).
Let's say Hagbard has deposited 1 STAKE token and after a second is withdrawing his reward.
His reward will be calculated to be 1 * 1e18 * 1e9 / 1e18
= 1e9.
But as we saw, the owner intended him to get 1e18 tokens, not 1e9.
So wrong amount is given.
Instead of dividing the calculation by 1e18:
decimals()
Note: getMaximumRewards
also uses the problematic 1e18 division.
#0 - illuzen
2022-05-11T09:52:56Z
Duplicate #60
#1 - gititGoro
2022-06-14T03:17:49Z