Liquid Collective security record

Base, Ethereumliquidcollective.ioX

As of 3 Nov 2026, Liquid Collective has 2 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 25 Mar 2025. Those reports list no open critical findings.

Critical findings
No open critical findings
Across 2 reports.
Open the latest report
Your label, live
Get the label for your site

Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.

Audits2

Newest first

AuditSecurity firmDateScopeCritical findingsReport
Liquid Collective - SolanaSecurity firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com
Liquid Collective LcethSecurity firm: QuantstampDate: Scope: Files named in the reportNo open critical findingsOpen · certificate.quantstamp.com

Findings17

Statuses are the security firm’s own, from each report.

SeverityOpenAcknowledgedFixed
Critical000
High001
Medium001
Low0411
HighAllow List Entries Can Be Added and Removed by Any State AllowerLiquid Collective - Solana · Quantstamp · 25 Mar 2025Fixed

The `AddToAllowlistEntry` and `RemoveFromAllowlistEntry` instructions have the following constraint for the `allower` signer value: ``` address = state.allower @ ErrorCode::OnlyAllower ``` However, the `allow_list_entry` account passed to both instructions only has the following constraint for its seeds: ``` seeds = [b"allow_list_entry", args.stake_pool.as_ref(), args.user_account.as_ref()] ``` This effectively means that a valid `state` has to be passed to the instruction and that the `allower` that signs the transaction has to be that state's allower. However, this also means that, as long as the allower passes a valid `stake_pool` account and a valid `user_account`, they can add/remove said user account to/from the allow list for the referenced stake pool. As a result, any account that has `allower` access to any state can add and remove users from any arbitrary stake pool they desire.

Open the report
MediumPotential Denial of Service in Report Generation Due to UnderflowLiquid Collective Lceth · Quantstamp · 1 May 2024Fixed

In `oracle/report.go:generateReport()`, there is a snippet of code that calculates the skimmed balance: ``` case withdrawalEpoch >= withdrawableEpoch: // All amounts below 32 ETH are considered exited validatorsExitedBalance += MinGwei( withdrawal.Amount, beaconcommon.Gwei(32_000_000_000), ) // All amounts above 32 ETH are considered skimmed validatorsSkimmedBalance += MaxGwei( 0, withdrawal.Amount - beaconcommon.Gwei(32_000_000_000), ) ``` However, the calculation of `validatorsSkimmedBalance` can potentially underflow when `withdrawal.Amount` is less than 32 ETH. Since the underlying type of `withdrawal.Amount` is simply `uint64`, underflowing will result in setting `validatorsSkimmedBalance` to a value larger than `2^64 - 1 - 32_000_000_000 = 1.8446744e+19 GWEI`, approximately 150 times the current ETH supply. When a validator is slashed, they are forced to exit the validator set, with the `withdrawableEpoch` set to 36 days after the slashing event. This means the underflow illustrated above can occur around 36 days after a slashing event since the withdrawable amount of a slashed validator is lower than 32 ETH. Consequently, the report submission to the smart contract side will likely revert at `OracleManager.1.sol::setConsensusLayerData() -> River.1.sol::_pullCLFunds()` because `collectedCLFunds` will not be equivalent to `_skimmedEthAmount + _exitedEthAmount`, given that `_skimmedEthAmount` will be a significantly large amount. ``` function _pullCLFunds(uint256 _skimmedEthAmount, uint256 _exitedEthAmount) internal override { ... IWithdrawV1(WithdrawalCredentials.getAddress()).pullEth(totalAmountToPull); uint256 collectedCLFunds = address(this).balance - currentBalance; if (collectedCLFunds != _skimmedEthAmount + _exitedEthAmount) { revert InvalidPulledClFundsAmount(_skimmedEthAmount + _exitedEthAmount, collectedCLFunds); } ... } ``` This could have an impact as it might cause a denial of service on the oracles since no one will be able to submit a consensus layer report to the smart contract; it will revert due to the underflow issue described.

Open the report
LowLack of Input ValidationLiquid Collective - Solana · Quantstamp · 25 Mar 2025Fixed

The following functions do not validate public key values before setting them, resulting in setting/resetting account identifiers to the default public key if it is sent in the transaction. This is similar to the lack of zero address checks issue in Solidity code bases. - The `new_pauser` key in `processors::set_pauser::exec()` - The `new_allower` key in `processors::set_allower::exec()` - The `admin`, `allower`, `pauser`, and `initial_manager` keys in `processors::initialize::exec()`

Open the report
LowNo Validation on the `FeeArgs` Data TypeLiquid Collective - Solana · Quantstamp · 25 Mar 2025Fixed

The `FeeArgs` struct consists of two parts, a `numerator` and a `denominator`, representing the two parts of a fraction. There is currently no validation that the `denominator` part of the struct isn't a zero, which may lead to unwanted consequences and potential reverts if an invalid `FeeArgs` is sent in transactions.

Open the report
LowBest Practices / OptimizationsLiquid Collective Lceth · Quantstamp · 1 May 2024Fixed

