[SECURITY COUNCIL] Preliminary disclosure of a security incident that impacted G$ liquidity on CELO network

Technical Report of the 2026-09-03 Superfluid/CELO Incident

By: @didi, @hellwolf

1. Background Knowledge

Callbacks (hooks), Super Apps, and the jail system

A Super App is a contract registered on the Superfluid Host. When an agreement operation (create, update, or delete a stream) involves that app, the Host runs beforeAgreement* and afterAgreement* hooks. The app must return the Superfluid context (ctx) unchanged in shape. The Host then checks that the context is intact and that app-credit rules were respected.

If a hook breaks a protocol rule, the Host jails the app. Jailing is permanent: later callbacks are skipped, and the agreement operation is still allowed to finish. The rule that matters here is termination. A delete or liquidation must be able to close a stream even when the app misbehaves. If afterAgreementTerminated reverts, the Host jails the app (rule 10, NO_REVERT_ON_TERMINATION_CALLBACK) and continues. A successful return that is not a well-formed context is the same idea (rule 22, CTX_IS_MALFORMATED): jail, then continue.

That path only runs when the callback call itself returns to the Host. It does not run when copying or decoding the callback’s return data exhausts the block.

Lifting of Super App whitelisting on Celo

It was understood that Super Apps considerably increase the attack surface, thus the protocol could be configured to have permissioned Super App registration.
The flag APP_WHITE_LISTING_ENABLED which enables this permissioned mode was set on all mainnets.
In 2023 there was internal conversation about lifting this restriction. Audits had found no vulnerabilities in the mechanism and the permissioned nature caused some friction for Super App developers. After much deliberation it was decided to lift the restriction on Celo, where at the time a lot of Super App related activity took place. This change allowed to reduce friction for devs and make the protocol more permissionless, on a chain/ecosystem where projects were perceived to be more experimental in nature than on other chains and with less at stake than on some other deployments.

2. The Exploit Explained

Jailbreak exploiting unbound callback returndata

On 1–2 September 2026 an operator permissionlessly registered Super Apps on Celo. The G$ app 0x932424d333475f48abcd9a61f570416b542bb94d registered as FINAL with create and update hooks set to no-op and terminate hooks left on. From 2 September 15:42 UTC it took nine inbound G$ streams from owner-seeded accounts, scaling to about 37,673 G$/s, and forwarded nothing. The same operator opened smaller streams on six other Super Tokens from 0x14ec5876a9a55b7fbaef114161ecdd2bbf7f220b. A third app, 0x4b027e5f78e1efd89e562c7acd62566bf6468430, was jailed on 1 September while the deposit still covered it, and added nothing.

Create callbacks were skipped, so opening the streams was cheap and succeeded. The terminate hooks returned successfully, with a large but ABI-well-formed bytes payload (on the order of 900 KB). Because the call succeeded, the Host did not take the revert-jail path. Because there was no cap, it copied the whole blob. The agreement then ABI-encoded that blob again, outside CALLBACK_GAS_LIMIT (15 million). Memory expansion exceeded Celo’s block gas (about 30 million). deleteFlow and liquidation died as an empty CallUtils revert. Senders went insolvent and could not be closed. The app stayed solvent and the operator swapped the incoming G$.

Superfluid._callCallback

_callCallback is the Host’s hook caller. It replaces the placeholder context, then CallbackUtils.externalCall or staticCall invokes the app with the callback gas stipend. Before the fix, those helpers copied all of returndatasize() into memory. The stipend bounds gas inside the app. It does not bound the memory cost of copying the return data in the caller, nor the second encoding the agreement does with that blob. A terminate hook could therefore succeed inside its stipend and still make the outer transaction impossible to include.

The fix: CALLBACK_RETURNDATA_CAP

The Celo hotfix (Host logic 0x724676722b69b8b0e5fCf030de796047E771E5f9, upgraded 4 September 2026, 14:48 UTC) stops the copy once return data exceeds a bound. The call’s success bit is left as-is, so an oversized “successful” return becomes an empty buffer, which the existing malformed-context path jails under rule 22. deleteFlow then completes. The same upgrade set APP_WHITE_LISTING_ENABLED back to true. The nine stuck G$ streams were closed that afternoon (14:49–15:53 UTC).

The follow-up protocol change names that bound CALLBACK_RETURNDATA_CAP in CallbackUtils and documents it on ISuperApp. The constant is 128 KiB of ABI-encoded callback return data. If returndatasize() is larger, the helper returns empty bytes and the Host jails instead of expanding memory to the size of the blob.

3. Impact Assessment

Closing the stuck streams booked the senders’ negative balances to TOGA. That is unbacked units already sitting in other accounts. totalSupply() was unchanged.

Token Unbacked Recovered
G$ 0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A 2,801,252,167 G$ (82% of supply before the streams; 117% of supply at the close) 0
CELOx 0x671425ae1f272bc6f79bec3ed5c4b00e9c628240 36,847.80 CELOx 27,382.36 CELOx
cEURx 0xd9f9a02e49225c7ab5b40fce8d44d256b0e984fb 61.21 cEURx 0
USDGLOx 0x2e7762ebdcbd3faf45d723f23e851ef4fc4e8617 2,844.11 USDGLOx 1,924.00 USDGLOx
USDCx 0x19b6b95bcfc3442ebab4dc10e1ad54f6e7600f19 73.48 USDCx 0
cUSDx 0x3acb9a08697b6db4cd977e8ab42b6f24722e6d6e 61.24 cUSDx 0
USDCx 0x618b6ba24725acb1081cd4bc93e67e2f375d8e45 24.50 USDCx 0

Recovered amounts are the 10 September transfers from the app into TOGA. G$ the operator sold was 2,256,724,080 G$. 544 million G$ was later burned with no stablecoin in return. G$ still on TOGA on 30 Sep 2026 was −2,801,228,939 G$.