
Date: September 1, 2026, with SlowMist's alert following on September 2
Chain: Ethereum
Loss: 5.9436 ETH, about $14.8K
Class: missing caller check on a helper function meant to run only through a user's proxy
Four SAFEs in a Reflexer GEB deployment on Ethereum, the vaults where users lock ETH as collateral to borrow against it, were registered to an unusual owner. The owner on record was GebProxyActions, a shared helper contract that users' proxies borrow code from, and the SAFEs had sat with that owner since they were opened in late October 2020, still holding ETH collateral.
On September 1, 2026, at 15:38:23 UTC, an attacker called GebProxyActions.quitSystem() directly, pointed it at those four SAFEs, set the destination to their own contract and took 5.9436 ETH, about $14,832, in a single transaction that SlowMist's alert flagged the next day. GebSafeManager, the contract that tracks who owns each SAFE, checked permissions correctly and saw that GebProxyActions owned the SAFEs, and quitSystem() itself never asked who was calling it.
GEB is Reflexer's framework, derived from MakerDAO's multi-collateral DAI system, and it's the codebase Reflexer's RAI stablecoin runs on. Users lock collateral in a SAFE and borrow the system's coin against it, and most of them do it through a DSProxy, a personal smart contract wallet that executes bundled actions on their behalf.
According to the DeFiHackLabs reproduction, the deployment hit here has been in Global Settlement, GEB's emergency shutdown mode, since January 2021, so these were dormant positions in a system that was no longer in active use.
Those bundled actions live in GebProxyActions. A user's DSProxy doesn't call GebProxyActions the normal way, it uses delegatecall, which runs GebProxyActions' code inside the DSProxy's own context. When GebProxyActions then talks to GebSafeManager, the manager sees the user's DSProxy as the caller, and GebSafeManager's safeAllowed check confirms that the DSProxy owns or is approved for the SAFE it's touching. Reflexer's documentation describes that safeAllowed guard and the quitSystem function on the manager side.
GebProxyActions is also deployed as an ordinary contract on mainnet, so anyone can call its functions directly. When that happens, the code runs in GebProxyActions' own context, and GebSafeManager sees GebProxyActions itself as the caller.
GebSafeManager's own event log shows how. When SAFEs 3, 5 and 8 were opened on October 29, 2020, and SAFE 18 on October 30, the manager's OpenSAFE event recorded GebProxyActions as both the sender and the owner, which is what happens when the helper's code runs in its own context instead of inside a user's DSProxy. Coverage of SlowMist's report describes the same thing from the user side, as people calling GebProxyActions directly instead of through their proxy. From then on, those SAFEs were owned by a contract that anyone in the world could call into, and the DeFiHackLabs reproduction confirms they still were on the day of the attack.
That's where the missing check mattered. GebProxyActions.quitSystem(manager, safe, dst) takes a SAFE ID and a destination and asks GebSafeManager to move the SAFE's position to that destination. It has no check on msg.sender, because it was written to run only inside a user's DSProxy, where the DSProxy's own ownership model does that job. Called directly, it forwarded the request with GebProxyActions as the caller, and for these four SAFEs GebSafeManager's guard gave the honest answer that GebProxyActions was allowed.
The idea for builders to take from this is that a function designed to be delegatecalled is still a public function on the contract where it lives. If it only makes sense inside someone's proxy, the code has to say so, either by refusing direct calls or by checking the caller itself. It works like a master key kept in a building's lobby for the doorman to use: as long as only the doorman reaches for it, the rule holds, but the key doesn't know who's holding it.
The attacker's play needed no flash loan and no price movement. In one transaction, the address 0xB929...1896 deployed an attack contract at 0x6a21...39E7 that worked through SAFEs 3, 5, 8 and 18 in turn. For each one it called GlobalSettlement's processSAFE() to settle the SAFE's debt under the shutdown rules and leave its collateral free, called quitSystem() on GebProxyActions directly with its own address as dst, and then called freeCollateral() to turn the migrated position into withdrawable ETH-A collateral. GebSafeManager's safeAllowed check passed each time because each SAFE's recorded owner was the caller, GebProxyActions. The contract then exited the collateral through CollateralJoin, the adapter that holds the system's ETH collateral, unwrapped the WETH and kept 5.943599831844387377 ETH, the figure given by both DeFiHackLabs and crypto.training's reproduction.
As of the news coverage, Reflexer hadn't issued a public statement.
Olympix reads a contract the way an attacker does, as states, transitions and entry points, and it takes in the context around the code, such as the fact that GebProxyActions is meant to run through DSProxy, to infer the invariants the system relies on but never writes down. The invariant here fits in one sentence: only a SAFE's real owner, acting through their own proxy, should be able to move that SAFE's position to a new address.
A contract's state space looks infinite, but it isn't. The engine maps every path and uses signals to choose which ones to attack, narrowing from all paths to the feasible ones and then to the ones that can touch SAFE ownership. Olympix's static analysis surfaces the signal in this case: an external function that moves a SAFE to a caller-chosen destination, with no msg.sender validation, in a contract that's also deployed and callable on its own.
BugPoCer then takes that ownership rule as its target, which is the core of Counterexample-Guided Invariant Refutation: it starts from what the whole system promises about SAFEs and executes paths until one breaks the promise. The path it finds here starts from a state the code allows, a SAFE whose recorded owner is GebProxyActions, followed by a direct call to quitSystem() from an unrelated address. The engine's other method, Bounded Adversarial Verification in BugScout, starts from a flagged function and works through known attack strategies, and the two share the engine's symbolic execution. The counterexample comes back as the concrete sequence of state changes, the minimal conditions that trigger it and a generated Forge test that replays it, which is the September 1 drain produced in development instead of on mainnet.
Olympix runs this automatically in development, at every merge and before release. Olympix has flagged this class before in other codebases, functions that perform a privileged state change without checking who asked for it.
GebProxyActions' safety rested on how it would be called, and every individual check in the system did its job on the inputs it saw. Writing that call path down as a check closes the gap where a shared helper and a user's mistake meet. If your protocol relies on proxies and delegatecall to decide who can do what, book a technical walkthrough.
Protocol: Reflexer Finance (GEB), GebProxyActions
Chain: Ethereum
Date: September 1, 2026, 15:38:23 UTC (block 25883379)
Loss: 5.9436 ETH (5.943599831844387377 ETH per crypto.training, about $14,832)
Vulnerable code: GebProxyActions.sol (reflexer-labs/geb-proxy-actions)
Affected SAFE IDs (ETH-A): 3, 5, 8 and 18
Attacker EOA: 0xB929...1896
Attack contract: 0x6a21...39E7
GebProxyActions (vulnerable library): 0x84FE...8616
GebSafeManager: 0xdF88...B255
GlobalSettlement: 0x4d37...D9a9
ETH-A CollateralJoin: 0xE843...0BBE
Exploit tx (deploys the attack contract): 0xfbce...058f
Block: 25883379
SAFE opening txs, October 2020 (IDs 3, 5, 8, 18): 0x1eae...fb1b, 0xa52a...3585, 0x714a...6029, 0xc3c0...f0d5
DeFiHackLabs: ReflexerGEB_exp.sol proof of concept
crypto.training: Reflexer GEB, unauthenticated GebProxyActions.quitSystem drains library-owned SAFEs