LSD Network - Stakehouse contest - Lambda'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: 10/92

Findings: 8

Award: $2,393.71

🌟 Selected for report: 2

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: ronnyx2017

Also found by: 9svR6w, HE1M, Lambda, Trust, rotcivegaf

Labels

bug
3 (High Risk)
satisfactory
upgraded by judge
duplicate-116

Awards

221.4628 USDC - $221.46

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/StakingFundsVault.sol#L337

Vulnerability details

Impact

StakingFundsVault.beforeTokenTransfer only checks if the _from address is not equal to address(0), but does not perform the same check for the _to address:

// distribute any due rewards for the `from` user
if (_from != address(0)) {
    _distributeETHRewardsToUserForToken(_from, address(token), token.balanceOf(_from), _from);
}

// in case the new user has existing rewards - give it to them so that the after transfer hook does not wipe pending rewards
_distributeETHRewardsToUserForToken(_to, address(token), token.balanceOf(_to), _to);

This can be very problematic when tokens are burned. Then, StakingFundsVault.beforeTokenTransfer will be called with a _recipient address (fourth parameter) of address(0) and this address will be passed to _distributeETHRewardsToUserForToken. However, this function reverts when the _to address is zero:

require(_recipient != address(0), "Zero address");

Therefore, the burn operation fails.

Proof Of Concept

This behavior can be seen in the test testBurnLPForETHWorksAsExpected:

│ │ ├─ [6740] LPToken::burn(0x2546BcD3c84621e976D8185a91A922aE77ECEc30, 4000000000000000000) [delegatecall] │ │ │ ├─ [1791] MockStakingFundsVault::beforeTokenTransfer(0x2546BcD3c84621e976D8185a91A922aE77ECEc30, 0x0000000000000000000000000000000000000000, 4000000000000000000) │ │ │ │ ├─ [636] 0xCF8cBf15457977aeCe0e243c2584B038B6Ac6933::syndicate() [staticcall] │ │ │ │ │ ├─ [470] MockLiquidStakingManager::syndicate() [delegatecall] │ │ │ │ │ │ └─ ← 0x0000000000000000000000000000000000000000

StakingFundsVault.beforeTokenTransfer is called with address(0) as the second parameter. However, because syndicate is set to address(0) in this test, the logic is not triggered and the test still passes.

Only call _distributeETHRewardsToUserForToken when _to is not zero.

#0 - c4-judge

2022-11-20T23:18:57Z

dmvt marked the issue as duplicate of #64

#1 - c4-judge

2022-11-21T16:30:50Z

dmvt marked the issue as not a duplicate

#2 - c4-judge

2022-11-21T16:40:32Z

dmvt marked the issue as duplicate of #60

#3 - c4-judge

2022-11-30T11:25:17Z

dmvt changed the severity to 3 (High Risk)

#4 - c4-judge

2022-11-30T11:32:03Z

dmvt marked the issue as satisfactory

#5 - C4-Staff

2022-12-22T06:52:25Z

liveactionllama marked the issue as not a duplicate

#6 - C4-Staff

2022-12-22T06:52:34Z

liveactionllama marked the issue as duplicate of #116

Findings Information

🌟 Selected for report: ronnyx2017

Also found by: Lambda

Labels

bug
3 (High Risk)
partial-50
satisfactory
sponsor confirmed
upgraded by judge
duplicate-173

Awards

506.3164 USDC - $506.32

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/SyndicateRewardsProcessor.sol#L79

Vulnerability details

Impact

SyndicateRewardsProcessor._updateAccumulatedETHPerLP performs the following calculation:

uint256 unprocessed = received - totalETHSeen;

received is the result of totalRewardsReceived(), i.e. the following:

function totalRewardsReceived() public virtual view returns (uint256) {
        return address(this).balance + totalClaimed;
}

totalETHSeen is set to the received value of the last _updateAccumulatedETHPerLP call.

The problem with this logic is that it assumes that totalRewardsReceived() is increasing, which is not always the case. When ETH is withdrawn using StakingFundsVault.withdrawETH, the ETH balance decreases, while totalClaimed does not increase (which it does when ETH is transferred in burnLPForETH).

Proof Of Concept

As described above, the problem can occur after stake operations when the ETH balance is reduced. When this happens, all succesive calls to _updateAccumulatedETHPerLP (which is called often in StakingFundsVault) will revert.

Handle the case of ETH withdrawals correctly

#0 - c4-judge

2022-11-20T23:12:09Z

dmvt marked the issue as primary issue

#1 - c4-sponsor

2022-11-28T17:42:41Z

vince0656 marked the issue as sponsor confirmed

#2 - c4-judge

2022-11-30T14:04:43Z

