Stader Labs - dwward3n's results

Decentralized ETH liquid staking protocol with 4 ETH bond for anyone to be a node operator.

General Information

Platform: Code4rena

Start Date: 02/06/2023

Pot Size: $100,000 USDC

Total HM: 15

Participants: 75

Period: 7 days

Judge: Picodes

Total Solo HM: 5

Id: 249

League: ETH

Stader Labs

Findings Distribution

Researcher Performance

Rank: 2/75

Findings: 3

Award: $8,210.54

🌟 Selected for report: 1

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: broccolirob

Also found by: 0x70C9, bin2chen, dwward3n, hals

Labels

bug
3 (High Risk)
satisfactory
duplicate-418

Awards

2059.0426 USDC - $2,059.04

External Links

Lines of code

https://github.com/code-423n4/2023-06-stader/blob/main/contracts/VaultProxy.sol#L17

Vulnerability details

Impact

Attacker can selfdestruct VaultProxy deployment.

constructor() {} //initialise the vault proxy with data function initialise( bool _isValidatorWithdrawalVault, uint8 _poolId, uint256 _id, address _staderConfig ) external { if (isInitialized) { revert AlreadyInitialized(); } UtilLib.checkNonZeroAddress(_staderConfig); isValidatorWithdrawalVault = _isValidatorWithdrawalVault; isInitialized = true; poolId = _poolId; id = _id; staderConfig = IStaderConfig(_staderConfig); owner = staderConfig.getAdmin(); } /**route all call to this proxy contract to the respective latest vault contract * fetched from staderConfig. This approch will help in changing the implementation * of validatorWithdrawalVault/nodeELRewardVault for already deployed vaults*/ fallback(bytes calldata _input) external payable returns (bytes memory) { address vaultImplementation = isValidatorWithdrawalVault ? staderConfig.getValidatorWithdrawalVaultImplementation() : staderConfig.getNodeELRewardVaultImplementation(); (bool success, bytes memory data) = vaultImplementation.delegatecall(_input); if (!success) { revert(string(data)); } return data; }

VaultProxy is first deployed using deploy script (deployContracts.ts). Since anyone can call initialise, attacker can craft malicious parameters for later fallback call. In the fallback, implementation address is get from staderConfig which is initialized in initialise() thus attacker have full control over it. Attacker can deploy a contract containing selfdestruct and call it via fallback.

Since VaultProxy is used in VaultFactory for deploying vaults, this will cause service disrupture. Admin can try to solve this by VaultFactory::updateVaultProxyAddress with another VaultProxy, but because VaultProxy::initialise can be frontrun, determined attacker can destroy that VaultProxy too.

Proof of Concept

