GithubHelp home page GithubHelp logo

2023-03-asymmetry-findings's People

Contributors

c4-judge avatar code423n4 avatar itsmetechjay avatar kartoonjoy avatar liveactionllama avatar paroxism avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

2023-03-asymmetry-findings's Issues

Potential DoS attack due to expensive operation during contract deployment

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L182

Vulnerability details

Impact

During the contract deployment, the addDerivative function iterates over all existing derivatives to calculate the total weight, which can become an expensive operation as the number of derivatives increases. An attacker could potentially exploit this by adding a large number of derivatives to the contract, causing the deployment transaction to fail or run out of gas, leading to a denial-of-service (DoS) attack.

Proof of Concept

function addDerivative(
address _contractAddress,
uint256 _weight
) external onlyOwner {
derivatives[derivativeCount] = IDerivative(_contractAddress);
weights[derivativeCount] = _weight;
derivativeCount++;

uint256 localTotalWeight = 0;
for (uint256 i = 0; i < derivativeCount; i++)
    localTotalWeight += weights[i];
totalWeight = localTotalWeight;
emit DerivativeAdded(_contractAddress, _weight, derivativeCount);

}
If the number of derivatives in the contract is large, the for loop that iterates over all derivatives to calculate the total weight can become an expensive operation, leading to a DoS attack during contract deployment.

Tools Used

Manual Review

Recommended Mitigation Steps

One possible solution to mitigate this vulnerability is to implement a limit on the number of derivatives that can be added to the contract. For Emergency situation add deny function for derivatives

DOS - User funds stuck permanently

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L182

Vulnerability details

Impact

If admin mistakenly adds a incorrect derivative contract, then all major contract functionality will permanently fail and User funds will stuck permanently

Proof of Concept

  1. Admin wants to add a new derivative using addDerivative function
function addDerivative(
        address _contractAddress,
        uint256 _weight
    ) external onlyOwner {
        derivatives[derivativeCount] = IDerivative(_contractAddress);
        weights[derivativeCount] = _weight;
        derivativeCount++;
        ...
    }
  1. Admin wants to use contract address as 0xabc but mistakenly uses 0xabd
  2. A new derivative with contract address 0xabd gets created
  3. After sometime Admin wants to rebalance weights using rebalanceToWeights which fails since derivatives[i].balance() does not exist for 0xabd contract
function rebalanceToWeights() external onlyOwner {
        uint256 ethAmountBefore = address(this).balance;
        for (uint i = 0; i < derivativeCount; i++) {
            if (derivatives[i].balance() > 0)
                derivatives[i].withdraw(derivatives[i].balance());
        }
...
}
  1. Similarly user stake and unstake operation also fails due to same reason
function unstake(uint256 _safEthAmount) external {
        ...

        for (uint256 i = 0; i < derivativeCount; i++) {
            // withdraw a percentage of each asset based on the amount of safETH
            uint256 derivativeAmount = (derivatives[i].balance() *
                _safEthAmount) / safEthTotalSupply;
            if (derivativeAmount == 0) continue; // if derivative empty ignore
            derivatives[i].withdraw(derivativeAmount);
        }

Recommended Mitigation Steps

Allow admin to remove a derivative, which will prevent such scenarios

Missing deadline checks allow pending transactions to be maliciously executed

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L83-L102

Vulnerability details

Impact

AMMs should provide their users with an option to limit the execution of their pending actions, such as swaps or adding and removing liquidity. The most common solution is to include a deadline timestamp as a parameter (for example see Uniswap V2). If such an option is not present, users can unknowingly perform bad trades

Proof of Concept

Alice wants to swap 1 WETH for 1 RETH and later sell the 1 RETH for 1000 DAI. She signs the transaction calling Pair.sell with inputAmount = 1 WETH and minOutputAmount = 0.99 RETH to allow for some slippage.
The transaction is submitted to the mempool, however, Alice chose a transaction fee that is too low for miners to be interested in including her transaction in a block. The transaction stays pending in the mempool for extended periods, which could be hours, days, weeks, or even longer.
When the average gas fee dropped far enough for Alice’s transaction to become interesting again for miners to include it, her swap will be executed. In the meantime, the price of RETH could have drastically changed. She will still at least get 0.99 RETH due to minOutputAmount, but the DAI value of that output might be significantly lower. She has unknowingly performed a bad trade due to the pending transaction she forgot about.
An even worse way this issue can be maliciously exploited is through MEV:

The swap transaction is still pending in the mempool. Average fees are still too high for miners to be interested in it. The price of RETH has gone up significantly since the transaction was signed, meaning Alice would receive a lot more ETH when the swap is executed. But that also means that her minOutputAmount value is outdated and would allow for significant slippage.
A MEV bot detects the pending transaction. Since the outdated minOutputAmount now allows for high slippage, the bot sandwiches Alice, resulting in significant profit for the bot and significant loss for Alice.

Tools Used

Manual review

Recommended Mitigation Steps

Introduce a deadline parameter to the mentioned functions.

Oracle price can be better secured (freshness + tamper-resistance)

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L111

Vulnerability details

SfrxEth contract is a derivative contract that enables the conversion of Frax's staked derivative sfrxETH into ETH. The contract relies on the Frax protocol for the conversion of sfrxETH into frxETH, and then the curve pool to convert frxETH into ETH.

The contract includes a function called ethPerDerivative that calculates the current price of the sfrxETH derivative in ETH. The calculation depends on the price oracle of the frxETH-ETH curve pool, which is returned by the price_oracle function of the pool contract.

Impact

The ethPerDerivative function relies on the accuracy of the price oracle returned by the curve pool contract. If the price oracle is manipulated or inaccurate, then the conversion rate from sfrxETH to ETH will be affected, leading to potential losses for users.

The Frax protocol provides a mechanism for the conversion of sfrxETH into frxETH, which introduces an additional layer of risk if the protocol is not secure or trustworthy.

Recommended Mitigation Steps

To mitigate the risk of inaccurate or manipulated price oracles, we recommend the following steps:

Use a price oracle, such as Chainlink oracles, can provide greater security and resistance to manipulation.

Implement a time-based limit on the price oracle: The ethPerDerivative function can be modified to include a check that ensures the price oracle was updated within a certain time limit. For example:

uint256 lastPriceUpdate = IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).lastUpdateTime();
require(block.timestamp - lastPriceUpdate <= 1 hours, 'stale price');
This will prevent the use of outdated or manipulated price oracles.

