Asymmetry contest - ladboy233's results

A protocol to help diversify and decentralize liquid staking derivatives.

General Information

Platform: Code4rena

Start Date: 24/03/2023

Pot Size: $49,200 USDC

Total HM: 20

Participants: 246

Period: 6 days

Judge: Picodes

Total Solo HM: 1

Id: 226

League: ETH

Asymmetry Finance

Findings Distribution

Researcher Performance

Rank: 36/246

Findings: 6

Award: $194.69

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: ladboy233

Also found by: 0xkazim, 0xnev, Bauer, J4de, Matin, UniversalCrypto, cryptothemex, jasonxiale, juancito, koxuan, latt1ce, neumo

Labels

bug
2 (Med Risk)
high quality report
primary issue
selected for report
M-01

Awards

63.2128 USDC - $63.21

External Links

Lines of code

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

Vulnerability details

Impact

When Calcuting the minOut before doing trade,

Division before multiplication truncate minOut and incurs heavy precision loss, then very sub-optimal amount of the trade output can result in loss of fund from user because of the insufficient slippage protection

Proof of Concept

In the current implementation,

slippage can be set by calling

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

/**
	@notice - Sets the max slippage for a certain derivative index
	@param _derivativeIndex - index of the derivative you want to update the slippage
	@param _slippage - new slippage amount in wei
*/
function setMaxSlippage(
	uint _derivativeIndex,
	uint _slippage
) external onlyOwner {
	derivatives[_derivativeIndex].setMaxSlippage(_slippage);
	emit SetMaxSlippage(_derivativeIndex, _slippage);
}

which calls the corresponding derivative contract

Case 1

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

/**
	@notice - Owner only function to set max slippage for derivative
	@param _slippage - new slippage amount in wei
*/
function setMaxSlippage(uint256 _slippage) external onlyOwner {
	maxSlippage = _slippage;
}

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

calling

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

IWETH(W_ETH_ADDRESS).deposit{value: msg.value}();
uint256 amountSwapped = swapExactInputSingleHop(
	W_ETH_ADDRESS,
	rethAddress(),
	500,
	msg.value,
	minOut
);

As we can see, the division before multiplication happens in the line of code

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

for example, if maxSlippage is 10 ** 17

(10 ** 18 - 10 ** 17) / (10 ** 18) = 0

then minOut is 0, slippage control is disabled because of the division before multipcation.

Case 2

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

/**
	@notice - Owner only function to set max slippage for derivative
*/
function setMaxSlippage(uint256 _slippage) external onlyOwner {
	maxSlippage = _slippage;
}

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

the divison before multipilcation happens below

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

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

for example, if maxSlippage is 10 ** 17

(10 ** 18 - 10 ** 17) / (10 ** 18) = 0

then minOut is 0, slippage control is disabled because of the division before multipcation.

Tools Used

Manual Review

We recommend the protocol avoid division before multiplcation when calcaluting the minOut to enable slippage protection and avoid front-running.

#0 - c4-pre-sort

2023-04-02T09:23:06Z

0xSorryNotSorry marked the issue as high quality report

#1 - c4-pre-sort

2023-04-04T16:37:51Z

0xSorryNotSorry marked the issue as duplicate of #1044

#2 - c4-judge

2023-04-22T10:27:52Z

Picodes marked the issue as selected for report

#3 - Picodes

2023-04-22T10:31:38Z

Note that there is a multiplication before the division, so the loss of precision is significant only if msg.value is small

Findings Information

Labels

bug
2 (Med Risk)
satisfactory
sponsor confirmed
duplicate-932

Awards

40.6368 USDC - $40.64

External Links

Lines of code

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

Vulnerability details

Impact

Lack of slippage control and deadline check when staking and unstaking

Proof of Concept

The protocol implemented slippage control but the slippage control is used when swapping asset either via uniswap pool or curve pool and does not has deadline check or slippage protection when staking and unstaking.

This is problematic because since the protocol is deployed to mainnet ethereum,

let us say for example, the user call stake function and want to deposit 1 ETH, he submitted a transaction with reasonable gas price, and the transaction is pending in the mempool to be executed.

