DAO Treasury Management Development
A DAO treasury is not just a multisig wallet with money. It's an asset management system that must survive team changes, endure bear markets, and remain resistant to governance attacks. MakerDAO lost 35% of treasury value in 2022 not from hacks — from poor native token allocation. Proper treasury architecture starts with diversification and ends with execution automation.
Treasury System Architecture
Modern DAO treasury is built on several layers with different access levels:
Level 1: Core Treasury (Gnosis Safe + Governor)
Main asset storage managed via on-chain governance. Any spending requires full governance cycle: proposal → voting → timelock → execution. TimelockController is mandatory intermediary.
Governance Governor ──propose──► TimelockController ──execute──► Gnosis Safe
▲ (48h delay) │
│ ▼
Token holders Treasury assets
Gnosis Safe here is final storage, not management tool. This is important: Safe shouldn't be signer-managed for main funds.
Level 2: Operational Budget (Sub-DAO Safe)
Separate Gnosis Safe for operational spending with limit (e.g., $50k/month). Managed by core team with 3/5 multisig. Funded from Core Treasury via regular governance proposal quarterly.
Level 3: Streaming Payments (Superfluid / Sablier)
Salaries and grants via token streaming — contributors receive continuous token flow that can be stopped anytime. No manual payments needed.
Treasury Smart Contracts
Treasury Controller
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/governance/TimelockController.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract DAOTreasury is AccessControl {
using SafeERC20 for IERC20;
bytes32 public constant GOVERNOR_ROLE = keccak256("GOVERNOR_ROLE");
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
// Limits for operational spending without governance
mapping(address => uint256) public monthlySpendLimit;
mapping(address => mapping(uint256 => uint256)) public monthlySpent; // token -> month -> amount
event FundsDisbursed(address indexed token, address indexed recipient, uint256 amount, string reason);
event AllocationUpdated(address indexed token, uint256 amount, string strategy);
constructor(address _governor, address _operator) {
_grantRole(DEFAULT_ADMIN_ROLE, _governor);
_grantRole(GOVERNOR_ROLE, _governor);
_grantRole(OPERATOR_ROLE, _operator);
}
// Disbursement via governance (no limits)
function disburse(
address token,
address recipient,
uint256 amount,
string calldata reason
) external onlyRole(GOVERNOR_ROLE) {
IERC20(token).safeTransfer(recipient, amount);
emit FundsDisbursed(token, recipient, amount, reason);
}
// Operational spending with monthly limits
function operationalDisburse(
address token,
address recipient,
uint256 amount
) external onlyRole(OPERATOR_ROLE) {
uint256 currentMonth = block.timestamp / 30 days;
uint256 spent = monthlySpent[token][currentMonth];
require(
spent + amount <= monthlySpendLimit[token],
"Monthly limit exceeded"
);
monthlySpent[token][currentMonth] = spent + amount;
IERC20(token).safeTransfer(recipient, amount);
emit FundsDisbursed(token, recipient, amount, "operational");
}
function setMonthlyLimit(address token, uint256 limit)
external
onlyRole(GOVERNOR_ROLE)
{
monthlySpendLimit[token] = limit;
}
// ETH receive
receive() external payable {}
}
Budget Streams via Sablier V2
Sablier V2 LockupLinear is standard for streaming payments in DAOs:
import { ISablierV2LockupLinear } from "@sablier/v2-core/interfaces/ISablierV2LockupLinear.sol";
import { LockupLinear, Broker } from "@sablier/v2-core/types/DataTypes.sol";
contract TreasuryStreaming {
ISablierV2LockupLinear public immutable sablier;
IERC20 public immutable daoToken;
// Create salary stream for contributor
function createContributorStream(
address contributor,
uint128 totalAmount,
uint40 startTime,
uint40 endTime,
uint40 cliffDuration,
bool cancelable
) external returns (uint256 streamId) {
daoToken.approve(address(sablier), totalAmount);
LockupLinear.CreateWithDurations memory params = LockupLinear.CreateWithDurations({
sender: address(this),
recipient: contributor,
totalAmount: totalAmount,
asset: daoToken,
cancelable: cancelable, // true for probation
transferable: false, // can't transfer rights
durations: LockupLinear.Durations({
cliff: cliffDuration, // e.g., 3 months
total: endTime - startTime
}),
broker: Broker(address(0), ud60x18(0))
});
streamId = sablier.createWithDurations(params);
}
// Cancel stream (on contributor termination)
function cancelStream(uint256 streamId) external {
sablier.cancel(streamId);
// Unpaid funds automatically return to treasury
}
}
Asset Diversification
Holding 80%+ treasury in native token is standard rookie mistake. In bear markets such treasury loses 80-95% purchasing power and DAO can't fund development.
Recommended Structure for Protocol with $5M+ Treasury
| Asset | Share | Reasoning |
|---|---|---|
| Stablecoins (USDC, DAI) | 40-50% | Operations, runway |
| ETH | 20-30% | Liquid reserve, yield via staking |
| Native token | 20-30% | Governance, incentives |
| Diversified DeFi (wBTC, etc.) | 0-10% | Optional |
Yield on Stablecoin Portion
Idle stablecoins in treasury are lost yield. Popular strategies:
Aave/Compound: simple lending, 3-8% APY on USDC. Minimal risk, instant withdrawal.
Maker DSR (DAI Savings Rate): official DAI yield from MakerDAO.
Yearn Finance: automated yield strategy management. Requires trusting Yearn vaults.
Governance proposal for each yield strategy change — correct approach for transparency.
Monitoring and Analytics
On-chain Treasury Metrics
// Daily treasury snapshot script
interface TreasurySnapshot {
timestamp: number;
assets: {
token: string;
balance: bigint;
usdValue: number;
}[];
totalUsdValue: number;
runwayMonths: number; // at current burn rate
}
async function getTreasurySnapshot(
provider: ethers.Provider,
treasuryAddress: string,
tokenList: string[]
): Promise<TreasurySnapshot> {
const assets = await Promise.all(
tokenList.map(async (token) => {
const contract = new ethers.Contract(token, ERC20_ABI, provider);
const balance = await contract.balanceOf(treasuryAddress);
const price = await getTokenPrice(token); // CoinGecko API
return {
token,
balance,
usdValue: Number(ethers.formatEther(balance)) * price
};
})
);
const totalUsdValue = assets.reduce((sum, a) => sum + a.usdValue, 0);
const monthlyBurn = await getMonthlyBurnRate(); // from historical transactions
return {
timestamp: Date.now(),
assets,
totalUsdValue,
runwayMonths: totalUsdValue / monthlyBurn
};
}
KPI Dashboard
| Metric | Target | Alert |
|---|---|---|
| Runway | > 24 months | < 12 months |
| Stable ratio | > 40% | < 25% |
| Monthly burn | Known and agreed | +20% overage |
| Yield APY | > 4% on stables | < 2% |
| Token concentration | < 40% in native | > 60% |
Governance Attack Protection
Proposal threshold: creating spending proposal should require significant stake (1%+ supply). Otherwise small attacker can attempt malicious proposal.
Timelock: mandatory delay before financial operations. Minimum 48 hours; for large amounts — 7 days. Gives community time to notice and react.
Spending caps: even via governance — don't allow withdrawing more than X% treasury per proposal. Require breaking large spending into pieces.
Veto mechanism: some DAOs (Compound, Uniswap) have Security Council — small multisig with veto rights on governance decisions during timelock period. Used only for obviously malicious proposals.
// Guardian / veto mechanism
contract TreasuryGuardian {
address public immutable securityCouncil; // 4/7 multisig
TimelockController public immutable timelock;
// Cancel malicious proposal during timelock period
function vetoOperation(bytes32 operationId) external {
require(msg.sender == securityCouncil, "Not security council");
timelock.cancel(operationId);
emit OperationVetoed(operationId);
}
}
Treasury is DAO's runway. Well-managed treasury allows protocol to survive bear market and continue development independent of native token price.







