Frax Ether Liquid Staking contest - Lambda'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: 3/133

Findings: 5

Award: $2,123.84

🌟 Selected for report: 3

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Lambda

Also found by: Critical, bin2chen

Labels

bug
question
3 (High Risk)
primary issue
sponsor confirmed
syncRewards wrong nextRewards

Awards

1941.0279 USDC - $1,941.03

External Links

Lines of code

https://github.com/code-423n4/2022-09-frax/blob/dc6684f77b4e9bd965e8862be7f5fb71473a4c4c/src/sfrxETH.sol#L50

Vulnerability details

Impact

sfrxETH.beforeWithdraw first calls the beforeWithdraw of xERC4626, which decrements storedTotalAssets by the given amount. If the timestamp is greater than the rewardsCycleEnd, syncRewards is called. However, the problem is that the assets have not been transfered out yet, meaning asset.balanceOf(address(this)) still has the old value. On the other hand, storedTotalAssets was already updated. Therefore, the following calculation will be inflated by the amount for which the withdrawal was requested:

uint256 nextRewards = asset.balanceOf(address(this)) - storedTotalAssets_ - lastRewardAmount_;

This has severe consequences: 1.) During the following reward period, lastRewardAmount is too high, which means that too much rewards are paid out too users who want to withdraw. A user could exploit this to steal the assets of other users. 2.) When syncRewards() is called the next time, it is possible that the nextRewards calculation underflows because lastRewardAmount > asset.balanceOf(address(this)). This is very bad because syncRewards() will be called in every withdrawal (after the rewardsCycleEnd) and none of them will succeed because of the underflow. Depositing more also does not help here, it just increases asset.balanceOf(address(this)) and storedTotalAssets by the same amount, which does not eliminate the underflow.

Note that this bug does not require a malicious user or a targeted attack to surface. It can (and probably will) happen in practice just by normal user interactions with the vault (which is for instance shown in the PoC).

Proof Of Concept

Consider the following test:

