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
Rank: 85/92
Findings: 2
Award: $50.87
🌟 Selected for report: 0
🚀 Solo Findings: 0
17.6542 USDC - $17.65
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.
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.
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
🌟 Selected for report: ladboy233
Also found by: 0xdeadbeef0x, SaeedAlipoor01988, bin2chen, immeas, minhtrng
33.2194 USDC - $33.22
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
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
.
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.
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