Here are some potential improvements to the codebase that can enhance the readability or performance of the program: 1. In `oracle/types.go`, the `Validator` struct has a typo in the field `widthdrawals`, which should be `withdrawals`. 2. In `cmd/allowlist.go:NewCmdAllowlistDeny()`, the sentence in the "Short" description seems unfinished: "Transact to deny accounts (disabling all operations including)". 3. In `cmd/validator.go:NewCmdAddValidators()`, consider adding `defer trackTicker.Stop()` and `defer trackTimerMax.Stop()` right after the initiation of the tickers to ensure the `Stop()` function is called even if the `NewCmdAddValidators()` function exits early. 4. Consider lowering the generated folder permissions in `cmd/oracle.go:NewCmdOracleRun()#L345`. The permission `0700` might be sufficient for the database's usage. 5. In `oracle/validators.go:syncCLValidators()`, the line `validatorIDs := make([]string, 0)` can be optimized by using `validatorIDs := make([]string, 0, len(o.fundedValidatorKeys))` to avoid capacity reallocation.

Open the report
LowDependencies on External Libraries Contain Known VulnerabilitiesLiquid Collective Lceth · Quantstamp · 1 May 2024Fixed

The Golang security checker `govulncheck` has reported that the project relies on external libraries with known vulnerabilities, particularly in the native Golang libraries `net/http` and `crypto`. During our review, we believe that these vulnerabilities are not directly exploitable given the current project design. However, we recommend keeping all dependencies up-to-date to mitigate potential security risks in the future.

Open the report
LowHandling Slashed Validators as Unsolicited Exits and Unexpected ErrorsLiquid Collective Lceth · Quantstamp · 1 May 2024Fixed