function testTotalAssetsAfterWithdraw() public { uint128 deposit = 1 ether; uint128 withdraw = 1 ether; // Mint frxETH to this testing contract from nothing, for testing mintTo(address(this), deposit); // Generate some sfrxETH to this testing contract using frxETH frxETHtoken.approve(address(sfrxETHtoken), deposit); sfrxETHtoken.deposit(deposit, address(this)); require(sfrxETHtoken.totalAssets() == deposit); vm.warp(block.timestamp + 1000); // Withdraw frxETH (from sfrxETH) to this testing contract sfrxETHtoken.withdraw(withdraw, address(this), address(this)); vm.warp(block.timestamp + 1000); sfrxETHtoken.syncRewards(); require(sfrxETHtoken.totalAssets() == deposit - withdraw); }

This is a normal user interaction where a user deposits into the vault, and makes a withdrawal some time later. However, at this point the syncRewards() within the beforeWithdraw is executed. Because of that, the documented accounting mistake happens and the next call (in fact every call that will be done in the future) to syncRewards() reverts with an underflow.

Call syncRewards() before decrementing storedTotalAssets, i.e.:

function beforeWithdraw(uint256 assets, uint256 shares) internal override { if (block.timestamp >= rewardsCycleEnd) { syncRewards(); } super.beforeWithdraw(assets, shares); // call xERC4626's beforeWithdraw AFTER }

Then, asset.balanceOf(address(this)) and storedTotalAssets are still in sync within syncRewards().

#0 - FortisFortuna

2022-09-26T15:57:58Z

Does this only occur if all users try to withdraw at the exact same time? If so, this is a known bug by us and the risk would be low in a real-life deployment scenario. We can also let the users know about the ramping of the rewards.

#1 - OpenCoreCH

2022-09-27T18:49:14Z

I do not think that this is a duplicate of #311. #311 (and the other issues that are linked there) describe a recoverable issue where the withdrawal for the last user fails (which was listed as a known issue of xERC4626) until the cycle ends.

The issue here that is described here and demonstrated in the PoC is a non-recoverable sfrxETH-specific issue (because sfrxETH potentially calls syncRewards() in the beforeWithdraw function) where withdrawals even fail after the cycle has ended. It also does not require all users to withdraw at the same time.

#2 - FortisFortuna

2022-09-27T20:29:32Z

#3 - OpenCoreCH

2022-09-27T20:32:09Z

@FortisFortuna Good catch did not see that, yes #24 addresses the same issue

#4 - FortisFortuna

2022-09-27T20:33:04Z

@OpenCoreCH I will mark yours as primary because it is better documented.

#5 - corddry

2022-09-29T21:49:19Z

Here's the proposed fix, which instead moves the syncRewards call to a modifier, so that it actually occurs before the withdraw instead of in beforeWithdraw. It also adds it to the other 4626 withdraw/redeem functions. Would appreciate feedback if you have any

https://github.com/FraxFinance/frxETH-public/pull/2/commits/1ec457c7f5faed618971fb29b9bcc6d54453b093

#6 - OpenCoreCH

2022-09-29T21:59:28Z

The modifier is currently missing for mint and redeem, is that on purpose? Otherwise, it looks good to me

#7 - corddry

2022-09-29T22:16:39Z

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/dc6684f77b4e9bd965e8862be7f5fb71473a4c4c/src/ERC20/ERC20PermitPermissionedMint.sol#L84

Vulnerability details

Impact

No elements are ever deleted in minters_array within ERC20PermitPermissionedMint, they are only set to address(0). When there are a lot of minters, it is therefore possible that this array will grow too large at one point and all iterations over it will run out of gas. The consequences of this would be quite severe. It would no longer be possible for governance to call removeMinter, as this function iterates over the whole array and therefore would always run out of gas. A malicious minter could therefore no longer be removed.

Proof Of Concept

There were a lot of fluctuations for the minters during the first 5 years of frxETH because frxETHMinter was redeployed a lot and the design was changed such that there are multiple minters with different addresses. In total, 600 minters were added, but 550 were deleted again. Because of this, minters_array contains 600 entries, but 550 of them are address(0). A new minter contract is added, but it turns out that the contract has a critical vulnerability, allowing anyone to mint tokens for free. Therefore, governance decides to remove the minter in an emergency proposal. Because of the position of this minter, the call always returns out of gas and removal does not succeed.

In my opinion, there is no reason to keep the indices within the array the same. Therefore, you could also completely delete an element (swap it with the last array element, pop the array) instead of setting the addresses to zero.

#0 - FortisFortuna

2022-09-25T23:05:58Z

Technically correct, but in practice, the number of minters will always remain low. If it becomes an issue, we can designate one minter as a "pre-minter" that has a batch of tokens minted to it beforehand, then auxiliary contracts can connect to that instead of ERC20PermitPermissionedMint.sol instead.

#1 - 0xean

2022-10-13T21:41:54Z

dupe of #12

Findings Information

🌟 Selected for report: Lambda

Also found by: 0x52, Bahurum, Bnke0x0, KIntern_NA, Respx, Soosh, TomJ, Trust, V_B, lukris02, rbserver, rotcivegaf, yixxas

Labels

bug
2 (Med Risk)
in discussion
primary issue
sponsor confirmed
depositEther OOG

Awards

39.1574 USDC - $39.16

External Links

Lines of code

https://github.com/code-423n4/2022-09-frax/blob/dc6684f77b4e9bd965e8862be7f5fb71473a4c4c/src/frxETHMinter.sol#L129

Vulnerability details

Impact

frxETHMinter.depositEther always iterates over all deposits that are possible with the current balance ((address(this).balance - currentWithheldETH) / DEPOSIT_SIZE). However, when a lot of ETH was deposited into the contract / it was not called in a long time, this loop can reach the gas limit. When this happens, no more calls to depositEther are possible, as it will always run out of gas.

Of course, the probability that such a situation arises depends on the price of ETH. For >1,000 USD it would need require someone to deposit a large amount of money (which can also happen, there are whales with thousands of ETH, so if one of them would decide to use frxETH, the problem can arise). For lower prices, it can happen even for small (in dollar terms) deposits. And in general, the correct functionality of a protocol should not depend on the price of ETH.

Proof Of Concept

Jerome Powell continues to rise interest rates, he just announced the next rate hike to 450%. The crypto market crashes, ETH is at 1 USD. Bob buys 100,000 ETH for 100,000 USD and deposits them into frxETHMinter. Because of this deposit, numDeposit within depositEther is equal to 3125. Therefore, every call to the function runs out of gas and it is not possible to deposit this ETH into the deposit contract.

It should be possible to specify an upper limit for the number of deposits such that progress is possible, even when a lot of ETH was deposited into the contract.

#0 - FortisFortuna

2022-09-26T16:31:57Z

Adding a maxLoops parameter or similar can help mitigate this for sure.

#1 - 0xean

2022-10-11T21:29:22Z

Warden(s) fail to demonstrate how this leads to a loss of funds which would be required for High Severity. This does however lead directly to emergency failover's having to be called to remove the now stuck ETH, and ultimately impairs the functionality and availability of the protocol, so M severity is appropriate.

Awards

12.4859 USDC - $12.49

Labels

bug
2 (Med Risk)

External Links

Lines of code

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

Vulnerability details

Impact

There is a function recoverERC20 to rescue any ERC20 tokens that were accidentally sent to the contract. However, there are tokens that do not return a value on success, which will cause the call to revert, even when the transfer would have been succesful. This means that those tokens will be stuck forever and not recoverable.

Proof Of Concept

Someone accidentally transfers USDT, one of the most commonly used ERC20 tokens, to the contract. Because USDT's transfer does not return a boolean, it will not be possible to recover those tokens and they will be stuck forever.

Use OpenZeppelin's safeTransfer.

#0 - FortisFortuna

2022-09-25T21:30:25Z

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 - 0xean

2022-10-11T21:46:55Z

I think this qualifies as a M risk. Sponsor has created functionality to recover erc20 tokens. Wardens have shown a path to which this functionality does not work correctly.

2 — Med: Assets not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.

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