dmvt changed the severity to 3 (High Risk)

#3 - c4-judge

2022-11-30T14:04:55Z

dmvt marked the issue as partial-50

#4 - c4-judge

2022-11-30T14:05:01Z

dmvt marked the issue as satisfactory

#5 - c4-judge

2022-12-15T09:29:21Z

dmvt marked the issue as full credit

#6 - c4-judge

2022-12-15T09:30:19Z

dmvt marked the issue as duplicate of #173

#7 - c4-judge

2022-12-15T09:30:25Z

dmvt marked the issue as partial-50

Awards

11.192 USDC - $11.19

Labels

bug
3 (High Risk)
satisfactory
duplicate-251

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantMevAndFeesPool.sol#L42 https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantSavETHVaultPool.sol#L48

Vulnerability details

Impact

In batchDepositETHForStaking, the user provides the addresses of the staking funds vaults in the array _stakingFundsVault. The system then checks that the liquid staking network manager that is returned by this vault is registered. If this check succeeds, the requested ETH amount is sent to the provided address (with a call to batchDepositETHForStaking):

StakingFundsVault sfv = StakingFundsVault(payable(_stakingFundsVault[i]));
            require(
                liquidStakingDerivativeFactory.isLiquidStakingManager(address(sfv.liquidStakingNetworkManager())),
                "Invalid liquid staking manager"
            );

            sfv.batchDepositETHForStaking{ value: _ETHTransactionAmounts[i] }(
                _blsPublicKeyOfKnots[i],
                _amounts[i]
            );

An attacker can exploit this by providing a malicious fake staking funds vault. This smart contract returns a valid liquid staking manager address when liquidStakingNetworkManager() is called. However, when batchDepositETHForStaking is called, it simply consumes all the ETH.

Proof Of Concept

A contract to exploit this vulnerability could look like this:

contract AttackerContract {
	function liquidStakingNetworkManager() public view returns (address) {
		return validLiquidStakingManager; // Some stored valid liquid staking manager address
	}

	function batchDepositETHForStaking(bytes[] calldata _blsPublicKeyOfKnots, uint256[] calldata _amounts) payable public {
		// Do nothing, just accept the ETH
	}
}

The attacker would deploy this contract and pass the address in the _stakingFundsVault array. Furthermore, he would pass in the whole ETH balance in the corresponding _ETHTransactionAmounts entry to drain the contract.

Validate that the provided vault is a registered vault.

#0 - c4-judge

2022-11-20T22:37:15Z

dmvt marked the issue as duplicate of #36

#1 - c4-judge

2022-11-29T15:36:38Z

dmvt marked the issue as satisfactory

#2 - C4-Staff

2022-12-21T05:40:16Z

JeeberC4 marked the issue as duplicate of #36

#3 - liveactionllama

2022-12-22T08:47:06Z

Duplicate of #251

Findings Information

🌟 Selected for report: 0xdeadbeef0x

Also found by: HE1M, JTJabba, Jeiwan, Lambda, Trust, V_B, aphak5010, hihen, joestakey, minhtrng, unforgiven

Labels

bug
2 (Med Risk)
satisfactory
duplicate-49

Awards

17.6542 USDC - $17.65

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantLP.sol#L45 https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/LPToken.sol#L68 https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantPoolBase.sol#L96 https://github.com/code-423n4/2022-11-stakehouse/blob/a0558ed7b12e1ace1fe5c07970c7fc07eb00eebd/contracts/liquid-staking/ETHPoolLPFactory.sol#L82

Vulnerability details

Impact

After a token transfer, lastInteractedTimestamp[_to] is set to the current block timestamp. The system checks this timestamp in multiple places to ensure that the most recent transfer / interaction is not too recent. For instance, in GiantPoolBase._assertUserHasEnoughGiantLPToClaimVaultLP, the following check is performed:

require(lpTokenETH.lastInteractedTimestamp(msg.sender) + 1 days < block.timestamp, "Too new");

This can be exploited by an attacker to grief (and potentially blackmail) users: Whenever a user wants to perform an action with such a check, the attacker transfers an extremely small amount (1 wei) of his LP tokens to this user such that his lastInteractedTimestamp is reset. Like that, the action always fails.

Proof Of Concept

Alice wants to withdraw vault LP tokens by burning her giant LP tokens and calls withdrawLPTokens to do so. The attacker Charlie frontsrun this call and transfers 1 Wei of lpTokenETH to Alice before the call. Because of this, the withdrawal will fail. An attacker can repeat this attack multiple times to prevent all withdrawals.

Only set lastInteractedTimestamp when the amount that was transferred is greater than some minimum amount.

#0 - c4-judge

2022-11-20T22:36:57Z

dmvt marked the issue as duplicate of #59

