Platform: Code4rena
Start Date: 22/09/2022
Pot Size: $30,000 USDC
Total HM: 12
Participants: 133
Period: 3 days
Judge: 0xean
Total Solo HM: 2
Id: 165
League: ETH
Rank: 6/133
Findings: 6
Award: $1,247.88
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: ladboy233
Also found by: 0xSmartContract, 8olidity, Aymen0909, Chom, IllIllI, OptimismSec, PaludoX0, TomJ, ayeslick, cccz, csanuragjain, joestakey, neko_nyaa, pashov, peritoflores, rbserver, rvierdiiev
19.982 USDC - $19.98
Governor can rug all unstaked ether in contract using recoverEther. This causes fund loss and state failures. A case of failure is frxETH will become unbacked by real staked ETH since it is rugged by an admin.
/// @notice For emergencies if something gets stuck function recoverEther(uint256 amount) external onlyByOwnGov { (bool success,) = address(owner).call{ value: amount }(""); require(success, "Invalid transfer"); emit EmergencyEtherRecovered(amount); }
recoverEther is not checking for active ETH supply. Admin can withdraw all ETH in the contract.
recoverEther should be able to recover only fund that over the remaining supply = total supply of frxETH - deposited amount - currentWithheldETH
#0 - FortisFortuna
2022-09-25T21:13:41Z
We are well aware of the permission structure. The owner will most likely be a large multisig. We mentioned the Frax Multisig in the scope too.
#1 - 0xean
2022-10-13T23:59:04Z
dupe of #107
🌟 Selected for report: __141345__
Also found by: Bahurum, Ch_301, Chom, Respx, Trust, datapunk, ronnyx2017
128.9427 USDC - $128.94
syncRewards must be called on all cycles. If one cycle is missed, the reward may be miscalculated.
function syncRewards() public virtual { uint192 lastRewardAmount_ = lastRewardAmount; uint32 timestamp = block.timestamp.safeCastTo32(); if (timestamp < rewardsCycleEnd) revert SyncError(); uint256 storedTotalAssets_ = storedTotalAssets; uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_; storedTotalAssets = storedTotalAssets_ + lastRewardAmount_; // SSTORE uint32 end = ((timestamp + rewardsCycleLength) / rewardsCycleLength) * rewardsCycleLength; // Combined single SSTORE lastRewardAmount = nextRewards.safeCastTo192(); lastSync = timestamp; rewardsCycleEnd = end; emit NewRewardsCycle(end, nextRewards); }
lastRewardAmount is the amount of rewards distributed in the most recent cycle. If a cycle is skipped, the lastRewardAmount of skipped cycle won't be calculated. It May cause reward lost if asset balance is high and cause reward to be higher than expected if asset balance is low.
Ensure that this function will always run on every cycle.
#0 - FortisFortuna
2022-09-26T17:13:03Z
From @denett syncRewards should be called by us at the beginning of each period, or we need to automatically call it before deposits/withdrawals.
#1 - FortisFortuna
2022-09-26T17:30:14Z
removeValidator without dont_care_about_ordering may always revert if there are too many validators. Due to gas limit.
// Fill the new validators array with all except the value to remove for (uint256 i = 0; i < original_validators.length; ++i) { if (i != remove_idx) { validators.push(original_validators[i]); } }
These lines in the OperatorRegistry contract will be reverted with the gas limit if the original_validators count is too high
Limit the number of validators
#0 - FortisFortuna
2022-09-25T22:48:15Z
We plan to keep an eye on the number free validators and have a decent sized buffer of them.
#1 - joestakey
2022-09-26T16:52:02Z
Duplicate of #12
🌟 Selected for report: Lambda
Also found by: 0x1f8b, 0x5rings, 0xSky, 0xSmartContract, 8olidity, Chom, CodingNameKiki, IllIllI, Ruhum, Sm4rty, brgltd, hansfriese, m9800, magu, pashov, pedroais, peritoflores, prasantgupta52, rokinot, seyni
12.4859 USDC - $12.49
recoverERC20 is using an unsafe transfer to transfer the token. Combined with require, this cause USDT to be completely non-recoverable. This is a fund loss.
USDT transfer function doesn't return anything as seen in https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code
// Forward ERC20 methods to upgraded contract if this one is deprecated function transfer(address _to, uint _value) public whenNotPaused { require(!isBlackListed[msg.sender]); if (deprecated) { return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value); } else { return super.transfer(_to, _value); } }
If tokenAddress is USDT, IERC20(tokenAddress).transfer(owner, tokenAmount)
will return nothing and will always revert with "recoverERC20: Transfer failed" due to not returning anything.
require(IERC20(tokenAddress).transfer(owner, tokenAmount), "recoverERC20: Transfer failed");
Use safeTransfer from the SafeERC20 library. Remind to remove required statement too since safeTransfer may not return boolean success but it has already checked for transfer success.
IERC20(tokenAddress).safeTransfer(owner, tokenAmount);
#0 - FortisFortuna
2022-09-25T21:36:35Z
Not really medium risk. Technically you could use safeTransfer, but if someone were to accidentally send something to this contract, it would most likely be either ETH, FRAX, frxETH, or sfrxETH, all of which are transfer compliant.
#1 - joestakey
2022-09-26T16:45:53Z
Duplicate of #18
recoverEther will cause currentWithheldETH to be wrong. Further deposits will lose a lot of ETH!
For example, if the admin withdraws all ETH using recoverEther. Assume we have 1024 ETH in the currentWithheldETH. After that, somebody deposits 64 ETH. This 64 ETH will cover the currentWithheldETH bad debt instead of can be used to stake to the validators. You need to waste 1024 ETH to cover the currentWithheldETH bad debt.
Assume we have 1024 ETH in the currentWithheldETH
function recoverEther(uint256 amount) external onlyByOwnGov { (bool success,) = address(owner).call{ value: amount }(""); require(success, "Invalid transfer"); emit EmergencyEtherRecovered(amount); }
Admin can recover all ether as it doesn't check against currentWithheldETH. Assume admin withdraws all ethers.
currentWithheldETH is still the same since it hasn't been set.
Assume someone deposits 64 ETH after admin withdrawal.
function depositEther() external nonReentrant { // Initial pause check require(!depositEtherPaused, "Depositing ETH is paused"); // See how many deposits can be made. Truncation desired. uint256 numDeposits = (address(this).balance - currentWithheldETH) / DEPOSIT_SIZE; require(numDeposits > 0, "Not enough ETH in contract"); // Give each deposit chunk to an empty validator for (uint256 i = 0; i < numDeposits; ++i) { // Get validator information ( bytes memory pubKey, bytes memory withdrawalCredential, bytes memory signature, bytes32 depositDataRoot ) = getNextValidator(); // Will revert if there are not enough free validators // Make sure the validator hasn't been deposited into already, to prevent stranding an extra 32 eth // until withdrawals are allowed require(!activeValidators[pubKey], "Validator already has 32 ETH"); // Deposit the ether in the ETH 2.0 deposit contract depositContract.deposit{value: DEPOSIT_SIZE}( pubKey, withdrawalCredential, signature, depositDataRoot ); // Set the validator as used so it won't get an extra 32 ETH activeValidators[pubKey] = true; emit DepositSent(pubKey, withdrawalCredential); } }
address(this).balance - currentWithheldETH = 64 - 1024
will be minus. We need to deposit 1024 ETH back to make it not a minus.
Emergency should be limited to only after toggling an emergency switch. The emergency switch should pause the entire contract.
#0 - FortisFortuna
2022-09-25T23:51:36Z
We are well aware of the permission structure. The owner will most likely be a large multisig. We mentioned the Frax Multisig in the scope too. If moving funds, it is assumed someone in the multisig would catch an invalid or malicious address.
#1 - joestakey
2022-09-26T17:23:13Z
Duplicate of #346
🌟 Selected for report: pfapostol
Also found by: 0x040, 0x1f8b, 0x4non, 0x5rings, 0xA5DF, 0xNazgul, 0xSmartContract, 0xmatt, 0xsam, Amithuddar, Aymen0909, B2, Ben, Bnke0x0, Chom, CodingNameKiki, Deivitto, Diana, Fitraldys, Funen, IllIllI, JAGADESH, JC, Metatron, Ocean_Sky, PaludoX0, Pheonix, RaymondFam, ReyAdmirado, RockingMiles, Rohan16, Rolezn, Satyam_Sharma, Sm4rty, SnowMan, SooYa, Tagir2003, TomJ, Tomio, Triangle, V_B, Waze, __141345__, ajtra, albincsergo, asutorufos, aysha, beardofginger, bobirichman, brgltd, bulej93, bytera, c3phas, ch0bu, cryptostellar5, cryptphi, d3e4, delfin454000, dharma09, drdr, durianSausage, emrekocak, erictee, fatherOfBlocks, gogo, got_targ, imare, jag, karanctf, ladboy233, leosathya, lukris02, medikko, mics, millersplanet, natzuu, neko_nyaa, oyc_109, peanuts, prasantgupta52, rbserver, ret2basic, rokinot, ronnyx2017, rotcivegaf, sach1r0, samruna, seyni, slowmoses, tnevler, wagmi, zishansami
12.8108 USDC - $12.81
Change
(bool success,) = payable(to).call{ value: amount }(""); require(success, "Invalid transfer");
to
bool success; assembly { // Transfer the ETH and store if it succeeded or not. success := call(gas(), to, amount, 0, 0, 0, 0) } if(!success) revert ETH_TRANSFER_FAILED();
for (uint256 i = 0; i < numDeposits; ++i) {
Should be
for (uint256 i = 0; i < numDeposits; ) { ... unchecked { ++i; } }
require(!depositEtherPaused, "Depositing ETH is paused");
Should be
error Paused(); ... if (depositEtherPaused) revert Paused();