Implement a price deviation limit: The ethPerDerivative function can be modified to include a check that ensures the last reported price of the curve pool is not too far from the current price oracle. For example:

uint256 lastPrice = IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).getPriceCumulativeLast();
uint256 oraclePrice = IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle();
uint256 percentDiff;
// require difference in prices to be within 5%
if (lastPrice > oraclePrice) {
percentDiff = (lastPrice - oraclePrice) * 1e18 / oraclePrice;
} else {
percentDiff = (oraclePrice - lastPrice) * 1e18 / oraclePrice;
}
require(percentDiff <= 5e16, 'volatile market');
This will prevent the use of price oracles that have deviated too far from the current value.

unstake() will revert if RocketPool deposit pool is empty, causing all funds to be stuck

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L108
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L118

Vulnerability details

Impact

The withdraw function of the reth derivative contract assumes it can always burn rETH for ETH. This is not the case. If Rocket Pool's deposit pool is empty, rETH.burn() will revert. As a result the derivative's withdraw() will revert (See note in RP docs). Subsequently SafETH.unstake() will also revert. This means that no user will be able to unstake their SafETH as long as Rocket Pool's deposit pool is empty (as seen here this can be an extended period of time (e.g., 5 months).

(note: rebalanceToWeights() will also fail)

Proof of Concept

  1. User deposits 50 ETH into SafETH
  2. Assume 20 ETH (of the 50) is swapped into rETH
  3. Rocket Pool uses the 20 ETH to create a minipool.
  4. Rocket Pool's deposit pool now only has 4 ETH
  5. User attempts to unstake his SafETH
  6. We attempt to burn 20ETH worth of rETH -> reverts since only 4 ETH is available

Recommended Mitigation Steps

  • Sell rETH on a DEX if rETH is unable to be burned.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a 👍 emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

Stake excessive ETH not refunded to user

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L88-L91

Vulnerability details

Impact

From the stake function, we can see that if a user stakes 1 ETH, it will be divided by the number of derivatives and transferred to different derivative accounts based on the weights. However, the remaining balance of the ETH will not be returned to the user, but will be kept in the contract.

The unused balance of the ETH depends on the number of derivatives and their respective weights.

Proof of Concept

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L88-L91

function stake() external payable {
        require(pauseStaking == false, "staking is paused");
        require(msg.value >= minAmount, "amount too low");
        require(msg.value <= maxAmount, "amount too high");

        uint256 underlyingValue = 0;

        // Getting underlying value in terms of ETH for each derivative
        for (uint i = 0; i < derivativeCount; i++)
            underlyingValue +=
                (derivatives[i].ethPerDerivative(derivatives[i].balance()) *
                    derivatives[i].balance()) /
                10 ** 18;

        uint256 totalSupply = totalSupply();
        uint256 preDepositPrice; // Price of safETH in regards to ETH
        if (totalSupply == 0)
            preDepositPrice = 10 ** 18; // initializes with a price of 1
        else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;

        uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system
        for (uint i = 0; i < derivativeCount; i++) {
            uint256 weight = weights[i];
            IDerivative derivative = derivatives[i];
            if (weight == 0) continue;
            uint256 ethAmount = (msg.value * weight) / totalWeight;

            // This is slightly less than ethAmount because slippage
            uint256 depositAmount = derivative.deposit{value: ethAmount}(); //@audit no return rest of funds to user.
            uint derivativeReceivedEthValue = (derivative.ethPerDerivative(
                depositAmount
            ) * depositAmount) / 10 ** 18;
            totalStakeValueEth += derivativeReceivedEthValue;
        }

Test

let depositAmount = ethers.utils.parseEther("100");
safEthProxy.stake({ value: depositAmount });
output:
ethAmount 33333333333333333333
ethAmount 33333333333333333333
ethAmount 33333333333333333333
ethAmountAfter balance left on safETH contract:1 

Tools Used

Manual

Recommended Mitigation Steps

Refund any excess back to the user.

Possibly reentrancy attacks in withdraw function

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L107-L114
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/SfrxEth.sol#L60-L88
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/WstEth.sol#L56-L67

Vulnerability details

Impact

Possibly reentrancy attacks in withdraw function

Proof of Concept

For example, Reth.sol calls rethAddress()).burn, which allows the Owner to call function in the fallback.

