Yellow engaged Guardian to review the security of their Yellow contract. From the 2nd of March 2026 to the 5th of March 2026, a team of 2 auditors reviewed the source code in scope.
- Published
- Review window
- March 2 to 5, 2026
- Language
- Solidity
- Chains
- Ethereum
- Sector
- Infrastructure
- 0 Critical
- 0 High
- 1 Medium
- 8 Low
- 0 Informational
Scope
Overview
Yellow engaged Guardian to review the security of their Yellow contract. From the 2nd of March 2026 to the 5th of March 2026, a team of 2 auditors reviewed the source code in scope.
Findings 9
-
M-01 Medium Quorum Floor Change Affects Previous Proposals Logical Error Resolved
Description
The quorum value is determined as the maximum of the fractional quorum and quorumFloor. While the fractional quorum, inherited from GovernorVotesQuorumFraction, is calculated using the snapshot at the time of proposal creation, quorumFloor uses the current value and is not snapshotted at proposal creation.
The queue and execute flows perform quorum checks based on updated value. As a result, any update to quorumFloor retroactively affects all proposals that have not yet been executed, including proposals whose voting period has already ended. A proposal may satisfy quorum and be marked as passed under the floor value in effect during voting, but a subsequent increase to quorumFloor can cause quorum validation to fail during the queuing or execution phase, preventing successful proposal execution.
Recommendation
Snapshot the quorum floor per proposal (store floor at proposal creation/snapshot) and use that stored value when computing quorum for that proposal’s snapshot block. Alternatively, make the floor change apply only to proposals created after a certain timepoint.
Resolution
Yellow Team: Resolved.
-
L-01 Low Quorum Floor Can Be Set Above Voting Supply Validation Resolved
Description
Governance can set _quorumFloor to an absolute value larger than the maximum possible votes at a snapshot (e.g., larger than total voting units / total locked supply), making quorum impossible to reach and effectively freezing proposal success/execution. Because setQuorumFloor itself is onlyGovernance, a malicious or compromised governance moment can permanently brick future governance.
Recommendation
Add an upper bound when setting the floor (e.g., <= token.totalSupply() or <= expected max voting units).
Resolution
Yellow Team: Resolved.
-
L-02 Low Undelegated Locks Raise Quorum With No Votes Logical Error Resolved
Description
In src/Locker.sol, lock(uint256 amount) calls _transferVotingUnits(address(0), msg.sender, received) regardless of delegation status. In OZ Votes, this unconditionally adds to _totalCheckpoints (the total supply used in quorum calculation).
However, when the user has not called delegate(), their delegatee defaults to address(0), so _moveDelegateVotes(address(0), address(0), amount) hits the from != to guard and is a no-op; no voting power is assigned to any address.
Recommendation
Consider auto-self-delegating on first lock when delegates(msg.sender) == address(0) (e.g., call _delegate(msg.sender, msg.sender) in lock()).
Resolution
Yellow Team: Resolved.
-
L-03 Low No Late-Quorum Protection On Governor Best Practices Resolved
Description
YellowGovernor (src/Governor.sol) does not inherit OZ's GovernorPreventLateQuorum extension. Without it, a whale who holds sufficient voting power can wait until the final block of the voting period and cast a decisive vote that triggers quorum and determines the outcome in a single transaction. Other tokenholders have no opportunity to observe the vote, react, or counter-vote because the proposal deadline has already passed.
Recommendation
Consider inheriting GovernorPreventLateQuorum and setting a reasonable voteExtension.
Resolution
Yellow Team: Resolved.
-
L-04 Low Ownership Can Be Renounced And Cause Permanent DoS Configuration Resolved
Description
The treasury owner is the deployer initially, and ownership will be transferred to the Timelock through a two-step transfer, after which the Timelock will accept the ownership.
However, the renounceOwnership function from Ownable.sol is not overridden and could cause a complete DoS if used by mistake during this process. Calling renounceOwnership not only removes the current owner but also clears the pendingOwner. As a result, the Timelock would not be able to accept ownership, and all funds would become permanently locked in the treasury.
Additionally, the Timelock itself can also renounce ownership via malicious governance, which would prevent the withdraw function in the treasury from being used.
Another point to note is that the deployer remains the owner until the Timelock accepts ownership. During this period, the deployer can withdraw funds from the treasury without any governance approval.
Recommendation
Since the treasury must always have an owner, override the renounceOwnership function to always revert, preventing the treasury from being left without an owner.
Resolution
Yellow Team: Resolved.
-
I-01 Low Rebasing Tokens Cannot Be Used In Locker Warning Resolved
Description
The Treasury and Locker contracts determine the exact transferred amount by comparing the before and after balances of users (or the locker itself) during transfers. This enables support for fee-on-transfer tokens.
However, the Locker is not compatible with rebasing or deflationary tokens. If the underlying token balance gradually decreases over time, withdrawals will fail, particularly for later or final users. This happens because the Locker’s balance decays over time, while the withdraw function attempts to transfer the original _balances recorded at the time of locking.
Recommendation
Be aware of this limitation. No changes are necessary if rebasing tokens are not intended to be used as the underlying asset in the Locker.
Resolution
Yellow Team: Resolved.
-
I-02 Low Mismatch Between Tests And Code Comment Informational Resolved
Description
The NatSpec comment in Locker states: “Single-asset vault with a 7-day time-locked withdrawal mechanism.”
However, in the tests, the immutable UNLOCK_PERIOD is set to 14 days.
Recommendation
While the tests do not necessarily need to be changed, ensure that the contract is deployed with a 7-day unlock period, or update the comment in Locker.sol accordingly.
Resolution
Yellow Team: Resolved.
-
I-03 Low Proposals Uncancellable Once Past Pending State Best Practices Resolved
Description
OZ Governor v5 base _validateCancel (Governor.sol:787-789) only allows cancelling during Pending. Once voting starts, proposal cancellation reverts for everyone, including the proposer. This is the default behavior in the OpenZeppelin Governor implementation. Neither GovernorTimelockControl nor YellowGovernor overrides this behavior. Once voting begins, only token holders can determine the outcome and no kill switch exists.
As a result, if a malicious proposal somehow gets voted through and queued:
- Attacker proposes something like treasury.withdraw(token, attacker, fullBalance).
- It passes the vote (social engineering, bribery, voter apathy, etc).
- Gets queued in the timelock.
- Community notices during the 2-day window.
- governor.cancel() — reverts.
- timelock.cancel() — reverts.
- Only option is a counter-proposal, but that needs votingDelay + votingPeriod + timelockDelay (~10
days). The malicious one executes in day 2.
- Anyone calls execute() (open EXECUTOR_ROLE), treasury gone.
While this is essentially a governance decision rather than an exploitable vulnerability, OpenZeppelin provides GovernorProposalGuardian for such edge cases, allowing proposals to be cancelled after voting when necessary.
Recommendation
Consider using OpenZeppelin’s GovernorProposalGuardian and assigning a trusted guardian (e.g., a multisig) that can cancel proposals at any stage.
However, this introduces an element of centralization. No change is required if the Yellow protocol intentionally chooses token holders/governance as the sole decision-maker without a kill switch.
Resolution
Yellow Team: Resolved.
-
I-04 Low Misleading Error Name In Constructor Validation Best Practices Resolved
Description
In Locker.sol:34, when unlockPeriod_ is zero the constructor reverts with InvalidAmount(). But the unlock period is a time duration, not a token amount. The same InvalidAmount() error is also used in lock() for 0 amount deposits.
Recommendation
Consider using a specific error like InvalidPeriod().
Resolution
Yellow Team: Resolved.
No findings match.
More from Yellow Network
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
