Skip to content
Quantix Security Labs

A governance proposal that tried to remove its own timelock

Borrowed voting power, a one-block voting delay, and a timelock that let a proposal lower the delay it was subject to. How Proposal 41 on Thessaly Governance was detected at creation and cancelled.
CriticalGovernance manipulationIncident response · 9 min read
Illustration: voters around a governance dais with borrowed votes from a lending pool, and a proposal inside a timelock ring whose segments are being removed.

The case at a glance

Thessaly Governance

Optimism · GovernanceSecurity profileResearch case · Quantix Protocol Research

During the case
BB70.1
Current profile
BBB77.6
To containment
9 h 29 min
To verified fix
5.5 days
  1. 03Problem

    Shortly after midnight UTC on 10 June 2026, an address that had held no voting power an hour earlier created Proposal 41.

    Read section
  2. 06Vulnerability

    CriticalGovernance manipulation

    Two well-intended design choices combined into a bypass. Applying delay changes at queue time was meant to let a proposal protect itself by raising the delay; checking readiness against the current delay was meant to keep the timelock simple.

    Read section
  3. 08Remediation

    1. 01Cancel Proposal 41 through the guardian
    2. 02Raise the voting delay to 43,200 blocks
    3. 03Enforce a 48-hour minimum timelock delay

    and 2 more steps

    Read section
  4. 09Outcome

    Proposal 41 was cancelled before voting closed and no treasury assets moved.

    Risk Index BB → BBB, +7.5 points

    Read section

Background

Thessaly Governance provides token-weighted voting with delegation and a timelock on Optimism. Voting power is read at a snapshot taken a voting delay after the proposal is created; passed proposals are queued in the Timelock and executed after its delay. A guardian multisig can cancel proposals.

The governance token, THS, is listed on lending markets, so voting power can be borrowed for as long as someone is willing to pay interest.

Architecture

VotesToken checkpoints voting power each block; the Governor counts votes at the snapshot and queues succeeded proposals in the Timelock, which holds the treasury and admin rights over dependent contracts.

Figure 1Thessaly Governance: components and paths involved in this case

Thessaly Governance: architecture relevant to this case7 elements and 6 connections. Highlighted components are directly involved in the case; dashed red arrows are attack paths, dotted brass arrows are control paths.borrow 1.9M THSdelegate()getPastVotes()queue / executecancel()controlsLending marketTHS borrowableNew delegatecreated 40 min earlierVotesTokenCheckpointed voting powerwith delegationGovernorProposal lifecycle and votecountingTimelockDelay and execution ofpassed proposalsGuardian multisigcancel rightTreasuryheld by the Timelock
Protocol componentInvolved in this caseExternal partyAttack pathControl path

Elements:

  • Lending market (external party): THS borrowable
  • New delegate (adversary): created 40 min earlier
  • VotesToken (protocol component): Checkpointed voting power with delegation
  • Governor (protocol component, involved in this case): Proposal lifecycle and vote counting
  • Timelock (protocol component, involved in this case): Delay and execution of passed proposals
  • Guardian multisig (external party): cancel right
  • Treasury (external party): held by the Timelock

Connections:

  • Lending market to New delegate: borrow 1.9M THS (attack path)
  • New delegate to VotesToken: delegate() (attack path)
  • VotesToken to Governor: getPastVotes() (data or call flow)
  • Governor to Timelock: queue / execute (control path)
  • Guardian multisig to Governor: cancel() (control path)
  • Timelock to Treasury: controls (control path)

Components in the Thessaly Governance profile

  • Governor(involved in this case)

    Proposal lifecycle and vote counting

  • VotesToken

    Checkpointed voting power with delegation

  • Timelock(involved in this case)

    Delay and execution of passed proposals

Security challenge

Shortly after midnight UTC on 10 June 2026, an address that had held no voting power an hour earlier created Proposal 41. Its payload had two calls: Timelock.updateDelay(0), then a transfer of the whole treasury to the proposer. 96% of the proposer’s voting power came from 1.9 million THS — 4.2% of supply — borrowed and delegated six minutes earlier.

The questions were how borrowed tokens could vote at all, and why a proposal would set the delay to zero before moving funds if the timelock delayed the whole proposal anyway.

Attack surface

The response examined how voting power could be acquired and what a passed proposal could reach.

Attack surface examined in Thessaly Governance
SurfaceExposureControl
Voting delayOne block between proposal and snapshot43,200 blocks (one day)
Borrowable THSVoting power available for the price of a short borrowLonger voting delay makes borrowing costly; R-07 alerts
Delay changesApplied when queued and read at execution timeEach operation keeps the delay in force when it was queued
Delay floorNone; the delay could be set to zeroMinimum of 48 hours
GuardianCancel right on proposalsUnchanged; used before voting closed