Tools Used

None.

Recommended Mitigation Steps

add reentrancy guard for critical function.

Missing deadline checks allow pending transactions to be maliciously executed

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L56-L67
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L60-L88
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L156-L204

Vulnerability details

Missing deadline checks allow pending transactions to be maliciously executed

Impact

The WstEth.sol and SfrxEth.sol derivatives contracts does not allow users to submit a deadline via withdraw when unstaking safEth via unstake() in the SafETH contract.

Similarly, the Reth.sol derivative contract does not allow users to submit a deadline via deposit() when staking safEth via stake() in the SafETH contract.

Transaction can be pending in the mempool for a long time and without deadline check, the transaction can be executed a long time after the user submits the transaction. By the time user's transaction is executed, the swap could have been done at a sub-optimal price, resulting in lesser safETH tokens minted or more safeETH tokens burned than expected.

Proof of Concept

1.Alice wants to unstake 1 safEth token for ETH and later sell the 1 ETH for 2000 DAI. She signs the transaction calling SafeEth.unstake with inputAmount = 1 safEth and minOut = 0.99 ETH to allow for some slippage.

2.The transaction is submitted to the mempool, however, Alice chose a transaction fee that is too low for miners to be interested in including her transaction in a block. The transaction stays pending in the mempool for extended periods, which could be hours, days, weeks, or even longer.

3.When the average gas fee dropped far enough for Alice's transaction to become interesting again for miners to include it, her swap will be executed. In the meantime, the price of ETH could have drastically decreased. She will still at least get 0.99 ETH due to minOut, but the DAI value of that output might be significantly lower. She has unknowingly performed a bad trade due to the pending transaction she forgot about.

An even worse way this issue can be maliciously exploited is through MEV:

1.The unstake transaction is still pending in the mempool. Average fees are still too high for miners to be interested in it. The price of safEth has gone up significantly since the transaction was signed, meaning Alice would receive a lot more ETH when the swap is executed. But that also means that her minOutput value is outdated and would allow for significant slippage.

2.A MEV bot detects the pending transaction. Since the outdated minOut now allows for high slippage, the bot sandwiches Alice, resulting in significant profit for the bot and significant loss for Alice.

The reverse can happen when staking ETH for Reth.sol derivatives contract.

Code Snippet

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L56-L67

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L60-L88

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L156-L204

Tools Used

Manual Analysis

Recommendation

Intoduce a deadline parameter to the functions withdraw() for WstEth.sol and SfrxEth.sol and deposit() for Reth.sol. This can be in the form of a modifier such as

modifier ensure(uint deadline) {
	require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
	_;
}

Incorrect Calculation of Price

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L86

Vulnerability details

Impact

The function returns the price of WstETH in terms of stETH. The underlying token which we desire is ETH.
Since stETH does not have the same value as WETH the output price incorrect.

Proof of Concept

function ethPerDerivative(uint256 _amount) public view returns (uint256) {
return IWStETH(WST_ETH).getStETHByWstETH(10 ** 18);
}

Tools Used

Manual review

Recommended Mitigation Steps

Add extra steps to approximate the rate for converting stETH to ETH.

Lack of slippage protection

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L48-L50
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L51-L53
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L58-L60

Vulnerability details

[M-03] Lack of slippage protection

Impact

Users can lose funds when setting maximum slippage via setMaxSlippage()

Proof of Concept

WstEth.sol#L48-L50

SfrxEth.sol#L51-L53

Reth.sol#L58-L60

/WstEth.sol
48:    function setMaxSlippage(uint256 _slippage) external onlyOwner {
49:        maxSlippage = _slippage;
50:    }

/SfrxEth.sol
51:    function setMaxSlippage(uint256 _slippage) external onlyOwner {
52:        maxSlippage = _slippage;
53:    }

/Reth.sol
58:    function setMaxSlippage(uint256 _slippage) external onlyOwner {
59:        maxSlippage = _slippage;
60:    }

The default maximum slippage is set at 1% and can be changed via owner only setMaxSlippage()
function. Owner of derivative contract can maliciously set a unbounded or too large maxSlippage leading to unintended losses by users staking.

Tools

Manual Analysis

Recommendation

Consider adding input validation checks and timelocks to bound _slippage and to allow users to react to slippage changes. Or add a warning on the UI of the protocol to warn and protect users when setting large slippages or when dealing with large token amounts.

Refer to this link for suggestions on slippage rate protection

Precision Loss that can lead to potential loss of funds through front running and sandwich attacks

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L74-L75

Vulnerability details

Impact

SfrxEth.sol#L74-L75

60:    function withdraw(uint256 _amount) external onlyOwner {
61:        IsFrxEth(SFRX_ETH_ADDRESS).redeem(
62:            _amount,
63:            address(this),
64:            address(this)
65:        );
66:        uint256 frxEthBalance = IERC20(FRX_ETH_ADDRESS).balanceOf(
67:            address(this)
68:        );
69:        IsFrxEth(FRX_ETH_ADDRESS).approve(
70:            FRX_ETH_CRV_POOL_ADDRESS,
71:            frxEthBalance
72:        );
73:        uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) *
74:            (10 ** 18 - maxSlippage)) / 10 ** 18;
75:
76:        IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange(
77:            1,
78:            0,
79:            frxEthBalance,
80:            minOut
81:        );
82:        // solhint-disable-next-line
83:        (bool sent, ) = address(msg.sender).call{value: address(this).balance}(
84:            ""
85:        );
86:        require(sent, "Failed to send Ether");
87:    }

