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
LowDuplicated Validators Can Be AddedLiquid Collective Lceth · Quantstamp · 1 May 2024Acknowledged

During the review, we noticed that the smart contract does not prevent the addition of duplicate validators by an operator. From discussions with the team, some mitigations are in place, such as an off-chain process that validates the "valid" validator count and sets operator limits to the verified amount, likely through the `cmd/node_operators.go:NewCmdSetOperatorLimit()` command. Additionally, the provided client embeds logic to prevent duplicate additions in the `cmd/validators.go:NewCmdAddValidators()`. However, since operators can directly add validators to the smart contract and operator limit protections can be bypassed by front-running the transaction, a malicious operator could still add duplicate or already exited validators to the contract. We have attempted to assess the impact, which appears to primarily lower the APY of the protocol. Depositing to a duplicate validator, whether still active or already withdrawn, results in the deposited ETH becoming non-productive. This requires a validator sweep to recycle these inactive balances back to the protocol, where they can then be redeposited to an effective validator. However, as the smart contract is outside the scope of this audit, we can only review the impact with limited knowledge of the contract codebase. There is a risk of other unexpected results if the attack occurs.

Open the report
LowExit Incentive MismatchLiquid Collective Lceth · Quantstamp · 1 May 2024Acknowledged

In the current system design, exited validators are determined by the `computeValidatorsToExit()` function in `watcher.go`. If these validators choose not to exit, they will not be prioritized for selection as the next active validator, as described in the `pickNextValidatorsToDepositFromActiveOperators()` function in the `OperatorRegistry.1.sol` smart contract implementation. The issue is that the current incentive is insufficient as the operator can still earn shared rewards from those supposed-to-exit validators. The motivation to exit (not prioritized for the next validator activation), is lower than the incentive to not exit (a promised shared staking reward). Consequently, validators may opt not to exit and continue earning rewards without any risk of being exited.

Open the report
LowOracles with Indices Greater than 255 Are Essentially Blocked From Submitting ReportsLiquid Collective Lceth · Quantstamp · 1 May 2024Acknowledged

In `oracle.go::getCurrentReportStatus()`, we have the following snippet which uses the `status` variable to keep track of which oracle member has reported consensus layer data to the oracle contract: ``` for i, a := range members { if status.Bit(i) == 1 { s.ReportedMembers = append(s.ReportedMembers, a) } } ``` Note that the `status` variable is essentially a `uint256` since it corresponds to the returned value from this function in `ReportPositions.sol`: ``` function getRaw() internal view returns (uint256) { return LibUnstructuredStorage.getStorageUint256(REPORTS_POSITIONS_SLOT); } ``` This means the maximum number of oracle members supported is 256. Particularly, when there are more than 256 members, there will be indices with a value greater than 255 (e.g., `uint256(memberIndex) > 255` in `Oracle.1.sol::reportConsensusLayerData()`). In `ReportPositions.sol::register()`, the code tracks which oracle member submitted a report by flipping the bit corresponding to its index in a uint256 variable, as follows: ``` function register(uint256 _idx) internal { uint256 mask = 1 << _idx; LibUnstructuredStorage.setStorageUint256( REPORTS_POSITIONS_SLOT, LibUnstructuredStorage.getStorageUint256(REPORTS_POSITIONS_SLOT) | mask ); } ``` However, this implies any oracle with an index over 255 will simply be ignored because the mask will simply be `uint256(0)` due to the bit shift. Currently, there are no enforced limits on how many oracle members can be registered (e.g., through `Oracle.1.sol::addMember()`), so it is possible to have more than 256 oracle members. When an oracle with an index greater than 255 reports consensus layer data (e.g., by calling `Oracle.1.sol::reportConsensusLayerData()`), it will fail because of the following check: ``` if (ReportsPositions.get(uint256(memberIndex))) { revert AlreadyReported(_report.epoch, msg.sender); } ``` Specifically, `ReportsPositions.get()` fails because `mask = 1 << _idx;` will simply be zero, so the condition `LibUnstructuredStorage.getStorageUint256(REPORTS_POSITIONS_SLOT) & mask == mask;` will always be true. ``` function get(uint256 _idx) internal view returns (bool) { uint256 mask = 1 << _idx; return LibUnstructuredStorage.getStorageUint256(REPORTS_POSITIONS_SLOT) & mask == mask; } ```

Open the report
LowUnable to Resubmit Report Until Next FrameLiquid Collective Lceth · Quantstamp · 1 May 2024Acknowledged

The `report.go:shouldReport()` function checks if `o.lastReportedEpoch >= epochSpec.FrameFirstEpoch()` and will return `false` if the daemon considers that it has already submitted a report for this frame. However, there are a few scenarios where resubmission might be preferred: 1. The admin makes changes to the operator member set or the quorum by calling `addMember()`, `removeMember()`, or `setQuorum()`, which will clear the reports on the contract side. 2. The daemon has attempted to submit the report through the `trySubmitReport()` function, but the transaction was eventually dropped or reverted. It might be better for the oracles to submit the report again in the above cases, or the system will need to wait for another frame (~1 day) before a new report can be submitted, risking the inability to reach the quorum.

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