The foundational modular DAO framework for Stacks — proposals as smart contracts, extensions as plugins, single-address ownership via sending context. Widely forked across the ecosystem.
| Contracts |
executor-dao.clar (core) + dao-traits-v4 (trait definitions)On-chain traits: SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS.dao-traits-v4
|
| Author | Marvin Janssen |
| Source | GitHub commit 4676718 (2022-07-11) · Traits verified on-chain via Hiro API |
| Clarity Version | Pre-Clarity 4 (Clarity 1 — no as-contract? asset allowances) |
| Lines of Code | 68 (core) + 86 (traits) = 154 total |
| Audit Date | 2026-02-25 |
| Confidence | 🟢 HIGH — compact codebase fully reviewed; all findings verified against source; no ambiguous external dependencies in core |
ExecutorDAO is the most influential DAO framework on Stacks, forked and deployed by ALEX, Charisma, Megapont, aibtcdev, and dozens of other projects. The design is elegantly minimal: a single core contract that executes proposals (which are themselves smart contracts) and manages a registry of extensions. Extensions are plugin contracts that add governance tokens, voting, treasuries, etc.
The core contract is only 68 lines — a testament to Clarity's expressiveness. The three key design principles are:
as-contract makes the DAO the tx-sender, so external ownable contracts can be governed by the DAOThe traits contract (dao-traits-v4) defines 10 traits covering SIP-010 tokens, proposals, extensions, governance tokens, NFTs, liquid FTs, CDK/EDK (land game mechanics), and fee-sharing.
construct function is a one-time bootstrap that permanently transfers executive control to the contract itselfas-contract Grants Unlimited Asset Authority to Proposals and ExtensionsLocation: execute (line 52), construct (line 60), request-extension-callback (line 67)
Description: The core contract uses as-contract in three places to execute proposals, bootstrap, and handle extension callbacks. In pre-Clarity 4, as-contract grants blanket authority over all assets held by the DAO contract — STX, fungible tokens, NFTs, everything. Any proposal or extension that runs under as-contract can move any asset the DAO holds, with no language-level restriction on which assets it can touch.
;; Proposal gets blanket asset authority via as-contract
(define-public (execute (proposal <proposal-trait>) (sender principal))
(begin
(try! (is-self-or-extension))
(asserts! (map-insert executed-proposals (contract-of proposal) block-height) err-already-executed)
(print {event: "execute", proposal: proposal})
(as-contract (contract-call? proposal execute sender)) ;; <-- blanket authority
)
)
;; Extension callback also gets blanket authority
(define-public (request-extension-callback (extension <extension-trait>) (memo (buff 34)))
(let ((sender tx-sender))
(asserts! (is-extension contract-caller) err-invalid-extension)
(asserts! (is-eq contract-caller (contract-of extension)) err-invalid-extension)
(as-contract (contract-call? extension callback sender memo)) ;; <-- blanket authority
)
)
Impact: A malicious or compromised proposal/extension can drain the entire DAO treasury in a single transaction. Since proposals are arbitrary smart contracts, a governance attack (e.g., flash-loan voting in a fork that uses token-weighted voting) could pass a proposal that steals all funds. The risk is amplified by the fact that most forks hold significant treasury assets.
Recommendation: Migrate to Clarity 4 and replace as-contract with as-contract? using explicit asset allowances. For example, a treasury management proposal should use (as-contract? (with-stx (with-ft .governance-token) ...)) to limit which assets the proposal can move. This provides defense-in-depth even if governance is compromised — a voting proposal can't touch treasury funds if it only has with-ft .governance-token allowance.
Location: Entire contract architecture
Description: Once bootstrapped, the only way to modify the DAO is through proposals executed by extensions. If a critical vulnerability is discovered in an extension, there is no way to pause the DAO, disable all extensions atomically, or prevent malicious proposals from being executed while a fix is prepared. The only recourse is to rush a counter-proposal through whatever voting mechanism the fork has implemented.
Impact: In an active exploit scenario, the time between detection and a fix proposal being voted on and executed could allow an attacker to drain the DAO. Many DAOs have multi-day voting periods, during which the exploit would be unstoppable.
Recommendation: Add an emergency guardian pattern: a designated principal (multisig or timelock) that can pause all proposal execution and extension calls. The guardian should be removable by governance to prevent centralization. Example: (define-data-var paused bool false) checked at the top of execute and set-extension.
sender Parameter in execute Is Caller-Controlled and UnverifiedLocation: execute function (line 49)
Description: The execute function takes a sender parameter that is passed to the proposal's execute function. This parameter is provided by the calling extension and is not validated against tx-sender or contract-caller. A malicious or buggy extension could pass an arbitrary principal as the sender, potentially impersonating another user to a proposal.
;; sender is caller-supplied, not verified
(define-public (execute (proposal <proposal-trait>) (sender principal))
(begin
(try! (is-self-or-extension))
(asserts! (map-insert executed-proposals (contract-of proposal) block-height) err-already-executed)
(print {event: "execute", proposal: proposal})
(as-contract (contract-call? proposal execute sender)) ;; proposal trusts sender
)
)
Impact: If a proposal uses the sender parameter for authorization decisions (e.g., "only admin X can trigger this proposal"), a compromised extension could spoof that identity. The severity depends on how downstream proposals use the sender value — in many forks, it's used purely for logging, making this low-impact. But in forks where proposals check sender identity, this is exploitable.
Recommendation: Either validate that sender equals tx-sender (or contract-caller) inside the core, or document clearly that proposals must NOT trust the sender parameter for authorization. The construct function correctly captures tx-sender in a local binding — execute should do the same.
construct Has No Replay Protection Across ForksLocation: construct function (line 56)
Description: The construct function checks that tx-sender equals the executive variable (set to the deployer at deploy time). After execution, it sets executive to the contract's own principal, preventing re-invocation. However, if the same deployer key is used across multiple chain forks or testnet deployments, the bootstrap proposal could be front-run or replayed if the deployer's key is compromised before construct is called.
(define-public (construct (proposal <proposal-trait>))
(let ((sender tx-sender))
(asserts! (is-eq sender (var-get executive)) err-unauthorised)
(var-set executive (as-contract tx-sender)) ;; one-time lock
(as-contract (execute proposal sender))
)
)
Impact: Low — requires compromising the deployer key in the window between deployment and construct call. In practice, deployers typically call construct in the same block or shortly after deployment.
Recommendation: Call construct in the same transaction as deployment when possible. Document that the deployer key should be rotated or secured after bootstrap.
as-contract? for Language-Level Asset SafetyLocation: Entire contract
Description: This contract was written for Clarity 1 (epoch 2.05). Clarity 4 (epoch 3.3, activated at Bitcoin block 923,222) introduced as-contract? with explicit asset allowances. This is the single most impactful improvement available to ExecutorDAO forks. Instead of granting blanket authority, each as-contract? call explicitly declares which assets (STX, specific FTs, specific NFTs) the enclosed code can move. This directly mitigates H-01.
Recommendation: All new forks should use Clarity 4. The core execute function should use as-contract? with the minimum required asset allowances per proposal type. Consider a proposal categorization system where treasury proposals get with-stx/with-ft while governance proposals get no asset allowances.
Location: dao-traits-v4
Description: The traits contract defines 10 traits in a single file, mixing core DAO concerns (proposal-trait, extension-trait, governance-token-trait) with domain-specific traits (cdk-trait, edk-trait for land games, liquid-ft-trait, share-fee-to-trait). This creates an unnecessary coupling: any contract that needs the proposal trait must deploy a contract that also contains game mechanics traits.
Impact: No security impact. This is a code organization concern that increases deployment costs and reduces modularity.
Recommendation: Split traits into separate contracts: core DAO traits (proposal, extension, governance-token), token traits (sip010, nft, liquid-ft, ft-plus), and domain-specific traits (cdk, edk, share-fee-to).
governance-token-trait Uses Non-Standard dmg- Prefixed FunctionsLocation: dao-traits-v4 — governance-token-trait
Description: The governance token trait defines functions with a dmg- prefix (e.g., dmg-get-balance, dmg-transfer, dmg-mint). "DMG" appears to be a specific token name (possibly "Dungeon Master's Gold" from the Megapont ecosystem). This naming couples the generic DAO framework to a specific project's naming convention, making it confusing for forks that use different governance token names.
Impact: No security impact. Forks must implement functions named dmg-* even if their token has nothing to do with DMG.
Recommendation: Use generic names: gov-get-balance, gov-transfer, gov-mint, gov-burn, gov-lock, gov-unlock, gov-get-locked, gov-has-percentage-balance.
is-self-or-extension uses tx-sender == as-contract tx-sender for self-calls and contract-caller for extension checks — correct and concisemap-insert on executed-proposals ensures proposals can only execute once — idempotency by designconstruct permanently transfers executive control, preventing re-initializationrequest-extension-callback validates both that the caller is an enabled extension AND that it matches the extension being called — prevents spoofingconstruct with a legitimate bootstrap proposal before the key is compromisedexecute — the core only checks that the caller is self or an extensionExecutorDAO's security is only as strong as its extensions. Forks should:
as-contract? and explicit asset allowances (mitigates H-01)sender parameter or document that proposals must not trust it (mitigates M-02)| Metric | Score | Weight | Weighted |
|---|---|---|---|
| Financial Risk | 2 — manages extensions that control assets | 3 | 6 |
| Deployment Likelihood | 3 — deployed on mainnet, widely forked | 2 | 6 |
| Code Complexity | 2 — 154 lines, multi-contract framework | 2 | 4 |
| User Exposure | 3 — most forked DAO framework on Stacks | 1.5 | 4.5 |
| Novelty | 3 — foundational pattern, first audit of original | 1.5 | 4.5 |
| Total (pre-penalty) | 2.5 / 3.0 | ||
| Clarity version penalty (pre-Clarity 4) | −0.5 | ||
| Final Score | 2.0 / 3.0 ✅ AUDIT | ||