Technical investigation

The voting snapshot was taken one block after the proposal. The borrower delegated at 23:55, the proposal followed at 00:01, and by the snapshot the borrowed balance was locked in as voting power. The tokens went back to the lending market at 01:20; voting power at the snapshot was unaffected by the repayment.

The payload explained the second question. Thessaly’s Governor applied timelock parameter changes when a proposal was queued, so that a proposal raising the delay would protect its own remaining calls. The Timelock checked readiness against the current delay rather than storing an execution time per operation. A proposal that lowered the delay therefore lowered it for itself: once queued, the treasury transfer would have been executable in the same block.

Monitoring raised a critical alert on the payload the moment the proposal was created, and a high alert on the borrowed voting power. The guardian cancelled the proposal that morning, before voting closed. The table compares the exposure at the time with the configuration after the fix.

Figure 2Path of Proposal 41

Path of Proposal 41ProposerVotesTokenGovernorMonitoringGuardian1. delegate(self) with 1.9M borrowed THS23:552. propose([updateDelay(0), treasury.transfer(100%)])00:013. ProposalCreated4. R-17 critical payload · R-07 borrowed votes5. repay borrowed THSafter snapshot6. cancel(41)before voting closes
Ordinary stepAttack stepDefensive step
  1. Proposer (adversary) to VotesToken: delegate(self) with 1.9M borrowed THS. 23:55 (attack step)
  2. Proposer (adversary) to Governor: propose([updateDelay(0), treasury.transfer(100%)]). 00:01 (attack step)
  3. Governor (on-chain) to Monitoring: ProposalCreated (normal step)
  4. Monitoring (off-chain) to Guardian: R-17 critical payload · R-07 borrowed votes (defensive step)
  5. Proposer (adversary) to VotesToken: repay borrowed THS. after snapshot (normal step)
  6. Guardian (participant) to Governor: cancel(41). before voting closes (defensive step)
Exposure under the configuration at the time of Proposal 41 and after the fix. Block time of 2 seconds.
ParameterAt the timeAfter the fix
Voting delay1 block (2 s)43,200 blocks (24 h)
Minimum time borrowed tokens must be held to vote2 s24 h
Time the borrowed tokens were actually held1 h 26 min—
Timelock delay floorNone48 h
Delay before a proposal that lowers the delay can move funds0 s≥ 48 h
Monitoring events recorded during the incident, with the rule that matched and its threshold
Time (UTC)RuleObservationMeasuredAlert
2026-06-09 23:55:12R-04Large holder movement1.9M THS (4.2% of supply) borrowed and delegated to a new addressMedium4.2 (threshold 1 % of supply)Alert
2026-06-10 00:01:04R-07Borrowed voting powerProposal 41 created with 96% borrowed voting powerHigh96 (threshold 10 % of votes acquired in 50 blocks)Alert
2026-06-10 00:01:04R-17High-risk proposal payloadProposal 41 sets timelock delay to zero and moves the treasuryCriticalCondition matchedAlert
2026-06-10 00:03:20R-08Governance lifecycleVoting snapshot taken one block after proposalMediumCondition matchedAlert
2026-06-10 01:20:47R-04Large holder movementBorrowed THS returned to the lending marketLow4.2 (threshold 1 % of supply)Alert
2026-06-10 09:30:05R-08Governance lifecycleGuardian cancels Proposal 41LowCondition matchedAlert
2026-06-11 15:00:00R-09Protocol parameter changeVoting delay raised to 43,200 blocks (one day)MediumRecorded for the timelineLogged
2026-06-11 15:00:00R-09Protocol parameter changeTimelock minimum delay floor of 48 hours enforcedMediumRecorded for the timelineLogged

Timeline

  1. Exploitation

    1.9M THS borrowed and delegated to a new address

  2. Detection

    Proposal 41 created; critical payload alert

  3. Voting snapshot one block after the proposal

  4. Borrowed THS returned to the lending market

  5. Containment

    Guardian cancels Proposal 41

  6. Fix

    Voting delay raised; 48-hour delay floor enforced

  7. Verification

    Timelock v2 behaviour verified against the Proposal 41 payload

Root cause

Two well-intended design choices combined into a bypass. Applying delay changes at queue time was meant to let a proposal protect itself by raising the delay; checking readiness against the current delay was meant to keep the timelock simple. Together they let a proposal shorten the delay it was subject to, and with no floor, shorten it to zero.

The one-block voting delay supplied the votes: it let tokens borrowed minutes earlier count at the snapshot.