There is a potential precision loss when calculating the minimumOut representing the minimum coins received during swap of sfrxETH to ETH, minOut minimum amount of ETH to receive may be set to zero when the _amount parameter to withdraw is low, such as when amount to withdraw is lower than one Eth.

73:        uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) *
74:            (10 ** 18 - maxSlippage)) / 10 ** 18;

If minOut is computed as 0, slippage check would fail and the swap is at risk of being front/run or sandwiched when it is triggered when unstake is called in SafEth.sol which can lead to loss of funds for users.

Tools Used

Manual Review

Recommendation

Perform multiplication before division to ensure no precision loss and possibility of minOut to be set as zero.

uint256 minOut = (ethPerDerivative(_amount) * _amount *
    (10 ** 18 - maxSlippage)) / 10 ** 18  / 10 ** 18;

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

`SafEth.sol` is intended to be upgradable but inherits from contracts that contain storage and no gaps

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L15
https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEthStorage.sol#L15

Vulnerability details

Impact

  • SafEth.sol inherits from contract SafEthStorage.sol that don't contain storage gaps which can be dangerous when upgrading.
  • Consequences that overwriting storage can lead to

Refer to the bottom part of this article: https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable

Proof of Concept

At the moment, the storage layout for the SafEth.sol contract will look like this:

storage slots:
- Initializable
- ERC20Upgradeable
- OwnableUpgradeable
- SafEthStorage

Since SafEthStorage.sol does not have a ___gap, the following situation can now occur:

  1. The SafEth contract is upgraded and starts inheriting a new contract, for example, ReentrancyGuard
- Initializable
- ERC20Upgradeable
- OwnableUpgradeable
- SafEthStorage
- ReentrancyGuard
  1. The contract is upgraded again, but now a new variable is added to SafEthStorage.sol

In this case, since we don't have a ___gap for SafEthStorage.sol, we will encounter a situation where the new variable overwrites the storage of ReentrancyGuard

- Initializable
- ERC20Upgradeable
- OwnableUpgradeable
- SafEthStorage
- 1 slot from SafEthStorage and ReentrancyGuard on some place

Tools Used

Manual review

Recommended Mitigation Steps

Consider adding a storage gap at the end of the upgradeable abstract contract SafEthStorage.sol

uint256[50] private __gap;

User can lose funds / Funds can stuck

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L138
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L165

Vulnerability details

Impact

"User can lose funds/Funds can stuck" in below 2 scenarios

  1. Admin mistakenly changes weight of an non existing _derivativeIndex
  2. Admin has changed weight of all derivatives to 0

Proof of Concept

  1. Lets say currently
derivativeCount = 3
 weights[0] = 10;
 weights[1] = 10;
 weights[2] = 10;
totalWeight = 30
  1. Admin calls adjustWeight function and instead of using _derivativeIndex as 2, mistakenly uses 3, with weight of 20
function adjustWeight(
        uint256 _derivativeIndex,
        uint256 _weight
    ) external onlyOwner {
        weights[_derivativeIndex] = _weight;
        uint256 localTotalWeight = 0;
        for (uint256 i = 0; i < derivativeCount; i++)
            localTotalWeight += weights[i];
        totalWeight = localTotalWeight;
        emit WeightChange(_derivativeIndex, _weight);
    }
  1. This updates like below:
 weights[3] = 20;
totalWeight = 50
derivativeCount = 3 (since this was not updated)
  1. Now if Admin call rebalanceToWeights function
function rebalanceToWeights() external onlyOwner {
        ...

        for (uint i = 0; i < derivativeCount; i++) {
            if (weights[i] == 0 || ethAmountToRebalance == 0) continue;
            uint256 ethAmount = (ethAmountToRebalance * weights[i]) /
                totalWeight;
            // Price will change due to slippage
            derivatives[i].deposit{value: ethAmount}();
        }
        ...
    }
  1. Since derivativeCount is 3 instead of 4 so loop runs for the initial 3 derivatives.
  2. The problem is totalWeight which is also considering the weight 20 of the mistakenly added derivative, so if ethAmountToRebalance was 50 then
uint256 ethAmount = (ethAmountToRebalance * weights[i]) / totalWeight;

// This becomes 

ethAmount = (50 * 10) / 50; = 10

// instead of 

ethAmount = (50 * 10) / 30; = 16
  1. So rebalancing will be incorrect (depositing 6 less tokens to derivative)
  2. This also causes the excess 50-30=20 balance to get stuck in contract as only amount 30 was deposited and amount 20 was untouched in contract
uint256 ethAmountBefore = address(this).balance;
        for (uint i = 0; i < derivativeCount; i++) {
            if (derivatives[i].balance() > 0)
                derivatives[i].withdraw(derivatives[i].balance());
        }
        uint256 ethAmountAfter = address(this).balance;
        uint256 ethAmountToRebalance = ethAmountAfter - ethAmountBefore;
  1. Similarly if Admin mistakenly set derivative weight to 0 and calls rebalanceToWeights function, then during unstake user get 0 funds due to derivative balance being 0