contract Destructible { function exit() external { selfdestruct(payable(msg.sender)); } } contract FakeStaderConfig { ... function getValidatorWithdrawalVaultImplementation() external returns (address) { // return address of deployed Destructible ... } } interface IVaultProxy { function initialise(bool, uint, uint, address) external; } contract Attack { ... function attack() external { IVaultProxy(vaultProxy).initialise( true, // _isValidatorWithdrawalVault 0, // any 0, // any ... // address of deployed FakeStaderConfig ); vaultProxy.call(abi.encodeWithSelector(Destructible.exit.selector)); } }

Tools Used

Manual

Add guard for VaultProxy::initialise.

>> address immutable factory; >> constructor() { factory = msg.sender; } //initialise the vault proxy with data function initialise( bool _isValidatorWithdrawalVault, uint8 _poolId, uint256 _id, address _staderConfig ) external { >> require(msg.sender == factory); if (isInitialized) { revert AlreadyInitialized(); }

Assessed type

call/delegatecall

#0 - c4-judge

2023-06-10T10:48:48Z

Picodes marked the issue as duplicate of #167

#1 - c4-judge

2023-06-26T15:49:40Z

Picodes marked the issue as satisfactory

Findings Information

🌟 Selected for report: dwward3n

Labels

bug
2 (Med Risk)
satisfactory
selected for report
sponsor confirmed
edited-by-warden
M-05

Awards

6119.6969 USDC - $6,119.70

External Links

Lines of code

https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderOracle.sol#L148 https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderOracle.sol#L290

Vulnerability details

Impact

if ( submissionCount == trustedNodesCount / 2 + 1 && _exchangeRate.reportingBlockNumber > exchangeRate.reportingBlockNumber ) { updateWithInLimitER( _exchangeRate.totalETHBalance, _exchangeRate.totalETHXSupply, _exchangeRate.reportingBlockNumber ); }

In submitExchangeRateData, consensus is reached if submissionCount is strictly equal to desired number. However, trustNodesCount can be decreased and this condition can be never met.

if ((submissionCount == (2 * trustedNodesCount) / 3 + 1)) { lastReportedSDPriceData = _sdPriceData; lastReportedSDPriceData.sdPriceInETH = getMedianValue(sdPrices); delete sdPrices;

In submitSDPrice, if this case happens, sdPrices doesn't get deleted and it will affect the next submission batch's price.

Proof of Concept

In the above snippet, let's assume trustedNodesCount = 10, submissionCount = 5. The condition doesn't meet for now (5 != 10/2+1). Then trustedNodesCount decreases to 9. Next time when a node submits, trustedNodesCount = 9, submissionCount = 6. Then the condition cannot be met since 6 != 9/2+1.

Tools Used

Manual

Replace strict equal with equal or greater than. Or replace it with greater than and decrease the right side.

Not sure about adding cooldown for add/remove trusted nodes.

Assessed type

Other

#0 - manoj9april

2023-06-20T08:04:29Z

Thanks, we will fix this.

#1 - c4-sponsor

2023-06-20T08:04:47Z

manoj9april marked the issue as sponsor confirmed

#2 - c4-judge

2023-07-02T10:06:03Z

Picodes marked the issue as satisfactory

#3 - Picodes

2023-07-02T10:19:45Z

Keeping medium severity because of the sponsor's label, but the justification of why it qualifies for Med and what the impacts of the bug are is insufficient.

For most functions it seems the submissionCount depends on the reportingBlockNumber so the impact would only be one period where the oracle couldn't be updated, which doesn't qualify without additional justification. For submitSocializingRewardsMerkleRoot and submitMissedAttestationPenalties for example the impact may be more important.

#4 - sanjay-staderlabs

2023-07-13T04:18:18Z

This is fixed

Awards

31.7954 USDC - $31.80

Labels

bug
2 (Med Risk)
satisfactory
duplicate-15

External Links

Lines of code

https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderOracle.sol#L646 https://github.com/code-423n4/2023-06-stader/blob/main/contracts/StaderOracle.sol#L648

Vulnerability details

Impact

In StaderOracle::getPORFeedData, latestRoundData is used, but there is no check if the return value indicates stale data.

(, int256 totalETHBalanceInInt, , , ) = AggregatorV3Interface(staderConfig.getETHBalancePORFeedProxy()) .latestRoundData(); (, int256 totalETHXSupplyInInt, , , ) = AggregatorV3Interface(staderConfig.getETHXSupplyPORFeedProxy()) .latestRoundData(); return (uint256(totalETHBalanceInInt), uint256(totalETHXSupplyInInt), block.number);

This could lead to stale prices according to the Chainlink documentation: https://docs.chain.link/data-feeds/historical-data

Proof of Concept

Tools Used

Manual

(uint80 roundID, int256 answer, , uint256 updatedAt, uint80 answeredInRound) These are return parameters of latestRoundData, so can add checks like below.

... require(answeredInRound >= roundID, "Stale price");

Assessed type

Oracle

#0 - c4-judge

2023-06-09T23:25:16Z

Picodes marked the issue as duplicate of #15

#1 - c4-judge

2023-07-02T10:49:27Z

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