Then there is a gas spike and high gas usage and the user's staking transaction is pending in the mempool for a long time.

assume the user will deposit the fund into LIDO finance, we are calling:

 (bool sent, ) = WST_ETH.call{value: msg.value}("");
 require(sent, "Failed to send Ether");

calling

/**
* @notice Shortcut to stake ETH and auto-wrap returned stETH
*/
receive() external payable {
	uint256 shares = stETH.submit{value: msg.value}(address(0));
	_mint(msg.sender, shares);
}

calling stETH.submit

/**
* @notice Send funds to the pool with optional _referral parameter
* @dev This function is alternative way to submit funds. Supports optional referral address.
* @return Amount of StETH shares generated
*/
function submit(address _referral) external payable returns (uint256) {
	return _submit(_referral);
}

calling

function _submit(address _referral) internal returns (uint256) {
	require(msg.value != 0, "ZERO_DEPOSIT");

	StakeLimitState.Data memory stakeLimitData = STAKING_STATE_POSITION.getStorageStakeLimitStruct();
	require(!stakeLimitData.isStakingPaused(), "STAKING_PAUSED");

	if (stakeLimitData.isStakingLimitSet()) {
		uint256 currentStakeLimit = stakeLimitData.calculateCurrentStakeLimit();

		require(msg.value <= currentStakeLimit, "STAKE_LIMIT");

		STAKING_STATE_POSITION.setStorageStakeLimitStruct(
			stakeLimitData.updatePrevStakeLimit(currentStakeLimit - msg.value)
		);
	}

	uint256 sharesAmount = getSharesByPooledEth(msg.value);
	if (sharesAmount == 0) {
		// totalControlledEther is 0: either the first-ever deposit or complete slashing
		// assume that shares correspond to Ether 1-to-1
		sharesAmount = msg.value;
	}

	_mintShares(msg.sender, sharesAmount);

	BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().add(msg.value));
	emit Submitted(msg.sender, msg.value, _referral);

	_emitTransferAfterMintingShares(msg.sender, sharesAmount);
	return sharesAmount;
}

note the code section:

uint256 sharesAmount = getSharesByPooledEth(msg.value);
if (sharesAmount == 0) {
	// totalControlledEther is 0: either the first-ever deposit or complete slashing
	// assume that shares correspond to Ether 1-to-1
	sharesAmount = msg.value;
}

_mintShares(msg.sender, sharesAmount);

which calls:

/**
 * @return the amount of shares that corresponds to `_ethAmount` protocol-controlled Ether.
 */
function getSharesByPooledEth(uint256 _ethAmount) public view returns (uint256) {
	uint256 totalPooledEther = _getTotalPooledEther();
	if (totalPooledEther == 0) {
		return 0;
	} else {
		return _ethAmount
			.mul(_getTotalShares())
			.div(totalPooledEther);
	}
}

as we can see the share minted is dependent on total shares and total pooled ether.

because it is possible the user's staking transaction is pending in the mempool for a long time and there can be a transaction landed that inflate the totalPooledEther to a high number because of the math ethAmount * totalShares / totalpooledEther, user's minted share via stake is greatedly diluted.

This is acutally the same case for withdraw transaction, the slippage control only applies to swap function but does not apply to how many share user get when depositing or how much asset user redeem via shares.

Tools Used

Manual Review

We recommend the protocol add deadline check for deposit and withdraw and add slippage control to let user specify the minShareReceived and minAssetReceived when deposit or withdraw from the underlying protocol.

#0 - c4-pre-sort

2023-04-04T21:12:20Z

0xSorryNotSorry marked the issue as primary issue

#1 - elmutt

2023-04-07T20:26:09Z

user specifying minOut is a good idea. We have seen this suggestion a couple times

#2 - c4-sponsor

2023-04-07T20:26:19Z

elmutt marked the issue as sponsor confirmed

#3 - c4-judge

2023-04-24T18:25:00Z

Picodes marked the issue as duplicate of #932

#4 - c4-judge

2023-04-24T21:20:59Z

Picodes marked the issue as satisfactory

Findings Information

Awards

28.8013 USDC - $28.80

Labels

bug
2 (Med Risk)
satisfactory
duplicate-812

External Links

Lines of code

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

Vulnerability details

Impact

Lack of consideration for LIDO finance deposit limit cap

Proof of Concept

In the current integration with Rocket Protocol, the code consider the deposit limit cap in Reth#deposit

If the logic is, if the deposit limit cap is reached, the code will aim to swap token for RETH in the Uniswap Trading pool, otherwise, deposit can getting the share token.

However, the current integration with LIDO finance WstETH failed to consider the deposit cap

LIDO finance introduces the deposit cap as well

https://cointelegraph.com/news/lido-finance-activates-staking-rate-limit-after-more-than-150-000-eth-staked

In implementation detail of the deposit cap limit is shown below

This is the token address for stETH

https://etherscan.io/token/0xae7ab96520de3a18e5e111b5eaab095312d7fe84#writeProxyContract

which is a proxy contract and the implementation address point to

https://etherscan.io/address/0x47ebab13b806773ec2a2d16873e2df770d130b50#code

We can see there is a function

https://etherscan.io/address/0x47ebab13b806773ec2a2d16873e2df770d130b50#code#F1#L250


    /**
    * @notice Returns full info about current stake limit params and state
    * @dev Might be used for the advanced integration requests.
    * @return isStakingPaused staking pause state (equivalent to return of isStakingPaused())
    * @return isStakingLimitSet whether the stake limit is set
    * @return currentStakeLimit current stake limit (equivalent to return of getCurrentStakeLimit())
    * @return maxStakeLimit max stake limit
    * @return maxStakeLimitGrowthBlocks blocks needed to restore max stake limit from the fully exhausted state
    * @return prevStakeLimit previously reached stake limit
    * @return prevStakeBlockNumber previously seen block number
    */
    function getStakeLimitFullInfo() external view returns (
        bool isStakingPaused,
        bool isStakingLimitSet,
        uint256 currentStakeLimit,
        uint256 maxStakeLimit,
        uint256 maxStakeLimitGrowthBlocks,
        uint256 prevStakeLimit,
        uint256 prevStakeBlockNumber
    )

the deposit limit cap refers to maxStakeLimit and currentStakeLimit and maxStakeLimitGrowthBlocks

if the deposit limit is breached, transaction will revert

    function _submit(address _referral) internal returns (uint256) {
        require(msg.value != 0, "ZERO_DEPOSIT");

        StakeLimitState.Data memory stakeLimitData = STAKING_STATE_POSITION.getStorageStakeLimitStruct();
        require(!stakeLimitData.isStakingPaused(), "STAKING_PAUSED");

        if (stakeLimitData.isStakingLimitSet()) {
            uint256 currentStakeLimit = stakeLimitData.calculateCurrentStakeLimit();

            require(msg.value <= currentStakeLimit, "STAKE_LIMIT");

            STAKING_STATE_POSITION.setStorageStakeLimitStruct(
                stakeLimitData.updatePrevStakeLimit(currentStakeLimit - msg.value)
            );
        }

        uint256 sharesAmount = getSharesByPooledEth(msg.value);
        if (sharesAmount == 0) {
            // totalControlledEther is 0: either the first-ever deposit or complete slashing
            // assume that shares correspond to Ether 1-to-1
            sharesAmount = msg.value;
        }

        _mintShares(msg.sender, sharesAmount);

        BUFFERED_ETHER_POSITION.setStorageUint256(_getBufferedEther().add(msg.value));
        emit Submitted(msg.sender, msg.value, _referral);

        _emitTransferAfterMintingShares(msg.sender, sharesAmount);
        return sharesAmount;
    }

note the check that revert the transaction.

	if (stakeLimitData.isStakingLimitSet()) {
		uint256 currentStakeLimit = stakeLimitData.calculateCurrentStakeLimit();

		require(msg.value <= currentStakeLimit, "STAKE_LIMIT");

		STAKING_STATE_POSITION.setStorageStakeLimitStruct(
			stakeLimitData.updatePrevStakeLimit(currentStakeLimit - msg.value)
		);
	}

Tools Used

Manual Review

We recommend the protocol consider the deposit limit cap for LIDO finance integration by implmenting the same approach to handle the Rocket protocol deposit cap, if the LIDO finance deposit limit is breached, swap token in external Uniswap pool to get the share token for user instead of letting the transaction revert.

#0 - c4-pre-sort

2023-04-04T18:57:49Z

0xSorryNotSorry marked the issue as duplicate of #812

#1 - c4-judge

2023-04-24T19:43:35Z

Picodes marked the issue as satisfactory

Awards

8.2654 USDC - $8.27

Labels

bug
2 (Med Risk)
satisfactory
sponsor disputed
edited-by-warden
duplicate-770

External Links

Lines of code

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

Vulnerability details

Impact

Deposit from Asymmetry finance would revert if external protocol's deposit is paused

Proof of Concept

In the current implementation, user needs to call SafETH.sol#stake to deposit ETH into the external protocol to generate yield.

The current external protocol that are integrated are frax ETH liquid staking, LIDO finance and rocket protocol

However, frax ETH protocol or LIDO finance protocol can pause the deposit

We are calling staking ->

	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}();
		uint derivativeReceivedEthValue = (derivative.ethPerDerivative(
			depositAmount
		) * depositAmount) / 10 ** 18;
		totalStakeValueEth += derivativeReceivedEthValue;
	}

calling derivative.deposit{value: ethAmount}()

if the derivative is sFrxETH.sol, we are calling

/**
	@notice - Owner only function to Deposit into derivative
	@dev - Owner is set to SafEth contract
 */
function deposit() external payable onlyOwner returns (uint256) {
	IFrxETHMinter frxETHMinterContract = IFrxETHMinter(
		FRX_ETH_MINTER_ADDRESS
	);
	uint256 sfrxBalancePre = IERC20(SFRX_ETH_ADDRESS).balanceOf(
		address(this)
	);
	frxETHMinterContract.submitAndDeposit{value: msg.value}(address(this));
	uint256 sfrxBalancePost = IERC20(SFRX_ETH_ADDRESS).balanceOf(
		address(this)
	);
	return sfrxBalancePost - sfrxBalancePre;
}

calling frxETHMinterContract.submitAndDeposit{value: msg.value}(address(this));

calling https://github.com/FraxFinance/frxETH-public/blob/7f7731dbc93154131aba6e741b6116da05b25662/src/frxETHMinter.sol#L70

/// @notice Mint frxETH and deposit it to receive sfrxETH in one transaction
/** @dev Could try using EIP-712 / EIP-2612 here in the future if you replace this contract,
	but you might run into msg.sender vs tx.origin issues with the ERC4626 */
function submitAndDeposit(address recipient) external payable returns (uint256 shares) {
	// Give the frxETH to this contract after it is generated
	_submit(address(this));

	// Approve frxETH to sfrxETH for staking
	frxETHToken.approve(address(sfrxETHToken), msg.value);

	// Deposit the frxETH and give the generated sfrxETH to the final recipient
	uint256 sfrxeth_recieved = sfrxETHToken.deposit(msg.value, recipient);
	require(sfrxeth_recieved > 0, 'No sfrxETH was returned');

	return sfrxeth_recieved;
}

calling _submit(adress(this)), which calling https://github.com/FraxFinance/frxETH-public/blob/7f7731dbc93154131aba6e741b6116da05b25662/src/frxETHMinter.sol#L85

   /// @notice Mint frxETH to the recipient using sender's funds. Internal portion
    function _submit(address recipient) internal nonReentrant {
        // Initial pause and value checks
        require(!submitPaused, "Submit is paused");
        require(msg.value != 0, "Cannot submit 0");

        // Give the sender frxETH
        frxETHToken.minter_mint(recipient, msg.value);

        // Track the amount of ETH that we are keeping
        uint256 withheld_amt = 0;
        if (withholdRatio != 0) {
            withheld_amt = (msg.value * withholdRatio) / RATIO_PRECISION;
            currentWithheldETH += withheld_amt;
        }

        emit ETHSubmitted(msg.sender, recipient, msg.value, withheld_amt);
    }

note the line of code: require(!submitPaued, "Submit is paused"), the frax ETH admin can toggle the submitPaused to block the user from deposit fund, because the usage of for loop, if one deposit revert, the deposit transaction revert.

The section above demonstrates that frax ETH deposit can be blocked, the section below would demonstrates the LIDO finance deposit can be blocked as well

When deposting into the LIDO finance, we are calling

(bool sent, ) = WST_ETH.call{value: msg.value}("");

which calling:

https://github.com/lidofinance/lido-dao/blob/df95e563445821988baf9869fde64d86c36be55f/contracts/0.6.12/WstETH.sol#L80

receive() external payable {
	uint256 shares = stETH.submit{value: msg.value}(address(0));
	_mint(msg.sender, shares);
}

calling stETH.submit{value: msg.value}(address(0));

calling https://etherscan.io/address/0x47ebab13b806773ec2a2d16873e2df770d130b50#code#F1#L290

function submit(address _referral) external payable returns (uint256) {
	return _submit(_referral);
}

which calling _submit

function _submit(address _referral) internal returns (uint256) {
	require(msg.value != 0, "ZERO_DEPOSIT");

	StakeLimitState.Data memory stakeLimitData = STAKING_STATE_POSITION.getStorageStakeLimitStruct();
	require(!stakeLimitData.isStakingPaused(), "STAKING_PAUSED");

as we can see, if the admin pause the contract staking in LIDO finance side, require(!stakeLimitData.isStakingPaused(), "STAKING_PAUSED"); revert.

If we revisit the deposit function in Asymmetry finance side,

	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}();
		uint derivativeReceivedEthValue = (derivative.ethPerDerivative(
			depositAmount
		) * depositAmount) / 10 ** 18;
		totalStakeValueEth += derivativeReceivedEthValue;
	}

the for loop introduce a dependency, meaning one deposit function revert, the whole deposit is blocked.

In fact, Rocket Protocol's admin can pause the depoist as well to block deposit and revert transaction.

https://github.com/rocket-pool/rocketpool/blob/967e4d3c32721a84694921751920af313d1467af/contracts/contract/deposit/RocketDepositPool.sol#L77

    // Accept a deposit from a user
    function deposit() override external payable onlyThisLatestContract {
        // Check deposit settings
        RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit"));
        require(rocketDAOProtocolSettingsDeposit.getDepositEnabled(), "Deposits into Rocket Pool are currently disabled");

Tools Used

Manual Review

We recommend the protocol check if deposit is enabled when depositing, when the deposit is disabled, the protocol can either set the weight to 0 or swapping the share token for user via uniswap pool or curve pool just like the protocol handling the rocket protocol deposit cap.

#0 - c4-pre-sort

2023-04-04T21:58:38Z

0xSorryNotSorry marked the issue as primary issue

#1 - elmutt

2023-04-07T20:38:21Z

We intentionally revert if 1 fails to keep logic easier to understand

#2 - c4-sponsor

2023-04-07T20:38:26Z

elmutt marked the issue as sponsor disputed

#3 - c4-judge

2023-04-21T09:58:24Z

Picodes marked the issue as satisfactory

#4 - c4-judge

2023-04-21T10:47:15Z

Picodes marked the issue as duplicate of #770

Findings Information

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sponsor acknowledged
duplicate-150

Awards

40.6368 USDC - $40.64

External Links

Lines of code

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

Vulnerability details

Impact

Protocol can lose fund if the integrated liquid staking protocol charge high fee

Proof of Concept

The protocol integrate with the LIDO finance and frax share and rocket protocol,

however, the user can deposit via the SafETH.sol can lose their deposit if the underlying protocol charge high fee.

For example, when depositing into the rocketProtoocol,

we are calling:

uint256 rethBalance1 = rocketTokenRETH.balanceOf(address(this));
rocketDepositPool.deposit{value: msg.value}();
uint256 rethBalance2 = rocketTokenRETH.balanceOf(address(this));

which calls rocketDepositPool.deposit{value: msg.value}();

https://github.com/rocket-pool/rocketpool/blob/967e4d3c32721a84694921751920af313d1467af/contracts/contract/deposit/RocketDepositPool.sol#L74

    // Accept a deposit from a user
    function deposit() override external payable onlyThisLatestContract {
        // Check deposit settings
        RocketDAOProtocolSettingsDepositInterface rocketDAOProtocolSettingsDeposit = RocketDAOProtocolSettingsDepositInterface(getContractAddress("rocketDAOProtocolSettingsDeposit"));
        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);
    }

note the code:

 uint256 depositFee = msg.value.mul(rocketDAOProtocolSettingsDeposit.getDepositFee()).div(calcBase);
        uint256 depositNet = msg.value.sub(depositFee);

the if the rocket protocl charge 50% of the fee or even 99% of the fee, it is not reasonable to still deposit the fund into the protocol for yield because the deposit instanlty incurs large loss.

This is the same case for frax liquid staking, when depositing, the protocol from liquid staking can withdraw a part of ETH based on the withhold ratio

When depositing into the frax pool, we are calling

frxETHMinterContract.submitAndDeposit{value: msg.value}(address(this));

which calls:

    /// @notice Mint frxETH and deposit it to receive sfrxETH in one transaction
    /** @dev Could try using EIP-712 / EIP-2612 here in the future if you replace this contract,
        but you might run into msg.sender vs tx.origin issues with the ERC4626 */
    function submitAndDeposit(address recipient) external payable returns (uint256 shares) {
        // Give the frxETH to this contract after it is generated
        _submit(address(this));

        // Approve frxETH to sfrxETH for staking
        frxETHToken.approve(address(sfrxETHToken), msg.value);

        // Deposit the frxETH and give the generated sfrxETH to the final recipient
        uint256 sfrxeth_recieved = sfrxETHToken.deposit(msg.value, recipient);
        require(sfrxeth_recieved > 0, 'No sfrxETH was returned');

        return sfrxeth_recieved;
    }

which calls:

https://github.com/FraxFinance/frxETH-public/blob/7f7731dbc93154131aba6e741b6116da05b25662/src/frxETHMinter.sol#L85

  /// @notice Mint frxETH to the recipient using sender's funds. Internal portion
    function _submit(address recipient) internal nonReentrant {
        // Initial pause and value checks
        require(!submitPaused, "Submit is paused");
        require(msg.value != 0, "Cannot submit 0");

        // Give the sender frxETH
        frxETHToken.minter_mint(recipient, msg.value);

        // Track the amount of ETH that we are keeping
        uint256 withheld_amt = 0;
        if (withholdRatio != 0) {
            withheld_amt = (msg.value * withholdRatio) / RATIO_PRECISION;
            currentWithheldETH += withheld_amt;
        }

        emit ETHSubmitted(msg.sender, recipient, msg.value, withheld_amt);
    }

As we can see, if the witholdRatio is very high, the frax protocol can withdraw a very large of ETH.

Tools Used

Mnaual Review

We recommend the protocol check if the depsoit fee is reasonable when depositing, otherwise, revert the transaction and set the derivative pool weight to 0.

#0 - c4-pre-sort

2023-04-04T21:54:39Z

0xSorryNotSorry marked the issue as primary issue

#1 - c4-sponsor

2023-04-07T20:23:44Z

toshiSat marked the issue as sponsor acknowledged

#2 - Picodes

2023-04-24T18:12:54Z

Related to #549

#3 - c4-judge

2023-04-24T18:19:56Z

Picodes changed the severity to 3 (High Risk)

#4 - c4-judge

2023-04-24T18:23:00Z

Picodes changed the severity to 2 (Med Risk)

#5 - c4-judge

2023-04-24T18:23:14Z

Picodes marked the issue as selected for report

#6 - c4-judge

2023-04-24T20:41:51Z

Picodes marked issue #150 as primary and marked this issue as a duplicate of 150

#7 - c4-judge

2023-04-24T21:20:52Z

Picodes marked the issue as satisfactory

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