// withdraw a percentage of each asset based on the amount of safETH
            uint256 derivativeAmount = (derivatives[i].balance() *
                _safEthAmount) / safEthTotalSupply;

Recommended Mitigation Steps

Add below checks:

function adjustWeight(
        uint256 _derivativeIndex,
        uint256 _weight
    ) external onlyOwner {
require(_derivativeIndex<derivativeCount, "Incorrect index provided");
}
function rebalanceToWeights() external onlyOwner {
require(totalWeight>0, "No derivative to rebalance");
}

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

set of sqrtPriceLimitX96 is 0 disables the slippage protection.

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L83-L101

Vulnerability details

Impact

protect against price impact is disabled by set sqrtPriceLimitX96 to 0 .

Proof of Concept

    function swapExactInputSingleHop(
        address _tokenIn,
        address _tokenOut,
        uint24 _poolFee,
        uint256 _amountIn,
        uint256 _minOut
    ) private returns (uint256 amountOut) {
        IERC20(_tokenIn).approve(UNISWAP_ROUTER, _amountIn);
        ISwapRouter.ExactInputSingleParams memory params = ISwapRouter
            .ExactInputSingleParams({
                tokenIn: _tokenIn,
                tokenOut: _tokenOut,
                fee: _poolFee,
                recipient: address(this),
                amountIn: _amountIn,
                amountOutMinimum: _minOut,
                sqrtPriceLimitX96: 0  <- here 
            });
        amountOut = ISwapRouter(UNISWAP_ROUTER).exactInputSingle(params);
    }

Tools Used

Manual Review

Recommended Mitigation Steps

Use separate price limit parameters for the perp order and the uniswap swap.

ref

https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps

Derivative contracts are emptied after calling withdraw()

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L63
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L84
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L110

Vulnerability details

Impact

withdraw() method accepts amount parameter which is used to calculate how much eth should be send to the owner(SafEth) but then it sends the whole balance(address(this).balance) which empties the balance of the contract and will result in incorrect arithmetics wherever the derivative contract's balance() method is used.

Proof of Concept

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L63
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L84
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L110

Tools Used

Manual review

Recommended Mitigation Steps

Pass only the necessary amount to address(msg.sender).call

miscalculation of ethPerDerivative for SfrxETH can lead to loss of funds when staking due to higher prices

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L72-L75
https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L111-L117

Vulnerability details

Impact

When staking , ethPerDerivative for SfrxETH is part of the sum that determines the price of the safETH tokens. However, due to an incorrect computation in ethPerDerivative, the returned sum will be higher and hence price of safETH tokens will be higher than it should be, causing loss of funds to user as they receive lesser tokens in return.

Proof of Concept

In staking of safETH, each derivative will call ethPerDerivative to get the value of the derivative in ETH value. SfrxETH is on of the derivative.

            underlyingValue +=
                (derivatives[i].ethPerDerivative(derivatives[i].balance()) *
                    derivatives[i].balance()) /
                10 ** 18;

However, in SfrxETH, notice return ((10 ** 18 * frxAmount) / IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle());. This is incorrect as price_oracle returns how much 1 frxETH is worth in terms of ETH. This can be confirmed by going to https://curve.fi/#/ethereum/swap and seeing the exchange rate of 1 frxETH to ETH. See price_oracle return value.

Even though frxETH currently worths less than ETH, the computation will cause ethPerDerivative for SfrxETH to be more than frxETH amount. Even if frxETH suddenly becomes worth more than ETH, the problem will flip to become ethPerDerivative for SfrxETH is less than frxETH amount.

    function ethPerDerivative(uint256 _amount) public view returns (uint256) {
        uint256 frxAmount = IsFrxEth(SFRX_ETH_ADDRESS).convertToAssets(
            10 ** 18
        );
        return ((10 ** 18 * frxAmount) /
            IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle());
    }

Tools Used

Manual Review

Recommended Mitigation Steps

Recommend using get_virtual_price that gets the worth of 1 ETH in terms of frxETH. See get_virtual_price return value.

    return ((10 ** 18 * frxAmount) /
            (10**18/IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).get_virtual_price()));

Reth derivative is vulnerable to oracle manipulation with flashloan

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L228-L242

Vulnerability details

Impact

Reth pool price can be manipulated to cause loss of funds for the protocol and other users

Proof of Concept

Reth poolPrice uses the UniV3Pool.slot0 to determine the price of reth/eth, slot0 is the most recent data point and can easily be manipulated.
This allows a malicious user to manipulate the valuation of the rETH. An example of this kind of manipulation would be to use large amount of reth to be withdraw.

Tools Used

Manual review

Recommended Mitigation Steps

Consider using TWAP oracle instead of reading from slot0

When stake the user could lose all funds if totalWeight is 0

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L87

Vulnerability details

Impact

In adjustWeight it doesn't validate that totalWeight must be greater than 0. If all weights are set to 0 accidentally by contract owner somehow, then when user calls stake it will receive 0 safEth tokens. In such case he/she can't unstake any tokens.

For example currently the function adjustWeight can only update one derivative at a time. Let's suppose there are two derivatives, derivative0's weight is 0 and derivative1's weight is 10 * 18, and if we need to adjust derivative0's weight to 10 * 18 and derivative1's weight to 0 then we must pay attention to the order of adjusting the two derivatives. If adjust derivative1's weight to 0 first then there could be a short period during which totalWeight is 0.