#1 - c4-judge

2022-11-21T15:54:28Z

dmvt marked the issue as not a duplicate

#2 - c4-judge

2022-11-21T15:54:34Z

dmvt marked the issue as duplicate of #49

#3 - c4-judge

2022-11-29T22:47:30Z

dmvt marked the issue as satisfactory

Findings Information

🌟 Selected for report: Lambda

Also found by: bearonbike

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
M-11

Awards

394.9268 USDC - $394.93

External Links

Lines of code

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

Vulnerability details

Impact

In registerBLSPublicKeys, it should be checked (according to the comment and error) if a BLS public key is part of the LSD network and not banned:

// check if the BLS public key is part of LSD network and is not banned
require(isBLSPublicKeyPartOfLSDNetwork(_blsPublicKey) == false, "BLS public key is banned or not a part of LSD network");

However, this is not actually checked. The function isBLSPublicKeyPartOfLSDNetwork only checks if the public key is part of the LSD network:

function isBLSPublicKeyPartOfLSDNetwork(bytes calldata _blsPublicKeyOfKnot) public virtual view returns (bool) {
        return smartWalletOfKnot[_blsPublicKeyOfKnot] != address(0);
}

The function isBLSPublicKeyBanned would perform both checks and should be called here:

function isBLSPublicKeyBanned(bytes calldata _blsPublicKeyOfKnot) public virtual view returns (bool) {
        return !isBLSPublicKeyPartOfLSDNetwork(_blsPublicKeyOfKnot) || bannedBLSPublicKeys[_blsPublicKeyOfKnot] != address(0);
}

Because of that, it is possible to pass banned BLS public keys to registerBLSPublicKeys and the call will succeed.

Use isBLSPublicKeyBanned instead of isBLSPublicKeyPartOfLSDNetwork.

#0 - c4-judge

2022-11-20T23:23:34Z

dmvt marked the issue as primary issue

#1 - c4-sponsor

2022-11-28T17:42:23Z

vince0656 marked the issue as sponsor confirmed

#2 - c4-judge

2022-11-30T14:06:16Z

dmvt marked the issue as satisfactory

#3 - c4-judge

2022-11-30T14:06:22Z

dmvt marked the issue as selected for report

Findings Information

🌟 Selected for report: Lambda

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
M-12

Awards

877.6151 USDC - $877.62

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/a0558ed7b12e1ace1fe5c07970c7fc07eb00eebd/contracts/liquid-staking/LiquidStakingManager.sol#L882 https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/syndicate/Syndicate.sol#L22

Vulnerability details

Impact

LiquidStakingManager._autoStakeWithSyndicate always stakes a fixed amount of 12 ETH. However, Syndicate.stake only allows a total staking amount of 12 ETH and reverts otherwise:

if (_sETHAmount + totalStaked > 12 ether) revert InvalidStakeAmount();

An attacker can abuse this and front-run calls to mintDerivatives (which call _autoStakeWithSyndicate internally). Because Syndicate.stake can be called by everyone, he can stake the minimum amount (1 gwei) such that the mintDerivatives call fails.

Proof Of Concept

As soon as there is a mintDerivatives call in the mempool, an attacker (that owns sETH) calls Syndicate.stake with an amount of 1 gwei. _autoStakeWithSyndicate will still call Syndicate.stake with 12 ether. However, _sETHAmount + totalStaked > 12 ether will then be true, meaning that the call will revert.

Only allow staking through the LiquidStakingManager, i.e. add access control to Syndicate.stake.

#0 - c4-judge

2022-11-20T23:31:38Z

dmvt marked the issue as primary issue

#1 - c4-sponsor

2022-11-28T17:36:06Z

vince0656 marked the issue as sponsor confirmed

#2 - c4-judge

2022-11-30T14:08:12Z

dmvt marked the issue as selected for report

#3 - c4-judge

2022-11-30T14:08:17Z

dmvt marked the issue as satisfactory

#4 - trust1995

2022-12-06T22:09:28Z

Classic Lambda find, awesome as usual.

Findings Information

🌟 Selected for report: hihen

Also found by: Lambda, Trust

Labels

bug
2 (Med Risk)
judge review requested
satisfactory
duplicate-382

Awards

182.2739 USDC - $182.27

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantPoolBase.sol#L96 https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/liquid-staking/GiantSavETHVaultPool.sol#L87

Vulnerability details

Impact

The loop within GiantPoolBase.withdrawLPTokens calls the function _assertUserHasEnoughGiantLPToClaimVaultLP for every token in the _lpTokens array. This function checks that lpTokenETH.lastInteractedTimestamp(msg.sender) is older than 1 day. This logic works fine when only one token is in the _lpTokens array. However, when it contains multiple ones, the withdrawal will fail. This happens because the lpTokenETH.burn(msg.sender, amount) call (in the first loop iteration) resets the lastInteractedTimestamp for msg.sender, as a burn also triggers GiantLP._afterTokenTransfer, which sets these values:

