All incidents

Xave DaoModule Takeover

Share
Oct 08, 2022 17:02 UTCAttackLoss: 100,000,000,000,000 HALOPending manual check2 exploit txWindow: 1m 48s
Estimated Impact
100,000,000,000,000 HALO
Label
Attack
Exploit Tx
2
Addresses
5
Attack Window
1m 48s
Oct 08, 2022 17:02 UTC → Oct 08, 2022 17:04 UTC

Exploit Transactions

TX 1Ethereum
0x71e2e18665d16bb2064bff4ae4c6e5a8bf366721e511242d403ad9aca5b138fa
Oct 08, 2022 17:02 UTCExplorer
TX 2Ethereum
0xc18ec2eb7d41638d9982281e766945d0428aaeda6211b4ccb6626ea7cff31f4a
Oct 08, 2022 17:04 UTCExplorer

Victim Addresses

0x8f9036732b9aa9b82d8f35e54b71faeb2f573e2fEthereum
0x7eaE370E6a76407C3955A2f0BBCA853C38e6454EEthereum
0xe94b97b6b43639e238c851a7e693f50033efd75cEthereum
0x6335A2E4a2E304401fcA4Fc0deafF066B813D055Ethereum
0x579270F151D142eb8BdC081043a983307Aa15786Ethereum

Loss Breakdown

100,000,000,000,000HALO

Similar Incidents

Root Cause Analysis

Xave DaoModule Takeover

1. Incident Overview TL;DR

An unprivileged Ethereum account exploited Xave Finance's governance module configuration to seize control of Safe-managed protocol assets. In transaction 0x71e2e18665d16bb2064bff4ae4c6e5a8bf366721e511242d403ad9aca5b138fa, the attacker created an arbitrary DaoModule proposal and self-answered the linked Realitio question. In transaction 0xc18ec2eb7d41638d9982281e766945d0428aaeda6211b4ccb6626ea7cff31f4a, the attacker waited for the one-second timeout to elapse and then executed four Safe module transactions that minted 100000000000000000000000000000000 HALO and transferred ownership of HaloToken, 0x6335A2E4a2E304401fcA4Fc0deafF066B813D055, and PrimaryBridge.

The root cause was a permissionless proposal path on DaoModule combined with production parameters that made oracle approval effectively trivial: questionTimeout=1, questionCooldown=0, and minimumBond=0. That combination let any caller turn the enabled DaoModule into a public Safe executor.

2. Key Background

Xave's DaoModule is attached to the Safe proxy at 0x7eaE370E6a76407C3955A2f0BBCA853C38e6454E. DaoModule stores a proposal as a Realitio question built from proposalId and a list of transaction hashes; later, executeProposalWithIndex checks the recorded question, verifies a positive oracle result, and forwards the chosen call to the Safe.

The Safe implementation matters because execTransactionFromModule is a privileged path for enabled modules. It bypasses owner signatures and only checks that msg.sender is an enabled module:

function execTransactionFromModule(address to, uint256 value, bytes memory data, Enum.Operation operation)
    public
    returns (bool success)
{
    require(msg.sender != SENTINEL_MODULES && modules[msg.sender] != address(0), "Method can only be called from an enabled module");
    success = execute(to, value, data, operation, gasleft());
}

The asset-side impact is straightforward. HaloToken is Ownable, and its mint function is gated only by onlyOwner and canMint:

function mint(address account, uint256 amount) external onlyOwner {
    require(canMint == true, "Total supply is now capped, cannot mint more");
    _mint(account, amount);
}

Before exploitation, the Safe owned HaloToken, the unnamed ownable component at 0x6335A2E4a2E304401fcA4Fc0deafF066B813D055, and PrimaryBridge. Once the attacker could make the Safe execute arbitrary calls through DaoModule, minting and ownership seizure followed directly.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an access-control failure in governance execution, not a bug in Safe itself. DaoModule exposes addProposal and addProposalWithNonce publicly, so any caller can register a proposal and create the corresponding Realitio question. On the live deployment used in the incident, the module also accepted a one-second question timeout, zero additional cooldown, and zero minimum answer bond. Those settings reduced proposal approval to a self-submitted answer=1 on Realitio plus a short wait. After that, executeProposalWithIndex only required that the proposal hash match, the oracle answer be positive, and the previous indexed transaction already be executed. Because DaoModule was an enabled Safe module, each validated proposal step was forwarded to execTransactionFromModule, which performs no owner-signature verification. The resulting system invariant failure was that governance approval no longer represented privileged consensus; it became a permissionless path to arbitrary Safe execution.

4. Detailed Root Cause Analysis

The decisive breakpoint is in DaoModule itself. addProposal simply delegates to addProposalWithNonce, and addProposalWithNonce does not restrict who may call it:

function addProposal(string memory proposalId, bytes32[] memory txHashes) public {
    addProposalWithNonce(proposalId, txHashes, 0);
}

function addProposalWithNonce(string memory proposalId, bytes32[] memory txHashes, uint256 nonce) public {
    string memory question = buildQuestion(proposalId, txHashes);
    bytes32 questionHash = keccak256(bytes(question));
    ...
    questionIds[questionHash] = expectedQuestionId;
    bytes32 questionId = oracle.askQuestion(templateId, question, arbitrator, timeout, 0, nonce);
    require(expectedQuestionId == questionId, "Unexpected question id");
}

The execution gate is similarly weak under the live configuration:

function executeProposalWithIndex(...) public {
    bytes32 questionId = questionIds[questionHash];
    require(questionId != bytes32(0), "No question id set for provided proposal");
    require(oracle.resultFor(questionId) == bytes32(uint256(1)), "Transaction was not approved");
    require(minimumBond == 0 || minimumBond <= oracle.getBond(questionId), "Bond on question not high enough");
    require(finalizeTs + uint256(questionCooldown) < block.timestamp, "Wait for additional cooldown");
    require(executor.execTransactionFromModule(to, value, data, operation), "Module transaction failed");
}

At block 15704736, the deployed DaoModule had questionTimeout=1, questionCooldown=0, minimumBond=0, and answerExpiration=604800. That means an unprivileged caller could create any proposal, answer it positively with a 1 wei bond, wait slightly more than one second, and then execute it through the Safe.

The trace for the first adversary transaction shows exactly that sequence:

DaoModule::addProposalWithNonce("2", [...], 0)
Realitio::submitAnswer{value: 1}(..., 0x...01, 0)

The second adversary transaction shows DaoModule forwarding the attacker-selected bundle through the Safe module path:

0x8f9036732b9aa9b82D8F35e54B71faeb2f573E2F::executeProposalWithIndex("2", [...], 0xE94B97..., 0, 0x40c10f19..., 0, 0)
  GnosisSafe::execTransactionFromModule(0xE94B97..., 0, 0x40c10f19..., 0)
  0xE94B97...::mint(0x0f44f3489d17e42ab13a6beb76e57813081fc1e2, 100000000000000000000000000000000)

The same execution transaction then transferred ownership of all three victim-owned contracts to the attacker via transferOwnership(address). The collected HALO balance diff confirms the semantic effect: the attacker EOA's HALO balance increased from 0 to 100000000000000000000000000000000. This is why the correct invariant is: only governance-authorized proposal bundles should ever reach execTransactionFromModule, and that invariant failed because proposal creation was public while approval parameters were effectively permissionless.

5. Adversary Flow Analysis

The adversary flow was a two-transaction takeover.

  1. 0x71e2e18665d16bb2064bff4ae4c6e5a8bf366721e511242d403ad9aca5b138fa at block 15704737 The attacker EOA 0x0f44f3489d17e42ab13a6beb76e57813081fc1e2 deployed helper contract 0xe167cdaac8718b90c03cf2cb75dc976e24ee86d3, computed four DaoModule transaction hashes, called addProposalWithNonce("2", txHashes, 0), and then submitted a positive Realitio answer with bond 1.

  2. 0xc18ec2eb7d41638d9982281e766945d0428aaeda6211b4ccb6626ea7cff31f4a at block 15704746 After the timeout elapsed, the attacker called executeProposalWithIndex four times in sequence. The four executed calls were: HaloToken.mint(attacker, 100000000000000000000000000000000), HaloToken.transferOwnership(attacker), 0x6335A2E4a2E304401fcA4Fc0deafF066B813D055.transferOwnership(attacker), and PrimaryBridge.transferOwnership(attacker).

The execution trace captures that sequence directly:

...::executeProposalWithIndex("2", [...], 0xE94B97..., 0, 0x40c10f19..., 0, 0)
  0xE94B97...::mint(attacker, 100000000000000000000000000000000)
...::executeProposalWithIndex("2", [...], 0xE94B97..., 0, 0xf2fde38b..., 0, 1)
  0xE94B97...::transferOwnership(attacker)
...::executeProposalWithIndex("2", [...], 0x6335A2..., 0, 0xf2fde38b..., 0, 2)
  0x6335A2...::transferOwnership(attacker)
...::executeProposalWithIndex("2", [...], 0x579270..., 0, 0xf2fde38b..., 0, 3)
  PrimaryBridge::transferOwnership(attacker)

No privileged Safe owner signatures, private keys, or attacker-only artifacts were needed. The only prerequisites were public contract interfaces, the enabled DaoModule, and the permissive Realitio settings present at the exploit block.

6. Impact & Losses

The on-chain loss artifact shows an unauthorized mint of HALO to the attacker:

{
  "token": "0xe94b97b6b43639e238c851a7e693f50033efd75c",
  "holder": "0x0f44f3489d17e42ab13a6beb76e57813081fc1e2",
  "before": "0",
  "after": "100000000000000000000000000000000",
  "delta": "100000000000000000000000000000000"
}

The incident also transferred ownership of HaloToken, the unnamed ownable component at 0x6335A2E4a2E304401fcA4Fc0deafF066B813D055, and PrimaryBridge from the Safe to the attacker. The measured token loss in the collected artifacts is:

  • HALO: 100000000000000000000000000000000 raw units, decimal=18

Because the attacker gained protocol ownership roles in addition to the token mint, the impact was broader than a single transfer: the Safe's governance boundary over token supply control and bridge-related administration was broken.

7. References

  1. DaoModule source: 0x8f9036732b9aa9b82d8f35e54b71faeb2f573e2f
  2. Gnosis Safe implementation source: 0x34cfac646f301356faa8b21e94227e3583fe3f5f
  3. HaloToken source: 0xe94b97b6b43639e238c851a7e693f50033efd75c
  4. Adversary tx 0x71e2e18665d16bb2064bff4ae4c6e5a8bf366721e511242d403ad9aca5b138fa trace and receipt
  5. Execution tx 0xc18ec2eb7d41638d9982281e766945d0428aaeda6211b4ccb6626ea7cff31f4a trace and balance diff
  6. Auditor oracle and validator execution log confirming the same semantic end state