In `exit/watcher.go:computeValidatorsToExit()`, the function computes a list of exitable validators and counts the number of exited validators using the statuses `active_exiting`, `exited_unslashed`, `withdrawal_possible`, and `withdrawal_done`, but it does not consider the slashed validators as exited. However, it should be noted that after the penalty period (i.e., 36 days) for a slashed validator, it will be forced to exit, eventually moving its status to `withdrawal_possible` (See [here](https://docs.blockdaemon.com/docs/eth-validator-statuses)). This means that the slashed validators will be considered as exited in a later invocation of the function `computeValidatorToExit()` (say after 40 days) because their status will have changed to `withdrawal_possible`. This can potentially lead to an undesired state where `exited > toExitCount`, where the system considers the exited slashed validators as unsolicited exits. ``` // In watcher.go:computeValidatorsToExit() if exited > toExitCount { return nil, fmt.Errorf("expecting to exit a total of %d; currently exited: %d; unsolicited exits: %d", toExitCount, exited, exited - toExitCount) } ``` This will return an error, causing the daemon to fail and either retry or wait for the next tick. However, unless a new exit is requested, it will remain in the current status. Note that similarly, in the `oracle/report.go:generateReport()` function, the slashed validator will not be counted as a "stopped validator" and the `validatorsExitingBalance` will not take into account those validators until they are in the "withdrawal" statuses. ``` // In oracle/report.go:generateReport() ... if v.IsStatus( StatusActiveExiting, StatusExitedUnslashed, StatusExited, StatusWithdrawal, StatusWithdrawalDone, StatusWithdrawalPossible, ) { validatorsExitingBalance += v.clData.Balance stoppedValidatorCountPerOperator[0]++ // sum stoppedValidatorCountPerOperator[v.operatorIdx+1]++ // count per operator } ... ``` After the discussion with the team, they confirmed that they intended to treat slashed validators similarly to unsolicitedly exited validators. Additionally, the team confirmed that despite the gap in accounting for the "exiting balance" in specific statuses, this is also considered. Given that the smart contract side is considered safe, we value this issue's severity as Informational.

Open the report
LowMissing Conversion to GweiLiquid Collective Lceth · Quantstamp · 1 May 2024Fixed

The `core.go:getLastConsensusLayerReport()` function retrieves the report data from the smart contract and returns the `types.CLReport` type. The "balance" fields: `ActiveValidatorsBalance`, `SkimmedValidatorsBalance`, `ExitedValidatorsBalance`, and `ExitingValidatorsBalance` are directly cast to `beaconcommon.Gwei` from the numbers received from the contract. However, in the contract, the values are stored in the unit of wei instead of Gwei. Thus, there is a misplacement of the field values here. We have not found any usage of these impacted "balance" fields within the codebase, and we assign a lower severity to the issue. However, if the codebase is imported and used in a different repository, there could be a larger impact.

Open the report
LowOracle Report May Not Have Majority ConsensusLiquid Collective Lceth · Quantstamp · 1 May 2024Fixed

When the system retrieves the latest oracle report through `core.go::GetLastConsensusLayerReport()`, there's an implicit assumption that the reported data has reached a majority consensus from all the oracle members. ``` See comments above Oracle.1.sol::_clearReportsAndSetQuorum(). /// @dev The quorum value Q should respect the following invariant, where O is oracle member count /// @dev (O / 2) + 1 <= Q <= O ``` However, this may not be the case as the quorum can be set to any integer irrespective of the number of oracles in the system. In particular, when adding a new oracle member to the system in `Oracle.1.sol::addMember()`, it allows the admin to set a new quorum, which internally calls `Oracle.1.sol::_clearReportsAndSetQuorum()`. However, the invariant where the quorum is at least half the number of oracle members is not actually enforced, as shown below. ``` function _clearReportsAndSetQuorum(uint256 _newQuorum, uint256 _previousQuorum) internal { uint256 memberCount = OracleMembers.get().length; if ((_newQuorum == 0 && memberCount > 0) || _newQuorum > memberCount) { revert LibErrors.InvalidArgument(); } _clearReports(); if (_newQuorum != _previousQuorum) { Quorum.set(_newQuorum); emit SetQuorum(_newQuorum); } } ```

Open the report
LowPanic Risk of Exiting Daemon Due to Not Setting `webhookCallback`Liquid Collective Lceth · Quantstamp · 1 May 2024Fixed

In the `exit.go:NewCmdExitRun()` function, the `webhookCallback` will be a `nil` pointer if it does not enter the `if v.GetBool(webhookFlag.ViperKey) && !dryRun {}` code block that sets the webhook. In other words, the webhook will be `nil` if it is in dry run mode, or if the `webhookFlag.ViperKey` environment or config is not set. The `webhookCallback` will be called in the `watcher.go:runExitRequestWatcher()` function as long as it is not in dry-run mode. There is a risk of the program panicking due to triggering the function of a `nil` pointer.

Open the report
Show 3 more
LowPotential File Inclusion via VariableLiquid Collective Lceth · Quantstamp · 1 May 2024Fixed

`gosec` reports the following places with the risk of unintended file path can be specified: 1. `cmd/validators.go:NewCmdAddValidators()#L289`: the `depositInputFile` variable. 2. `cmd/validators.go:NewCmdAddValidators()#L273`: the `depositDataFile` variable. 3. `cmd/gen.go:GenMarkdownTreeCustom()#L99`: the `file` variable. 4. `cmd/deposit_data.go:NewCmdDepositDataValidate()#L33`: the `depositDataFile` variable.

Open the report
LowRisk of Panic Due to Inconsistent Report Status DataLiquid Collective Lceth · Quantstamp · 1 May 2024Fixed

The `oracle.go:GetCurrentReportStatus()` function is used in the `report.go:report()` function to provide the report status from the contract. However, the underlying `getCurrentReportStatus()` function makes several calls to build the `types.Status` data. These calls do not ensure reading from the same block, and there is a risk that they might read inconsistent data. For instance, the code first calls `members, err := c.getOracleMembers(opts)` and then `status, err := c.oracle.GetGlobalReportStatus(opts)`. The `s.ReportedMembers` are built by the status' bits. If the member order changes due to the removal of oracle members, then the `ReportedMembers` will be incorrectly flagged as the status no longer reflects the correct ordering. Another potential impact is that the members might not match the quorum due to the `members, err := c.getOracleMembers(opts)` and `s.Quorum, err = c.getQuorum(opts)` calls potentially reading from different blocks. If the quorum is larger than the number of members due to new members joining between the calls, it can risk causing the `computeWaitEpoch()` function to panic on the line `range opOrder[quorum:]` as it may attempt to slice the array with an index that is out of bounds.

Open the report
LowRisk of Using Incorrect `OperatorsCount` ValueLiquid Collective Lceth · Quantstamp · 1 May 2024Fixed

The `report.go:refreshFrameReport()` function reuses some data from `currentEpoch` for `currentFrame` in the line: `currentFrame := currentEpoch // Copy CLSpec & OperatorsCount from currentEpoch`. The `CLSpec` is very unlikely to change as it is tied to the consensus, but the operator count can be updated and cause the `currentFrame` to use an incorrect value. Currently, the `OperatorsCount` is only used to initialize the `stoppedValidatorCountPerOperator` array in the `generateReport()` function. From our understanding, operators can only be added but not removed on the contract side, so the operator count will only increase over time. As a result, even if the `currentFrame` uses an incorrect operator count, it will only waste some memory to initialize unnecessary slots for the array. There should not be any impact on business logic based on our current analysis. However, we would like to alert the team to the risk and any future usage of the `OperatorsCount` data here.

Open the report

Contracts

No deployed address is recorded for these audits.

The reports name a repository or files. Open a report to see its scope.

The repositories and files the reports name

Show this on your site

Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.

Content

Facts

Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.

Fixed as
Language

Style

Layout
Theme
Accent
Corners

Format

Size
Date
Embed as
HTML
<head>
<script src="https://widget.trustblock.run/project-widget.min.js" defer></script>
</head>
<body>
<div
data-tb-project-widget
data-tb-project-slug="liquid-staked-eth"
></div>
</body>

Paste the code where the label should appear. It uses no API credit and updates by itself.

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us