function _afterTokenTransfer(address _from, address _to, uint256 _amount) internal override {
        lastInteractedTimestamp[_from] = block.timestamp;
        lastInteractedTimestamp[_to] = block.timestamp;
        if (address(transferHookProcessor) != address(0)) ITransferHookProcessor(transferHookProcessor).afterTokenTransfer(_from, _to, _amount);
}

Therefore, all burns where multiple LP tokens are provided fail.

Note that this problem is also present in GiantSavETHVaultPool.

Proof Of Concept

A user calls withdrawLPTokens and passes two LP tokens in the _lpTokens array. lpTokenETH.lastInteractedTimestamp(msg.sender) is older than 1 day. However, after the first lpTokenETH.burn(msg.sender, amount) call, it will be set to the current block.timestamp, meaning that the second time _assertUserHasEnoughGiantLPToClaimVaultLP is called, this check fails (although it should not):

require(lpTokenETH.lastInteractedTimestamp(msg.sender) + 1 days < block.timestamp, "Too new");

Only check once if lpTokenETH.lastInteractedTimestamp(msg.sender) + 1 days < block.timestamp.

#0 - c4-judge

2022-11-20T23:28:54Z

dmvt marked the issue as primary issue

#1 - vince0656

2022-11-28T17:41:32Z

Dupe of #382

#2 - c4-sponsor

2022-11-28T17:41:36Z

vince0656 requested judge review

#3 - c4-judge

2022-11-30T14:07:49Z

dmvt marked the issue as satisfactory

#4 - C4-Staff

2022-12-21T00:09:01Z

JeeberC4 marked the issue as duplicate of #382

Findings Information

🌟 Selected for report: Trust

Also found by: Lambda, datapunk

Labels

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

Awards

182.2739 USDC - $182.27

External Links

Lines of code

https://github.com/code-423n4/2022-11-stakehouse/blob/a0558ed7b12e1ace1fe5c07970c7fc07eb00eebd/contracts/liquid-staking/ETHPoolLPFactory.sol#L111 https://github.com/code-423n4/2022-11-stakehouse/blob/23c3cf65975cada7fd2255a141b359a6b31c2f9c/contracts/syndicate/Syndicate.sol#L215

Vulnerability details

Impact

In ETHPoolLPFactory._depositETHForStaking, a minimum deposit amount (set to 0.001 ether) is required:

require(_amount >= MIN_STAKING_AMOUNT, "Min amount not reached");

On the other hand, the maximum amount is also restricted such that maxStakingAmountPerValidator will not be exceeded:

require(lpToken.totalSupply() + _amount <= maxStakingAmountPerValidator, "Amount exceeds the staking limit for the validator");

This combination of checks can lead to situations where maxStakingAmountPerValidator is never reachable, namely when lpToken.totalSupply() > maxStakingAmountPerValidator - MIN_STAKING_AMOUNT. Every deposit that is larger than MIN_STAKING_AMOUNT will revert because it would exceed the maximum staking amount and every deposit that is smaller reverts because this is the enforced minimum.

Note that the same problem also can occur in Syndicate.stake where a minimum stake amount of 1 gwei is enforced, but the total staked amount cannot exceed 12 ether.

Proof Of Concept

Let's say that a StakingFundsVault for a given BLS public key has already 4 - 0.001 ether and 1 wei. When a user now wants to deposit < 0.001 ether, the call reverts because the minimum amount is not reached. For more than 0.001 ether, the call reverts because this would exceed the staking limit.

Do not allow deposits that bring the total supply into the range [maxStakingAmountPerValidator - MIN_STAKING_AMOUNT, maxStakingAmountPerValidator - 1].

#0 - c4-judge

2022-11-20T22:39:43Z

dmvt marked the issue as primary issue

#1 - c4-judge

2022-11-20T22:40:29Z

dmvt changed the severity to G (Gas Optimization)

#2 - dmvt

2022-11-20T23:10:23Z

Note, this should be medium, not Gas. Accidental button click on my end.

#3 - Simon-Busch

2022-11-21T06:06:48Z

@dmvt Fixed and revert as per your request

#4 - c4-sponsor

2022-11-28T17:43:00Z

vince0656 marked the issue as sponsor confirmed

#5 - c4-judge

2022-11-30T14:00:33Z

dmvt marked the issue as satisfactory

#6 - C4-Staff

2022-12-21T00:06:57Z

JeeberC4 marked the issue as duplicate of #422

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