LSD Network - Stakehouse contest - yixxas's results

A permissionless 3 pool liquid staking solution for Ethereum.

General Information

Platform: Code4rena

Start Date: 11/11/2022

Pot Size: $90,500 USDC

Total HM: 52

Participants: 92

Period: 7 days

Judge: LSDan

Total Solo HM: 20

Id: 182

League: ETH

Stakehouse Protocol

Findings Distribution

Researcher Performance

Rank: 23/92

Findings: 6

Award: $651.11

🌟 Selected for report: 2

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: ladboy233

Also found by: 0xbepresent, Trust, bitbopper, btk, yixxas

Labels

bug
3 (High Risk)
partial-25
duplicate-110

Awards

55.3657 USDC - $55.37

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LiquidStakingManager.sol#L326-L350

Vulnerability details

Impact

Node runners can withdraw more than 4 ether from the associated smart wallet and I believe only 4 ether should be allowed to be refunded to the node runner.

Proof of Concept

Once this function is called once, bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet, which means that it can only be called once. However, a malicious user can reenter when ether is received as mapping is updated only after rawExecute. Note that even though the comment says "refund 4 ether from smart wallet to node runner's EOA", there is no check to verify that _recipient is not a smart contract address.

LiquidStakingManager.sol#L326-L350

    function withdrawETHForKnot(address _recipient, bytes calldata _blsPublicKeyOfKnot) external {
        require(_recipient != address(0), "Zero address");
        require(isBLSPublicKeyBanned(_blsPublicKeyOfKnot) == false, "BLS public key has already withdrawn or not a part of LSD network");

        address associatedSmartWallet = smartWalletOfKnot[_blsPublicKeyOfKnot];
        require(smartWalletOfNodeRunner[msg.sender] == associatedSmartWallet, "Not the node runner for the smart wallet ");
        require(isNodeRunnerBanned(nodeRunnerOfSmartWallet[associatedSmartWallet]) == false, "Node runner is banned from LSD network");
        require(associatedSmartWallet.balance >= 4 ether, "Insufficient balance");
        require(
            getAccountManager().blsPublicKeyToLifecycleStatus(_blsPublicKeyOfKnot) == IDataStructures.LifecycleStatus.INITIALS_REGISTERED,
            "Initials not registered"
        );

        // refund 4 ether from smart wallet to node runner's EOA
        IOwnableSmartWallet(associatedSmartWallet).rawExecute(
            _recipient,
            "",
            4 ether
        );

        // update the mapping
        bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet;

        emit ETHWithdrawnFromSmartWallet(associatedSmartWallet, _blsPublicKeyOfKnot, msg.sender);
    }

Tools Used

Manual Review

Do the update of the mapping bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet before transferring ether, or add the nonReentrant modifier to this function.

#0 - c4-judge

2022-11-21T12:01:42Z

dmvt marked the issue as duplicate of #113

#1 - c4-judge

2022-11-21T17:01:43Z

dmvt marked the issue as not a duplicate

#2 - c4-judge

2022-11-21T17:01:48Z

dmvt marked the issue as duplicate of #110

#3 - c4-judge

2022-11-30T12:57:17Z

dmvt marked the issue as nullified

#4 - dmvt

2022-11-30T12:57:30Z

Doesn't pass the burden of proof

#5 - yixxas

2022-12-06T19:09:13Z

Hi @dmvt, would like to seek clarifications on why this report is nullified. I believe this is a valid dupe of #110.

The crux of the issue is explained in the report. Because ether is transferred via rawExecute, it gives the attacker the opportunity to reenter at that point. This is possible as the mapping of bannedBLSPublicKeys[_blsPublicKeyOfKnot] = associatedSmartWallet is updated only after rawExecute is called. Another important point is also noted here, the fact that _recipient can be a smart contract and not an EOA as wrongly noted in the comments. Combining all of the above points makes the reentrancy attack possible in withdrawETHForKnot().

#6 - c4-judge

2022-12-07T16:09:55Z

dmvt marked the issue as not nullified

#7 - c4-judge

2022-12-07T16:10:00Z

dmvt marked the issue as partial-25

#8 - dmvt

2022-12-07T16:13:26Z

I've upgraded it to 25% credit to match #330, which I upgraded from QA. Both reports are of roughly the same quality. I am giving partial credit and initially nullified the report because the issue as reported:

Doesn't pass the burden of proof

This is my final ruling.

Findings Information

🌟 Selected for report: aphak5010

Also found by: HE1M, Jeiwan, Trust, yixxas

Labels

bug
2 (Med Risk)
satisfactory
duplicate-92

Awards

88.5851 USDC - $88.59

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/GiantPoolBase.sol#L53

Vulnerability details

Impact

User may have withdrawn an amount that results in the remaining withdrawable amount to be less than the MIN_STAKING_AMOUNT which prevents them from doing withdrawETH(), especially when they do not have enough ether to "top up" to make the minimum withdraw.

Proof of Concept

withdrawETH() forces users to have at least 0.001 ether deposited to be able to withdraw.

require(_amount >= MIN_STAKING_AMOUNT, "Invalid amount")

But if for example users deposits 4 ether, withdraws 3.9995 ether, has 0.0005 ether remaining, the user will not be able to make the withdraw.

Tools Used

Manual Review

withdrawETH() should not require _amount >= MIN_STAKING_AMOUNT. Recommend this check to be removed.

#0 - c4-judge

2022-11-20T23:59:11Z

dmvt marked the issue as duplicate of #98

#1 - c4-judge

2022-11-20T23:59:16Z

dmvt marked the issue as not a duplicate

#2 - c4-judge

2022-11-21T00:00:06Z

dmvt marked the issue as duplicate of #92

#3 - c4-judge

2022-11-29T23:09:37Z

dmvt marked the issue as satisfactory

Findings Information

🌟 Selected for report: HE1M

Also found by: Jeiwan, SmartSek, joestakey, yixxas

Labels

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

Awards

88.5851 USDC - $88.59

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LiquidStakingManager.sol#L308-L321 https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LiquidStakingManager.sol#L289-L303 https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LiquidStakingManager.sol#L435

Vulnerability details

Impact

Node runner can set a smart contract wallet as the smartWalletRepresentative[smartWallet] address. This breaks the underlying assumption of the protocol that this address must be an EOA, as frequently commented in the contract. This can cause or lead to many unintended effects.

Proof of Concept

In registerBLSPublicKeys(), the check is done with require(!Address.isContract(_eoaRepresentative), "Only EOA representative permitted"). However this check is not done in rotateEOARepresentative() hence a user can first register a key with an EOA account, then rotate it to a smart contract wallet with rotateEOARepresentative() to reach the mentioned state.

Tools Used

Manual Review

The same check needs to be done in both rotateEOARepresentative() and rotateEOARepresentativeOfNodeRunner().

#0 - c4-judge

2022-11-21T12:12:28Z

dmvt marked the issue as primary issue

#1 - dmvt

2022-11-21T12:13:16Z

I feel compelled to mention that the mitigation in both this report and #93 is itself a bug, but the underlying reported concern is accurate.

#2 - c4-sponsor

2022-11-28T17:23:58Z

vince0656 marked the issue as sponsor confirmed

#3 - c4-judge

2022-11-30T12:24:54Z

dmvt marked the issue as satisfactory

#4 - C4-Staff

2022-12-21T05:53:33Z

JeeberC4 marked the issue as duplicate of #93

Findings Information

🌟 Selected for report: aphak5010

Also found by: arcoun, datapunk, unforgiven, wait, yixxas

Labels

bug
2 (Med Risk)
satisfactory
duplicate-98

Awards

66.4388 USDC - $66.44

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/GiantPoolBase.sol#L69-L90

Vulnerability details

Impact

A user can be tricked to call withdrawLPTokens() with a malicious token address that is not part of the LPToken issued by the lptokenFactory which can potentially cause them to lose all their giant LP token.

Proof of Concept

withdrawLPTokens() does not check that _lpTokens is a token that is issued by lptokenFactory. This token can be one that is not registered as part of any BLS public key. A malicious user can use this fact and create a "fake" LPToken, send it into the contract and trick a user to call this function with this fake token as input. End result is that users will be transferred this fake token which has no value in the stakehouse ecosystem, but burn their valuable giant LP token.

Tools Used

Manual Review

A check should be added in withdrawLPTokens() to ensure that tokens being used are created by the lptokenFactory.

#0 - c4-judge

2022-11-21T00:01:11Z

dmvt marked the issue as duplicate of #98

#1 - c4-judge

2022-11-30T12:33:38Z

dmvt marked the issue as satisfactory

Findings Information

🌟 Selected for report: yixxas

Also found by: CloudX, ladboy233

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor disputed
M-17

Awards

236.9561 USDC - $236.96

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LiquidStakingManager.sol#L435

Vulnerability details

Impact

The underlying assumption of eoaRepresentative being an EOA can be untrue. This can cause many unintended effects as the contract comments strongly suggests that this must be an EOA account.

Proof of Concept

