Frax Ether Liquid Staking contest - Chom's results

A liquid ETH staking derivative designed to uniquely leverage the Frax Finance ecosystem.

General Information

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

Frax Finance

Findings Distribution

Researcher Performance

Rank: 6/133

Findings: 6

Award: $1,247.88

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

19.982 USDC - $19.98

Labels

bug
duplicate
2 (Med Risk)
out of scope
edited-by-warden

External Links

Lines of code

https://github.com/code-423n4/2022-09-frax/blob/55ea6b1ef3857a277e2f47d42029bc0f3d6f9173/src/frxETHMinter.sol#L190-L196

Vulnerability details

Impact

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.

Proof of Concept

/// @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

Findings Information

🌟 Selected for report: __141345__

Also found by: Bahurum, Ch_301, Chom, Respx, Trust, datapunk, ronnyx2017

Labels

bug
duplicate
2 (Med Risk)
sponsor confirmed
syncRewards sniping

Awards

128.9427 USDC - $128.94

External Links

Lines of code

https://github.com/corddry/ERC4626/blob/643cd044fac34bcbf64e1c3790a5126fec0dbec1/src/xERC4626.sol#L78-L97

Vulnerability details

Impact

syncRewards must be called on all cycles. If one cycle is missed, the reward may be miscalculated.

Proof of Concept

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

Findings Information

🌟 Selected for report: oyc_109

Also found by: 0x4non, Chom, Lambda, Respx, V_B, ladboy233, lukris02, pashov

Labels

bug
duplicate
2 (Med Risk)

Awards

103.1542 USDC - $103.15

External Links

Lines of code

https://github.com/code-423n4/2022-09-frax/blob/55ea6b1ef3857a277e2f47d42029bc0f3d6f9173/src/OperatorRegistry.sol#L114-L118

Vulnerability details

Impact

removeValidator without dont_care_about_ordering may always revert if there are too many validators. Due to gas limit.

Proof of Concept

// 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

Awards

12.4859 USDC - $12.49

Labels

bug
duplicate
2 (Med Risk)

External Links

Lines of code

https://github.com/code-423n4/2022-09-frax/blob/55ea6b1ef3857a277e2f47d42029bc0f3d6f9173/src/frxETHMinter.sol#L200

Vulnerability details

Impact

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.

Proof of Concept

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

Findings Information

🌟 Selected for report: joestakey

Also found by: Chom

Labels

bug
duplicate
2 (Med Risk)

Awards

970.5139 USDC - $970.51

External Links

Lines of code

https://github.com/code-423n4/2022-09-frax/blob/55ea6b1ef3857a277e2f47d42029bc0f3d6f9173/src/frxETHMinter.sol#L191-L196

Vulnerability details

Impact

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.

Proof of Concept

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.

  1. Create an emergency switch that pauses the entire contract (Migrate the old one to a new one)
  2. Check that emergency switch before allowing recoverEther.
  3. Add a function to withdraw staked funds from validators if the emergency switch is on.
  4. Remember to migrate frxETHToken and sfrxETHToken too.

#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

Using assembly to optimize ETH sending

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();

Use unchecked { ++i; }

for (uint256 i = 0; i < numDeposits; ++i) {

Should be

for (uint256 i = 0; i < numDeposits; ) { ... unchecked { ++i; } }

Use custom errors instead of requires

require(!depositEtherPaused, "Depositing ETH is paused");

Should be

error Paused(); ... if (depositEtherPaused) revert Paused();
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter