The DAO
Summary
ExpandThe codebase implements a Decentralized Autonomous Organization framework comprising a governance contract (`DAO.sol`), a token contract (`Token.sol`), token creation logic (`TokenCreation.sol`), and several contractor offer contracts (`Offer.sol`, `PFOffer.sol`, `RewardOffer.sol`). The architecture separates token ownership from governance through a `Plutocracy` interface that enforces token transfer restrictions during active votes. Proposals follow a lifecycle of creation, voting, and execution, with quorum requirements and a curator-managed recipient whitelist. The offer contracts employ a `noEther` modifier to prevent accidental ETH deposits, and proposal deposits serve as an economic deterrent against spam. The design reflects an early attempt at on-chain governance with meaningful separation of concerns between token mechanics, voting logic, and proposal execution.
The audit identified 10 findings across the merged corpus: 3 Critical, 3 High, and 4 Medium severity. The most severe issue is a reentrancy vulnerability in `executeProposal` (C-1) — the exact bug exploited in the historic June 2016 DAO hack — where an external call via `p.recipient.call.value(p.amount)(_transactionData)` occurs before `closeProposal` updates accounting state, enabling a malicious recipient to recursively drain the contract's entire ETH balance. Equally critical, the token transfer blocking logic in `Token.sol` is inverted (C-2), permitting transfers for blocked voters and rejecting them for unblocked ones, rendering the vote-manipulation prevention mechanism entirely non-functional. The third critical finding (C-3) reveals that `unVoteAll()` passes the loop index rather than the proposal ID when clearing votes, silently corrupting vote state across unrelated proposals. The high-severity findings compound these issues: `minQuorum` divides by a value derived from `actualBalance()` without a zero check (H-1), the DTHPool `__callback` function lacks access control allowing arbitrary vote execution (H-2), and voting power is determined by spot `balanceOf()` rather than a snapshot mechanism (H-3), enabling vote multiplication through token transfers between accounts even if the blocking bug were corrected.
The codebase does demonstrate deliberate security thinking in several areas — the `proposalPassed` flag set before external calls represents an attempt to mitigate single-proposal reentrancy, the curator whitelist limits proposal execution targets, and the deposit mechanism discourages governance spam. These patterns indicate awareness of common attack vectors, though the implementation falls short of the rigor required for a contract holding significant value.
Findings
10 issues identified
Reentrancy in executeProposal allows recursive fund drainage
DAO.sol:258
View Details
Description
The executeProposal function calls p.recipient.call.value(p.amount)(_transactionData) forwarding all gas before calling closeProposal. While p.proposalPassed is set before the call, closeProposal (which updates sumOfProposalDeposits and p.open) runs after, allowing a malicious recipient to re-enter and observe inconsistent state. This is the vulnerability exploited in the 2016 DAO hack.
Impact
An attacker can drain the entire ETH balance of the DAO through recursive calls during proposal execution.
Recommendation
Move closeProposal and all state updates before the external call, following the checks-effects-interactions pattern.
Fix
By moving closeProposal before the external call, all state changes (sumOfProposalDeposits update, p.open = false) are finalized before any untrusted code executes. This follows the checks-effects-interactions pattern and prevents reentrancy from observing inconsistent accounting state.
DAO.sol
Inverted blocking logic in Token.transfer permits unrestricted vote manipulation
Token.sol:77
View Details
Description
Token.transfer requires plutocracy.getOrModifyBlocked(_to) to return true for the transfer to succeed. However, getOrModifyBlocked returns true when an account IS blocked and false when NOT blocked. This means transfers only succeed TO blocked accounts and fail for unblocked recipients. The check should be on the sender (not recipient) and should prevent transfer when blocked (not require it).
Impact
The token transfer blocking mechanism is completely inverted. Blocked voters can freely transfer tokens since the sender is never checked. Normal transfers to non-voting accounts fail. This renders the vote-manipulation prevention useless and breaks normal token functionality.
Recommendation
Check the sender instead of recipient, and invert the condition to prevent transfer when the sender IS blocked.
Fix
The fix checks whether the sender (msg.sender) is blocked rather than the recipient, and inverts the condition so transfers succeed when the sender is NOT blocked. This correctly prevents token holders from transferring while they have active votes on open proposals.
Token.sol
unVoteAll() passes loop index instead of proposal ID, corrupting vote state
DAO.sol
View Details
Description
In unVoteAll(), the call unVote(i) passes the loop index instead of the actual proposal ID votingRegister[msg.sender][i]. This causes unVote to operate on wrong proposals (0, 1, 2…) instead of the user’s actual voted proposals, corrupting vote tallies and enabling double-voting.
Impact
Vote tallies for unrelated proposals are corrupted (votes subtracted from wrong proposals). The user’s actual votes remain, and after votingRegister is wiped, they can vote again on the same proposals, doubling their voting weight.
Recommendation
Pass the actual proposal ID from the voting register instead of the loop index.
Fix
Replace the loop index i with the actual proposal ID votingRegister[msg.sender][i] so unVote operates on the correct proposals.
DAO.sol
Division by zero in minQuorum when actualBalance() is zero
DAO.sol
View Details
Description
The minQuorum() function divides by actualBalance(). If actualBalance() returns 0 (when DAO balance equals sumOfProposalDeposits), this causes a revert, preventing any proposal from being executed.
Impact
An attacker can submit proposals whose deposits sum to the DAO’s balance, permanently blocking all proposal execution and freezing DAO governance.
Recommendation
Add a zero-balance check that returns totalSupply as the quorum requirement when actualBalance is zero.
Fix
When actualBalance is zero, return totalSupply as a safe maximum quorum requirement, preventing the division by zero while maintaining conservative governance.
DAO.sol
DTHPool __callback lacks access control, allowing arbitrary vote execution
DTHPool.sol
View Details
Description
The __callback function is callable by anyone, not just the Oraclize service. An attacker can call it with arbitrary oraclize IDs to trigger executeVote on proposal 0 (the default mapping value) or front-run legitimate callbacks.
Impact
An attacker can trigger premature or unauthorized vote execution in the DTHPool, potentially disrupting the delegate’s intended voting strategy.
Recommendation
Add access control to verify the caller is the Oraclize callback address.
Fix
Verify that only the Oraclize callback address can invoke this function, preventing unauthorized vote execution.
DTHPool.sol
Governance voting power based on spot balanceOf() without snapshot mechanism
DAO.sol:206-220
View Details
Description
The vote() function uses token.balanceOf(msg.sender) to determine voting weight at the time of the call. There is no snapshot or checkpoint mechanism (e.g., ERC20Votes / ERC20Snapshot) to record balances at proposal creation time. An attacker can acquire a large token position (via flash loan, borrowing, or market purchase), vote on a proposal, and then transfer or sell the tokens. The unVote() function compounds this issue: it also uses the current balanceOf() to subtract votes, meaning if a voter transfers tokens between voting and unvoting, the accounting becomes inconsistent (subtracting a different amount than was added).
Impact
An attacker with temporary access to a large token balance (e.g., via a flash loan on a DEX or lending protocol that lists the DAO token) could swing any governance vote in a single transaction. They could pass malicious proposals to drain the DAO’s entire ether balance. Additionally, the vote weight mismatch between vote() and unVote() can corrupt the yea/nay tallies—a voter who transfers tokens after voting and then unvotes will leave phantom votes in the tally.
Recommendation
Use a snapshot-based voting mechanism where voting power is determined by token balances at the block when the proposal was created (similar to ERC20Votes getPastVotes()). Alternatively, store the voter’s balance at vote time in the proposal struct and use that stored value for unvoting. Consider adding a time-lock between token acquisition and voting eligibility.
Fix
DAOTokenCreationProxyTransferer unchecked send loses user funds
DAOTokenCreationProxyTransferer.sol
View Details
Description
In sendValues(), when token creation fails, the fallback send to the owner is not checked: owner.send(this.balance). If the send fails, ether is permanently trapped with no recovery mechanism.
Impact
Users who fund the proxy contract after closing time or when token creation fails permanently lose their ether if the refund send fails.
Recommendation
Revert the transaction if the refund send fails.
Fix
Revert on failed refund so the ether remains accessible and the user can retry the withdrawal.
DAOTokenCreationProxyTransferer.sol
simpleWithdrawTrustee trusteeWithdraw() has unchecked send and potential underflow
simpleWithdrawTrustee.sol
View Details
Description
The trusteeWithdraw() function performs arithmetic (this.balance + mainDAO.balanceOf(this)) - mainDAO.totalSupply() which can underflow if the sum is less than totalSupply. The send() return value is also unchecked.
Impact
An underflow would cause a massive erroneous send amount. If the send fails, trustee funds are stuck.
Recommendation
Add an underflow check and verify the send return value.
Fix
Add an explicit check that total exceeds totalSupply before subtraction to prevent underflow, and revert if the send fails.
simpleWithdrawTrustee.sol
Denial of Service via Unbounded Loop in unVoteAll()
DAO.sol:219-228
View Details
Description
The unVoteAll() function iterates over votingRegister[msg.sender], an array that grows unboundedly each time vote() is called (line 208). Every call to vote() pushes a new entry via votingRegister[msg.sender].push(_proposalID) with no size limit. If a user votes on a large number of proposals, the unVoteAll() loop will eventually exceed the block gas limit, permanently bricking the function for that user. The code itself contains the comment // DANGEROUS loop with dynamic length - needs improvement.
Impact
A user who has voted on many proposals will be unable to call unVoteAll() as it will revert due to out-of-gas. Since unVoteAll() resets blocked[msg.sender] and removes votes, the user could become permanently blocked from transferring their DAO tokens. The commented-out withdraw() function also calls unVoteAll(), meaning any future withdrawal mechanism relying on it would also be bricked.
Recommendation
Implement a paginated unvote pattern that accepts a range of indices, or use a pull-over-push pattern where individual proposals can be unvoted one at a time. Alternatively, cap the number of active votes per user.
Fix
DoS via Unexpected Revert in executeProposal deposit refund
DAO.sol:executeProposal (line with `if (!p.creator.send(p.proposalDeposit)) throw`)
View Details
Description
When a proposal reaches quorum, executeProposal attempts to refund the proposal deposit to p.creator using .send() and reverts the entire transaction if the send fails. If the proposal creator is a smart contract that reverts on receiving ETH (e.g., has no payable fallback, or intentionally reverts), the deposit refund fails, causing the entire executeProposal call to revert. This permanently blocks execution of that proposal, even though it legitimately passed the vote. The proposal remains open, blocking voters’ tokens (via the blocked mapping) until executeProposalPeriod expires and someone calls executeProposal again to trigger closeProposal. Note that in the !allowedRecipients branch, the return value of p.creator.send() is intentionally NOT checked (the comment acknowledges this), but in the quorum-met branch the throw on failure creates the DoS vector.
Impact
A malicious actor can create a proposal from a contract that refuses ETH, gather enough community votes to reach quorum, and then the proposal can never execute. This wastes community voting resources, blocks voters’ tokens for the executeProposalPeriod duration, and prevents legitimate proposals from being carried out. The proposal deposit is also locked until the proposal is eventually closed.
Recommendation
Use a pull-over-push pattern for deposit refunds. Instead of sending the deposit inline and reverting on failure, credit the deposit to the creator’s balance in a mapping and let them withdraw it separately. Alternatively, remove the throw on send failure (as was already done in the !allowedRecipients branch) so that a failed refund does not block proposal execution.
Fix
Conclusion
ExpandThe DAO framework exhibits fundamental security deficiencies across its core subsystems — proposal execution, token transfer restrictions, and vote accounting — that collectively render the contract unable to safely custody funds or execute governance decisions. The reentrancy in executeProposal (C-1) permits complete fund drainage in a single transaction, the inverted blocking logic (C-2) leaves vote manipulation entirely unmitigated, and the index-versus-ID confusion in unVoteAll (C-3) means any attempt to clear votes corrupts governance state. The high-severity findings further erode trust in the governance model: division-by-zero freezes in minQuorum (H-1) can permanently halt proposal passage, the unprotected DTHPool callback (H-2) allows unauthorized vote casting, and the absence of balance snapshots (H-3) makes voting weight trivially gameable. The four medium-severity findings — unchecked send calls in the proxy transferer (M-1) and trustee withdrawal (M-2), unbounded iteration in unVoteAll (M-3), and deposit refund revert DoS in executeProposal (M-4) — introduce additional vectors for fund loss and governance disruption.
The DAO should not be deployed in its current state, consistent with the repository’s own notice that the code is unmaintained and should not be used as-is. Remediation would require fundamental architectural changes: adopting the checks-effects-interactions pattern throughout all external calls, implementing explicit reentrancy guards, correcting the transfer blocking predicate, recording vote weights at vote time via a snapshot mechanism, adding access control to callback functions, and bounding all loops with gas-safe iteration patterns. Given the depth and breadth of the findings — spanning reentrancy, access control, arithmetic safety, and governance integrity — a complete re-audit would be necessary after these changes are applied.
Legal Disclaimer: This report covers the code submitted for analysis. It does not account for infrastructure, deployment configuration, third-party dependencies, or changes made after the audit date. Automated analysis may produce false positives or miss context-dependent vulnerabilities. audited.xyz provides this report “as is” without warranty of any kind.