Class
Governance manipulation
Severity
Critical
Components involved
GovernorTimelock
Origin
Incident response
Governor and Timelock (before the fix)solidity
// ThessalyGovernor: timelock parameters apply when a proposal is queuedfunction _queueOperations(uint256 id, address[] memory t, uint256[] memory v, bytes[] memory d) internal {    for (uint256 i = 0; i < t.length; i++) {        if (t[i] == address(timelock) && bytes4(d[i]) == Timelock.updateDelay.selector) {            timelock.updateDelay(abi.decode(d[i][4:], (uint256)));   // takes effect now            continue;        }        timelock.queue(hashOperation(t[i], v[i], d[i], id), block.timestamp);    }} // Timelockfunction updateDelay(uint256 newDelay) external onlyGovernor {    delay = newDelay;                                     // no lower bound} function isReady(bytes32 op) public view returns (bool) {    return queuedAt[op] != 0 && block.timestamp >= queuedAt[op] + delay;   // current delay}

Risk evaluation

The bypass is modelled as an open critical finding for the period it existed, and Governance Risk is reduced because borrowed voting power could reach the proposal threshold. The after state is the current profile, which still reflects that THS can be borrowed.

70.1BBElevated risk
During the caseBB70.1
77.6BBBModerate risk
Current profileBBB77.6
CodeSecurityOperationalSecurityGovernanceRiskEconomicSecurityInfrastructureResilienceTransparencyDuring the case, Code Security: 62During the case, Operational Security: 76During the case, Governance Risk: 58During the case, Economic Security: 76During the case, Infrastructure Resilience: 78During the case, Transparency: 84Current profile, Code Security: 80Current profile, Operational Security: 76Current profile, Governance Risk: 72Current profile, Economic Security: 76Current profile, Infrastructure Resilience: 78Current profile, Transparency: 84
During the caseCurrent profile

The score moves from 70.1 (BB) to 77.6 (BBB), a change of +7.5 points. The largest single contribution is Code Security: restoring it from 62.0 to 80.0 alone moves the score from 70.1 to 75.5.

Basis for the case state: Delay changes applied at queue time with no floor; one-block voting delay.
ComponentWeightRangeDuring caseTodayScore points
Code Security30%62.080.0+5.4
Operational Security20%76.076.00.0
Governance Risk15%58.072.0+2.1
Economic Security15%76.076.00.0
Infrastructure Resilience10%78.078.00.0
Transparency10%84.084.00.0
Quantix Risk Index100%70.177.6+7.5
During the caseCurrent profileBasis for the case state: Delay changes applied at queue time with no floor; one-block voting delay.

Scores use the six weighted components of the Quantix Risk Index. Open findings reduce Code Security by a fixed penalty per severity; the current profile is the protocol’s entry in Quantix Intelligence.

Mitigation strategy

Timelock v2 enforces a minimum delay of 48 hours, so no proposal can remove the delay it is subject to. Each operation now stores the execution time computed when it is queued, and delay changes go through the queue like any other call.

The voting delay rose from one block to 43,200 blocks, so borrowed tokens must be held — and paid for — for a day before they count. Both changes went through governance the next day.

  1. 01Cancel Proposal 41 through the guardian
  2. 02Raise the voting delay to 43,200 blocks
  3. 03Enforce a 48-hour minimum timelock delay
  4. 04Store an execution time per operation; queue delay changes like any call
  5. 05Alert on borrowed voting power and high-risk payloads at proposal creation
Timelock v2 (verified)diff
+uint256 public constant MIN_DELAY = 172800; // 48 hours  function updateDelay(uint256 newDelay) external onlyGovernor {+    if (newDelay < MIN_DELAY) revert DelayBelowMinimum(newDelay);     delay = newDelay; } -function isReady(bytes32 op) public view returns (bool) {-    return queuedAt[op] != 0 && block.timestamp >= queuedAt[op] + delay;-}+function queue(bytes32 op) external onlyGovernor {+    eta[op] = block.timestamp + delay;   // fixed when queued+}++function isReady(bytes32 op) public view returns (bool) {+    return eta[op] != 0 && block.timestamp >= eta[op];+}

Security outcome

Proposal 41 was cancelled before voting closed and no treasury assets moved. The voting delay and the delay floor took effect the next afternoon.

The proposer and borrower addresses are tracked in the monitoring rule set, and both alerts that fired on Proposal 41 remain active for every monitored governor.

Detection to containment
9 h 29 min
Detection to first fix
38 h 59 min
Detection to verified fix
5.5 days
Risk Index change
BB → BBB (+7.5)

Lessons learned

  1. 01

    A timelock that a proposal can shorten is not a timelock for that proposal.

  2. 02

    Fix each operation’s execution time when it is queued, and set a floor on the delay.

  3. 03

    Borrowable governance tokens make the voting delay a security parameter.

  4. 04

    Review proposals by their decoded calldata, from the moment they are created.

Have a problem like this one?

Tell us what you are building and where it runs. We will reply with the review plan we would use for your architecture.