When BLS public key is registered in registerBLSPublicKeys(), it has the check of

require(!Address.isContract(_eoaRepresentative), "Only EOA representative permitted")

However, this check can be passed even though input is a smart contract if

  1. Function is called in the constructor. Address.isContract() checks for the code length, but during construction code length is 0.
  2. Smart contract that has not been deployed yet can be used. The CREATE2 opcode can be used to deterministically calculate the address of a smart contract before it is created. This means that the user can bypass this check by calling this function before deploying the contract.

Tools Used

Manual Review

It is generally not recommended to enforce an address to be only EOA and AFAIK, this is impossible to enforce due to the aforementioned cases. I recommend the protocol team to take a closer look at this and build the protocol with the assumption that _eoaRepresentative == EOA.

#0 - c4-judge

2022-11-21T12:21:40Z

dmvt marked the issue as duplicate of #109

#1 - c4-judge

2022-11-21T16:56:29Z

dmvt marked the issue as not a duplicate

#2 - c4-judge

2022-11-21T16:56:42Z

dmvt marked the issue as duplicate of #108

#3 - c4-judge

2022-11-24T09:44:19Z

dmvt marked the issue as selected for report

#4 - c4-sponsor

2022-11-28T17:17:45Z

vince0656 marked the issue as sponsor disputed

#5 - vince0656

2022-11-28T17:18:31Z

Using tx.origin is generally frowned upon

#6 - dmvt

2022-11-30T12:50:25Z

The sponsor confirming that they know it's an issue does not invalidate it as an issue.

#7 - c4-judge

2022-11-30T12:51:08Z

dmvt marked the issue as satisfactory

#8 - trust1995

2022-12-06T21:49:06Z

Report does not explain what "unintended side effects" happen when address is not EOA. In order to be considered a M level report, warden should provide some real impact , otherwise it is a QA level suggestion. Multiple examples of this exact issue judged QA: https://github.com/code-423n4/2022-10-blur-findings/issues/139 https://github.com/code-423n4/2022-10-holograph-findings/issues/63 https://github.com/code-423n4/2021-12-maple-findings/issues/72 @GalloDaSballo

#9 - dmvt

2022-12-07T10:31:18Z

See my response in the post-judging qa discussion.

#10 - C4-Staff

2022-12-21T05:41:24Z

JeeberC4 marked the issue as not a duplicate

#11 - C4-Staff

2022-12-21T05:41:36Z

JeeberC4 marked the issue as primary issue

Findings Information

🌟 Selected for report: yixxas

Also found by: cccz, joestakey, pashov, sahar

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor disputed
M-18

Awards

115.1607 USDC - $115.16

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LiquidStakingManager.sol#L948-L955

Vulnerability details

Impact

Node runners can have all their stake rewards taken by the DAO as commissions can be set to a 100%.

Proof of Concept

There is no limits on _updateDAORevenueCommission() except not exceeding MODULO, which means it can be set to a 100%.

LiquidStakingManager.sol#L948-L955

    function _updateDAORevenueCommission(uint256 _commissionPercentage) internal {
        require(_commissionPercentage <= MODULO, "Invalid commission");

        emit DAOCommissionUpdated(daoCommissionPercentage, _commissionPercentage);

        daoCommissionPercentage = _commissionPercentage;
    }

This percentage is used to calculate uint256 daoAmount = (_received * daoCommissionPercentage) / MODULO in _calculateCommission(). Remaining is then caculated with uint256 rest = _received - daoAmount, and in this case rest = 0. When node runner calls claimRewardsAsNodeRunner(), the node runner will receive 0 rewards.

Tools Used

Manual Review

There should be maximum cap on how much commission DAO can take from node runners.

#0 - c4-judge

2022-11-21T12:28:18Z

dmvt marked the issue as primary issue

#1 - c4-sponsor

2022-11-28T17:11:23Z

vince0656 marked the issue as sponsor disputed

#2 - vince0656

2022-12-02T12:36:14Z

Node runners can see ahead of time what the % commission is and therefore, they can make a decision based on that. However, on reflection, a maximum amount is not a bad idea

#3 - dmvt

2022-12-02T17:17:37Z

I will leave this in place as I think it's a valid concern. If the DAO is compromised (specifically included in scope), the impact is felt immediately and applies to all unclaimed rewards. The node runners can't necessarily see a high fee rate coming in advance.

#4 - c4-judge

2022-12-02T17:19:30Z

dmvt marked the issue as satisfactory

#5 - c4-judge

2022-12-02T17:19:34Z

dmvt marked the issue as selected for report

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