For this protocol there should be an invariant that totalWeight should always be greater than 0.

Proof of Concept

I added a test in SafEth.test.ts, it will

1. Set all derivatives' weight to 0
2. Stake 200ETH
3. Validate the user's SafETH balance should be greater than 0, which will fail
  describe("stake with totalWeight be 0", function () {
    it ("should revert if totalWeight is 0", async function () {
      const derivativeCount = (await safEthProxy.derivativeCount()).toNumber();

      const zeroWeight = BigNumber.from("0");

      for (let i = 0; i < derivativeCount; i++) {
        const tx1 = await safEthProxy.adjustWeight(i, zeroWeight);
        await tx1.wait();
      }

      const depositAmount = ethers.utils.parseEther("200");
      const tx1 = await safEthProxy.stake({ value: depositAmount });
      const [signer] = await ethers.getSigners();
      // This expect will fail because the totalWeight is 0, however when signer calls
      // stake he should always receive SafETH if the tx doesn't revert
      expect((await safEthProxy.balanceOf(signer.address)).gt(0)).to.equal(true);
    });
  });

Tools Used

Manual review

Recommended Mitigation Steps

In stake function add following validation

require(totalWeight > 0, "Weight is not set");

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Attacker can manipulate rETH-derivative's ethPerDerivative() to mint discounted safETH and drain funds.

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L212

Vulnerability details

Impact

An attacker can manipulate ethPerDerivative() for the rETH-derivative, allowing him to mint cheap SafETH which can then be unstaked at normal prices. If repeated, could drain a large amount of funds.

Mechanisms

  • ethPerDerivative() for rETH changes depending on _amount: either returns the poolPrice or the native rETH price.

  • An attacker can manipulate the poolPrice to significantly discount rETH (e.g., using flashloan)

Proof of Concept

Assumptions:

  • rETH is the only derivative used (for math simplicity)
  • rETH native price is 1 ETH (for math simplicity)
  • SafETH holds 5000 rETH (worth 5000 ETH)

Exploit:

  1. Attacker sells large amount of rETH on uniswap (e.g., using flashloan) -> rETH price is discounted to 0.5ETH
  2. Attacker stakes 100 ETH on SafETH
  3. ethPerDerivative on line 73 in stake() returns the poolPrice. This is because SafETH holds 5000 rETH, which is larger than Rocket Pool's deposit pool limit, thus poolCanDeposit() will be false.
  4. As a result underlyingValue will only be half of the actual (native) value.
  5. ethPerDerivative on line 92 in stake() will return the native value.
  6. mintAmount will be double of what it should be. -> Attacker minted SafETH at half the price
  7. Attacker rebuys large amount of rETH on uniswap
  8. Attacker unstakes SafETH at normal prices
  9. Repeat

Recommended Mitigation Steps

ethPerDerivative() should return the same value regardless of amount. Either the market price or the native price.

QA Report

See the markdown file with the details of this report here.

Loss of precision in calc preDepositPrice

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L68-L81

Vulnerability details

Impact

When calculating underlyingVaule, it gets every derivative price multiplied reverse. But the code firstly converts it's to ether (div 1018), but afterwhile code use underlyingValue multiplies 1018 to calculate preDepositPrice, it may cause precision loss in price.

Proof of Concept

uint256 underlyingValue = 0;

// Getting underlying value in terms of ETH for each derivative
for (uint i = 0; i < derivativeCount; i++)
    underlyingValue +=
        (derivatives[i].ethPerDerivative(derivatives[i].balance()) *
            derivatives[i].balance()) /
        10 ** 18;

uint256 totalSupply = totalSupply();
uint256 preDepositPrice; // Price of safETH in regards to ETH
if (totalSupply == 0)
    preDepositPrice = 10 ** 18; // initializes with a price of 1
else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;

Tools Used

manual review

Recommended Mitigation Steps

Remove div 1018 in calc underlyingVaule and also remove multiplies 1018 in calc preDepositPrice.

Protocol susceptible to price manipulation

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L228-L241

Vulnerability details

Impact

The price of derivative tokens is obtained from the current pool price, which makes it susceptible to price manipulation through flash loan. which may pose a risk. Price can be easily manipulated by attackers using flash loans to control the pool's price. So attacker can mint more than expected SafEth token. Then attacker can use SafEth to unsatke to make profit. Therefore, it is advisable to use like Uniswap's TWAP to obtain an average price.

The stake() & unstake() functions in SafEth.sol relies:
1.In Reth.sol ethPerDerivative() fetch the current price of Reth from uniswap. Use poolPrice() to calculate Reth's underlying value, derivativeReceivedEthValue, and the derivative.deposit{} of Reth will be affected as well.

2.In SfrxEth.sol ethPerDerivative() fetch the current price of frxETH from curve pool over price_oracle.

3.In WstEth.sol ethPerDerivative() fetch the current price of WstEth from stETH.getPooledEthByShares(_wstETHAmount)
@return the amount of Ether that corresponds to _sharesAmount token shares.

These three prices are the current prices and are susceptible to price manipulation.

