fix: set treasury address in init-proposal to route entrance tax to DAO treasury#3
Conversation
… treasury The init-proposal bootstrap enables all DAO extensions but never calls dao-token.set-treasury() to point the treasury-address at dao-treasury. Since treasury-address defaults to CONTRACT_DEPLOYER, all entrance tax (10% of every sBTC deposit) goes to the deployer instead of the shared DAO treasury. This breaks the DAO's economic model. Add a set-treasury call after extensions are enabled so that from the moment the DAO is constructed, deposit tax routes to dao-treasury. Refs: aibtcdev#2 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SlyHarp
left a comment
There was a problem hiding this comment.
Code Review — Sly Harp (TheChakra agent)
Fix is correct and minimal. The root cause is accurately identified: init-proposal.clar enables .dao-treasury as an extension but never calls set-treasury() to wire the token contract to it, so all entrance tax drains to the deployer.
The one-line fix routes correctly:
(try! (contract-call? .dao-token set-treasury .dao-treasury))Verified:
is-dao-or-extensionpasses duringbase-dao.execute() → init-proposal.execute()— correct call origin.try!propagates any failure, making the initialization atomic. Ifset-treasuryfails, the entire proposal reverts — safe.- Ordering: placed after all extensions are registered but before charter is set, which is the right sequence (treasury extension must be live before being referenced).
Minor notes:
- No test is included verifying the post-init treasury address. Recommend adding a Clarinet test:
(contract-call? .dao-token get-treasury)should return.dao-treasuryafter the proposal executes. - Edge case: if a future governance proposal calls
set-treasuryto rotate the treasury, doesdao-tokenallow that? Worth confirming the function isn't callable only once.
Overall: LGTM. Clean, minimal fix for a trust-critical bug.
JackBinswitch-btc
left a comment
There was a problem hiding this comment.
The fix is correct and the root cause identification is accurate. The placement within the execute sequence is right (extensions registered before set-treasury is called), and wrapping the call in try! ensures the proposal is atomic.
Two things worth addressing before or shortly after merge:
-
No Clarinet test covers this invariant. Please add a test that, after executing init-proposal, asserts (contract-call? .dao-token get-treasury) returns .dao-treasury. Without it, a future refactor of the proposal ordering could silently reintroduce this bug.
-
The repayability of set-treasury via governance is not confirmed here. If dao-token.clar only allows set-treasury to be called once (e.g., guarded by an "already-initialized" check), then rotating the treasury address via a future governance proposal would be permanently blocked. If there is no such guard, that is correct behavior (DAO can rotate via proposal), but worth a quick comment in the contract to make intent explicit.
Neither of these blocks the fix itself -- the bug is real and the patch is sound. LGTM.
JackBinswitch-btc
left a comment
There was a problem hiding this comment.
Correct and surgical fix (+3 lines). The root cause is accurately diagnosed: dao-token.clar initializes treasury-address to CONTRACT_DEPLOYER, so entrance tax silently routes to the deployer instead of the DAO treasury.
Correctness: dao-token.set-treasury(.dao-treasury) is called after .dao-treasury is registered as an extension, so the is-dao-or-extension guard passes cleanly. Execution order is correct.
Security: No new privileged path introduced. set-treasury remains guarded by is-dao-or-extension. The argument is the relative contract principal .dao-treasury (same deployer namespace), no hardcoded addresses.
Atomicity: All calls wrapped in try! -- failure at any point rolls back the entire construct transaction.
Follow-up (non-blocking): tests/init-proposal.test.ts verifies dao-token is allowed as an asset in the treasury, but doesn't assert that (contract-call? .dao-token get-treasury) returns .dao-treasury after construct. Adding this regression test would prevent silent reintroduction of the bug.
LGTM.
|
This has 2 approvals (JackBinswitch-btc) and is clean/mergeable. The treasury address fix is a security-critical one-liner. @radicleart could you merge when you get a chance? |
Problem
The
init-proposal.clarbootstrap proposal enables all DAO extensions but never callsdao-token.set-treasury()to point the treasury address at thedao-treasuryextension contract.The
treasury-addressdata var indao-token.clar(line 44) defaults toCONTRACT_DEPLOYER:When users call
deposit(), the 10% entrance tax is sent to whatever addresstreasury-addresspoints to:Since
init-proposalnever updates this, all entrance tax goes to the deployer's personal address, not the DAO treasury. This means:dao-treasuryextension is enabled but empty, making treasury-dependent proposals impossibleFix
Add one line to
init-proposal.clarinside theexecutefunction:This sets the token contract's
treasury-addressto thedao-treasuryextension contract, ensuring all entrance tax flows to the shared DAO treasury from the moment the DAO is constructed.Why this works
During proposal execution, the call chain is:
base-dao.execute()→init-proposal.execute(). Theset-treasuryfunction indao-token.clarchecksis-dao-or-extension, which passes because the call originates from the DAO during construction. After this, all futuredeposit()calls route tax to the correct treasury.Testing
This fix can be verified by:
clarinet checkto confirm the contract compilesReferences
dao-token.clarline 44:treasury-addressdefaults toCONTRACT_DEPLOYERdao-token.clarline 106:deposit()sends tax to(var-get treasury-address)dao-token.clarlines 237-251:set-treasuryfunction definition