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

Findings: 2

Award: $50.87

🌟 Selected for report: 0

🚀 Solo Findings: 0

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/4b6828e9c807f2f7c569e6d721ca1289f7cf7112/contracts/liquid-staking/LPToken.sol#L66-L70

Vulnerability details

Impact

The LP tokens have a mechanism that prevent certain interactions if some LP tokens have been recently added to the calling address. This can be used by a malicious actor to grief an address by sending tiny amounts of LP tokens to that address, either in certain intervals or by watching the mempools for transactions from that address and frontrunning those.

The features that are affected are claiming rewards, burning LPs for Ether, rotating LP tokens, and withdrawing LP tokens.

Proof of Concept

The LPToken._afterTokenTransfer hook sets the lastInteractedTimestamp for both _from and _to:

lastInteractedTimestamp[_from] = block.timestamp;
lastInteractedTimestamp[_to] = block.timestamp;

This lastInteractedTimestamp is used to restrict the execution of certain functions such as SavEthVault.burnLPToken:

bool isStaleLiquidity = _lpToken.lastInteractedTimestamp(msg.sender) + 30 minutes < block.timestamp;
...
require(isStaleLiquidity, "Liquidity is still fresh");

This allows transfers of tiny amounts to an address to prevent that address from calling functions that use the lastInteractedTimestamp in require statements.

Tools Used

Manual Review

A workaround could be by involving a checkpointing mechanism that stores balances for a blocknumber whenever the balance changes. However that would increase implementation complexity by a fair margin.

#0 - c4-judge

2022-11-21T22:55:18Z

dmvt marked the issue as duplicate of #49

#1 - c4-judge

2022-11-29T22:43:36Z

dmvt marked the issue as satisfactory

Findings Information

🌟 Selected for report: ladboy233

Also found by: 0xdeadbeef0x, SaeedAlipoor01988, bin2chen, immeas, minhtrng

Labels

bug
2 (Med Risk)
grade-b
partial-50
duplicate-132

Awards

33.2194 USDC - $33.22

External Links

Lines of code

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

Vulnerability details

Impact

The totalSupply of StakingFundsVault.lpTokenForKnot can be increased above the intended limit of 4 ether worth of deposits. This can be used to permanently DOS the staking of a certain blsPublicKey.

Proof of Concept

The LiquidStakingManager.stake function calls _assertEtherIsReadyForValidatorStaking(blsPubKey); to check if enough ether has been deposited in the corresponding vaults for staking. This function performs the following check:

LPToken stakingFundsLP = stakingFundsVault.lpTokenForKnot(blsPubKey);
...
require(stakingFundsLP.totalSupply() == 4 ether, "DAO staking funds vault balance must be at least 4 ether");

The ETHPoolLPFactory (which StakingFundsVault inherits from) limits the minting of LP above 4 ether by using the following check in _depositETHForStaking:

// maxStakingAmountPerValidator is set to 4 ether in StakingFundsVault._init
require(lpToken.totalSupply() + _amount <= maxStakingAmountPerValidator,

However the limit can be circumvented by using the ETHPoolLPFactory.rotateLPTokens function (which is public and inherited by StakingFundsVault):

require(_amount + _newLPToken.totalSupply() <= 24 ether, "Not enough mintable tokens");
...
_oldLPToken.burn(msg.sender, _amount);
...
_newLPToken.mint(msg.sender, _amount);

The problem is that the limit of the totalSupply is hardcoded as 24 ether, which is the limit that is intended for SavETHVault but not for StakingFundsVault as pointed out above.

Hence a malicious actor can deposit at least 0.0001 Ether once the deposits in StakingFundsVault hit 4 ether to DOS the LiquidStakingManager.stake function by using the rotateLP function and burning the LP tokens from another token contract.

To permanently DOS the staking for a specific blsPublicKey the malicious actor could also deposit more than 4 ether in total by himself. This way it would not be possible for others to withdraw deposits and redeposit the difference necessary to hit exactly 4 ether worth of deposits.

Tools Used

Manual Review

In the function ETHPoolLPFactory.rotateLPTokens change the hardcoded 24 ether to use maxStakingAmountPerValidator instead.

Alternatively, change the check require(stakingFundsLP.totalSupply() == 4 ether from == to >=.

#0 - c4-judge

2022-11-21T21:15:07Z

dmvt marked the issue as duplicate of #188

#1 - dmvt

2022-12-02T17:37:38Z

On further review, I will downgrade all of these to QA. The attack described requires 1 wei over 4 ETH of the attacker's funds to be locked and has minimal impact on the node runner. They could use a new key. While annoying, this is easy to mitigate, and the griefing attack has a very high cost.

#2 - c4-judge

2022-12-02T17:37:52Z

dmvt changed the severity to QA (Quality Assurance)

#3 - c4-judge

2022-12-02T17:37:56Z

dmvt marked the issue as grade-b

#4 - Minh-Trng

2022-12-02T18:23:02Z

On further review, I will downgrade all of these to QA. The attack described requires 1 wei over 4 ETH of the attacker's funds to be locked and has minimal impact on the node runner. They could use a new key. While annoying, this is easy to mitigate, and the griefing attack has a very high cost.

fair enough, though #132 and this submission (and the other duplicate) describe the same issue and even the mitigation matches, so they should be put on an equal severity, be it QA or medium

Edit: quick remark about the cost: its only 4.0001 eth to permanently DOS a public key. the attack will be significantly cheaper if the attacker watches the mempool and frontruns the staking call just in time and increases the staked amount by 0.0001 ether

#5 - dmvt

2022-12-03T11:31:32Z

Nice catch. I had missed it due to the different approaches of the report (accidental vs attack) and the line numbers in question being quite different. I'm going to reassign these (#271 & #326) as duplicates of #132, but with partial credit.

#6 - Simon-Busch

2022-12-04T06:41:40Z

Change severity to Medium as requested by @dmvt

#7 - c4-judge

2022-12-05T10:15:12Z

dmvt marked the issue as not a duplicate

#8 - c4-judge

2022-12-05T10:15:40Z

dmvt marked the issue as duplicate of #118

#9 - c4-judge

2022-12-05T10:15:45Z

dmvt marked the issue as partial-50

#10 - C4-Staff

2022-12-21T05:49:14Z

JeeberC4 marked the issue as duplicate of #132

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