Root cause
DAOstack's UController.newOrganization(Avatar _avatar) is permissionless and resolves Reputation and Token contracts by calling _avatar.nativeReputation() and _avatar.nativeToken() on the caller-supplied address. There is no check that the returned Reputation contract is not already governed by a different organization.
// DAOstack UController — simplified (vulnerable)
function newOrganization(Avatar _avatar) public {
Reputation rep = Reputation(_avatar.nativeReputation());
// MISSING: require(rep.owner() == address(_avatar) || rep.isController(address(this)))
// Registers _avatar as a scheme with full mintReputation rights over `rep`
schemes[address(_avatar)][rep] = SchemePermissions({
isRegistered: true,
canRegisterSchemes: true,
canUpgradeController: true,
canGenericCall: true
});
emit NewOrganization(address(_avatar));
}
An attacker can deploy a shape-shifting Avatar that:
- Returns dummy/placeholder addresses on initial introspection calls
- Switches to return the real victim Reputation address on the call made inside
newOrganization()
This registers the fake Avatar as a fully-privileged scheme over the real Reputation contract.
Attack steps
| Step | Action | Detail |
|---|---|---|
| 1 | Deploy shape-shifting fake Avatar contract | Returns dummy rep/token initially; switches to real Genesis REP on UController's getter call |
| 2 | Call UController.newOrganization(fakeAvatar) | UController calls fakeAvatar.nativeReputation() → receives real Genesis REP address |
| 3 | UController registers fakeAvatar as scheme with mintReputation rights over real REP | No ownership proof required |
| 4 | FakeAvatar calls mintReputation(20000, attacker) via UController | Inflates supply from 9,738.6 → 29,738.6 REP |
| 5 | Attacker now holds ~67% of all REP — majority governance | Supermajority in one tx |
| 6 | Submit and vote for proposal calling sendEther(4.025 ETH, attacker) | Vote passes with attacker's REP |
| 7 | 4.025 ETH (~$11,000) drained from Genesis Alpha DAO treasury | Ethereum mainnet block 26,059,089 |
Impact
- 4.025 ETH (~$11,000) (est.) drained from Genesis Alpha DAO treasury
- REP supply inflated from 9,738.6 to 29,738.6 REP (~205% inflation)
- Governance integrity of Genesis Alpha DAO permanently compromised
- Pattern applies to all DAOstack UController deployments that hold on-chain treasury funds
Lessons for auditors
- Permissionless registration + privileged actions = critical escalation path. Any function that grants mint or governance authority must verify the caller owns the target contract:
require(target.owner() == address(avatar))or an equivalentisControllercheck. - Shape-shifting contract attacks. Never trust a contract's return value in a privileged context without verifying it against a pre-committed immutable reference. Snapshot values at construction, not at call time.
- Governance quorum sanity checks. Flag any single mint that would give one address majority voting power in one transaction; require a time-lock or multisig for mints above, e.g., 10% of existing supply.
- Legacy DAO contract audits. DAOstack UController is a long-deployed governance primitive still holding real funds. All governance frameworks with permissionless registration primitives deserve fresh review for this class of binding-assertion flaw.
- Fuzz
newOrganizationwith adversarial Avatar mocks that return different values on consecutive calls, verifying that any returned address is a contract actually governed by the supplied Avatar.