These four functions have a notPaused modifier and cannot be called if the contract is paused.
The problem is that pausing is not synchronous across the gateways: pausing L1GraphTokenGateway does not trigger pausing in L2GraphTokenGateway, and vice-versa.
This means it is possible for a pauseGuardian (which is a separate entity from the governor) to pause one of the gateways while withdrawals are pending, effectively freezing the users GRT on L1.
The issue arises mainly because pausing the gateways is instantaneous (which is understandable for emergency reasons) while the Arbitrum dispute period is quite lengthy (~ 1 week)
Impact
Medium
Proof Of Concept
Alice wants to withdraw her GRT back to L1 and calls L2GraphTokenGateway.outboundTransfer(), initiating a transfer to L1 and burning her L2 GRT.
The malicious PauseGuardian calls L1GraphTokenGateway.setPaused(true)
The dispute period is complete, Alice tries to call Outbox.executeTransaction() to execute L1GraphTokenGateway.finalizeInboundTransfer() and retrieve her GRT. But because L1GraphTokenGateway is paused, the function reverts.
Alice GRT on L1 is frozen.
Tools Used
Manual Analysis
Mitigation
You can ensure the pausing mechanism happen in both L1GraphTokenGateway and L2GraphTokenGateway instead of being independent from each other. For instance:
calling L1GraphTokenGateway.setPause() would set _paused to true and send a retryable ticket to L2GraphTokenGateway, which will set L2GraphTokenGateway._paused to true
Lines of code
https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/gateway/L1GraphTokenGateway.sol#L198 https://github.com/code-423n4/2022-10-thegraph/blob/309a188f7215fa42c745b136357702400f91b4ff/contracts/gateway/L1GraphTokenGateway.sol#L269
Vulnerability details
The Pause guardian is a separate entity from the governor that can pause the gateway contract.
Both transfers (deposit L1 to L2, and withdraw from L2 back to L1) involve one function on the L1Gateway and one function on the L2Gateway.
These four functions have a
notPaused
modifier and cannot be called if the contract is paused.The problem is that pausing is not synchronous across the gateways: pausing
L1GraphTokenGateway
does not trigger pausing inL2GraphTokenGateway
, and vice-versa.This means it is possible for a
pauseGuardian
(which is a separate entity from the governor) to pause one of the gateways while withdrawals are pending, effectively freezing the usersGRT
on L1.The issue arises mainly because pausing the gateways is instantaneous (which is understandable for emergency reasons) while the Arbitrum dispute period is quite lengthy (~ 1 week)
Impact
Medium
Proof Of Concept
GRT
back to L1 and callsL2GraphTokenGateway.outboundTransfer()
, initiating a transfer to L1 and burning her L2GRT
.PauseGuardian
callsL1GraphTokenGateway.setPaused(true)
Outbox.executeTransaction()
to executeL1GraphTokenGateway.finalizeInboundTransfer()
and retrieve herGRT
. But becauseL1GraphTokenGateway
is paused, the function reverts.GRT
on L1 is frozen.Tools Used
Manual Analysis
Mitigation
You can ensure the pausing mechanism happen in both
L1GraphTokenGateway
andL2GraphTokenGateway
instead of being independent from each other. For instance:L1GraphTokenGateway.setPause()
would set_paused
totrue
and send a retryable ticket toL2GraphTokenGateway
, which will setL2GraphTokenGateway._paused
totrue