Proof of Concept

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L228-L241

        @notice - Price of derivative in liquidity pool
     */
    function poolPrice() private view returns (uint256) {
        address rocketTokenRETHAddress = RocketStorageInterface(
            ROCKET_STORAGE_ADDRESS
        ).getAddress(
                keccak256(
                    abi.encodePacked("contract.address", "rocketTokenRETH")
                )
            );
        IUniswapV3Factory factory = IUniswapV3Factory(UNI_V3_FACTORY);
        IUniswapV3Pool pool = IUniswapV3Pool(
            factory.getPool(rocketTokenRETHAddress, W_ETH_ADDRESS, 500)
        );
        (uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
        return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2);
    }

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/SafEth.sol#L63-L99

    function stake() external payable {
        require(pauseStaking == false, "staking is paused");
        require(msg.value >= minAmount, "amount too low");
        require(msg.value <= maxAmount, "amount too high");

        uint256 underlyingValue = 0;

        // Getting underlying value in terms of ETH for each derivative
        for (uint i = 0; i < derivativeCount; i++)
            underlyingValue +=
                (derivatives[i].ethPerDerivative(derivatives[i].balance()) *    //@audit reth price can be manipulated over flash loan
                    derivatives[i].balance()) /
                10 ** 18;

        uint256 totalSupply = totalSupply();
        uint256 preDepositPrice; // Price of safETH in regards to ETH
        if (totalSupply == 0)
            preDepositPrice = 10 ** 18; // initializes with a price of 1
        else preDepositPrice = (10 ** 18 * underlyingValue) / totalSupply;

        uint256 totalStakeValueEth = 0; // total amount of derivatives worth of ETH in system
        for (uint i = 0; i < derivativeCount; i++) {
            uint256 weight = weights[i];
            IDerivative derivative = derivatives[i];
            if (weight == 0) continue;
            uint256 ethAmount = (msg.value * weight) / totalWeight;

            // This is slightly less than ethAmount because slippage
            uint256 depositAmount = derivative.deposit{value: ethAmount}();   //@audit reth price can be manipulated over flash loan
            uint derivativeReceivedEthValue = (derivative.ethPerDerivative(   //@audit reth price can be manipulated over flash loan
                depositAmount
            ) * depositAmount) / 10 ** 18;
            totalStakeValueEth += derivativeReceivedEthValue;
        }
        // mintAmount represents a percentage of the total assets in the system
        uint256 mintAmount = (totalStakeValueEth * 10 ** 18) / preDepositPrice;
        _mint(msg.sender, mintAmount);
        emit Staked(msg.sender, msg.value, mintAmount);
    }

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L116

            IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).price_oracle());

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L87

    function ethPerDerivative(uint256 _amount) public view returns (uint256) {
        return IWStETH(WST_ETH).getStETHByWstETH(10 ** 18);
    }

Tools Used

Manual

Recommended Mitigation Steps

Use TWAP to obtain an average price.

Add access control to unstake() function

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/SafEth.sol#L108-L129

Vulnerability details

The unstake() function allows any user to withdraw tokens from the contract. This could be a potential security issue if the contract holds a large amount of assets. It would be better to add an access control mechanism, such as a onlyOwner modifier, to restrict who can call this function.

Minimum output size for stETH sell does not account for possible market depeg

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L60

Vulnerability details

Impact

The withdraw() function for wstETH-derivative does not account for a possible discount of stETH. This can cause funds to be frozen for a while.

Explanation

wstETH derivative's withdraw() function calculates minOut with the assumption that 1 stETH = 1 ETH. This is not always true, stETH has traded at large discounts (>1%) for extended periods of time. If the maxSlippage is set to 1% while stETH has a large discount, the (stETH sell)[https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/WstEth.sol#L61] would revert, causing unstake() to revert, resulting in all funds being stuck. An operator would have to manually increase slippage for this derivative to fix it.

Recommended Mitigation Steps

  • Calculate minOut based on current market price for stETH.

When rocketpool node deposits are not enabled, users will not be able to stake.

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/44b5cd94ebedc187a08884a7f685e950e987261c/contracts/SafEth/derivatives/Reth.sol#L148

Vulnerability details

Impact

When rocketpool node deposits are not enabled, users will not be able to stake.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Whenever user stake system check whether rocketpool can be deposited to - poolCanDeposit

    function ethPerDerivative(uint256 _amount) public view returns (uint256) {
        if (poolCanDeposit(_amount))
            return
                RocketTokenRETHInterface(rethAddress()).getEthValue(10 ** 18);
        else return (poolPrice() * 10 ** 18) / (10 ** 18);
    }

SafEth/derivatives/Reth.sol#L212

Here is the function poolCanDeposit

    function poolCanDeposit(uint256 _amount) private view returns (bool) {
        address rocketDepositPoolAddress = RocketStorageInterface(
            ROCKET_STORAGE_ADDRESS
        ).getAddress(
                keccak256(
                    abi.encodePacked("contract.address", "rocketDepositPool")
                )
            );
        RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(
                rocketDepositPoolAddress
            );

        address rocketProtocolSettingsAddress = RocketStorageInterface(
            ROCKET_STORAGE_ADDRESS
        ).getAddress(
                keccak256(
                    abi.encodePacked(
                        "contract.address",
                        "rocketDAOProtocolSettingsDeposit"
                    )
                )
            );
        RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(
                rocketProtocolSettingsAddress
            );

        return
            rocketDepositPool.getBalance() + _amount <=
            rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize() &&
            _amount >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit();
    }

SafEth/derivatives/Reth.sol#L120

I think the whole system was designed to work even when user cannot deposit to rocketpool. Here you can see code from rocketpool, require from line 77 is not implemented, all the other requires are in the place

    function deposit() override external payable onlyThisLatestContract {
        // Check deposit settings
        RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit"));
77:        require(rocketDAOProtocolSettingsDeposit.getDepositEnabled(), "Deposits into Rocket Pool are currently disabled");
        require(msg.value >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit(), "The deposited amount is less than the minimum deposit size");
        RocketVaultInterface rocketVault = RocketVaultInterface(getContractAddress("rocketVault"));
        require(rocketVault.balanceOf("rocketDepositPool").add(msg.value) <= rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize(), "The deposit pool size after depositing exceeds the maximum size");
        // Calculate deposit fee
        uint256 depositFee = msg.value.mul(rocketDAOProtocolSettingsDeposit.getDepositFee()).div(calcBase);
        uint256 depositNet = msg.value.sub(depositFee);
        // Mint rETH to user account
        RocketTokenRETHInterface rocketTokenRETH = RocketTokenRETHInterface(getContractAddress("rocketTokenRETH"));
        rocketTokenRETH.mint(depositNet, msg.sender);
        // Emit deposit received event
        emit DepositReceived(msg.sender, msg.value, block.timestamp);
        // Process deposit
        processDeposit(rocketVault, rocketDAOProtocolSettingsDeposit);
    }

deposit/RocketDepositPool.sol#L73-L91

Tools Used

Manual review

Recommended Mitigation Steps

Add missing check, so user`s transactions will not be reverted when rocketpool node deposits are not enabled

    function poolCanDeposit(uint256 _amount) private view returns (bool) {
        address rocketDepositPoolAddress = RocketStorageInterface(
            ROCKET_STORAGE_ADDRESS
        ).getAddress(
                keccak256(
                    abi.encodePacked("contract.address", "rocketDepositPool")
                )
            );
        RocketDepositPoolInterface rocketDepositPool = RocketDepositPoolInterface(
                rocketDepositPoolAddress
            );

        address rocketProtocolSettingsAddress = RocketStorageInterface(
            ROCKET_STORAGE_ADDRESS
        ).getAddress(
                keccak256(
                    abi.encodePacked(
                        "contract.address",
                        "rocketDAOProtocolSettingsDeposit"
                    )
                )
            );
        RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(
                rocketProtocolSettingsAddress
            );

        return
+            rocketDAOProtocolSettingsDeposit.getDepositEnabled() &&
            rocketDepositPool.getBalance() + _amount <=
            rocketDAOProtocolSettingsDeposit.getMaximumDepositPoolSize() &&
            _amount >= rocketDAOProtocolSettingsDeposit.getMinimumDeposit();
    }

Using Uniswap spot price as price oracle.

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/Reth.sol#L228-L242

Vulnerability details

Impact

In Reth.sol, function poolPrice use to calc ethPerDerivative and mints shares to user. But it use Uniswap spot price, which is easy to manipulate by flashloan. malicious users can use flashloan to manipulate price in order to mint more shares. It is dangerouse.

Proof of Concept

function poolPrice() private view returns (uint256) {
    address rocketTokenRETHAddress = RocketStorageInterface(
        ROCKET_STORAGE_ADDRESS
    ).getAddress(
            keccak256(
                abi.encodePacked("contract.address", "rocketTokenRETH")
            )
        );
    IUniswapV3Factory factory = IUniswapV3Factory(UNI_V3_FACTORY);
    IUniswapV3Pool pool = IUniswapV3Pool(
        factory.getPool(rocketTokenRETHAddress, W_ETH_ADDRESS, 500)
    );
    (uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
    return (sqrtPriceX96 * (uint(sqrtPriceX96)) * (1e18)) >> (96 * 2);
}

Tools Used

manual review

Recommended Mitigation Steps

Recommend use Uniswap TWAP oracle to get pool price which is very hard to manpulate.

division before multiplication in SfrxEth can cause slippage to be higher than expected

Lines of code

https://github.com/code-423n4/2023-03-asymmetry/blob/main/contracts/SafEth/derivatives/SfrxEth.sol#L74-L82

Vulnerability details

Impact

When withdrawing from SfrxEth, slippage control is put in place when swapping sFrxEth for Eth. However, due to division before multiplication when calculating minOut, slippage control can be lower than intended, causing slippage that is more than expected.

Proof of Concept

In withdraw of SfrxEth minOut act as the slippage control. Notice that multiplication before division is done which can cause minOut to be lower than intended. This can cause slippage to be more than expected.

        uint256 minOut = (((ethPerDerivative(_amount) * _amount) / 10 ** 18) *
            (10 ** 18 - maxSlippage)) / 10 ** 18;


        IFrxEthEthPool(FRX_ETH_CRV_POOL_ADDRESS).exchange(
            1,
            0,
            frxEthBalance,
            minOut
        );

Note: This also applies to deposit in Reth.

            uint256 minOut = ((((rethPerEth * msg.value) / 10 ** 18) *
                ((10 ** 18 - maxSlippage))) / 10 ** 18);

Tools Used

Manual Review

Recommended Mitigation Steps

Recommend multiplying before dividing.

      uint256 minOut = ethPerDerivative(_amount) * _amount *
            (10 ** 18 - maxSlippage) / 10 ** 18 / 10 ** 18;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.