π― What You'll Learn
By the end of this tutorial, you'll understand:
- How Octant v2 transforms yield into sustainable public goods funding
- The architecture of yield-donating strategies
- How to build and deploy your own strategy
- Real-world examples with code walkthroughs
Part 1: Understanding Octant v2
The Problem Octant Solves
Traditional funding models require either:
- Depleting treasuries (unsustainable)
- Taking from users (creates friction)
Octant's Innovation: Use ONLY the yield, keep the principal forever!
The Flow
User deposits 10,000 USDC
β
Strategy invests in Aave (earning 5% APY)
β
After 1 year: 500 USDC yield generated
β
User still has 10,000 USDC (can withdraw anytime)
500 USDC goes to public goods as donation shares
Key Concept: Donation Shares
- User gets shares representing their PRINCIPAL
- Strategy mints NEW shares for YIELD
- Yield shares go to dragonRouter (donation address)
- This is like giving interest to charity while keeping your savings account balance!
Part 2: Architecture Deep Dive
Core Components
1. YieldDonatingTokenizedStrategy
The base contract you inherit from. Key functions:
// YOU IMPLEMENT THESE:
function _deployFunds(uint256 _amount) internal virtual;
function _freeFunds(uint256 _amount) internal virtual;
function _harvestAndReport() internal virtual returns (uint256);
// OCTANT HANDLES THESE:
function report() external onlyKeepers returns (uint256 profit, uint256 loss);
// β This mints profit shares to dragonRouter automatically!
2. The Report Mechanism
// When keeper calls report():
1. Call _harvestAndReport() to get current totalAssets
2. Compare with previous totalAssets
3. If profit: mint shares to dragonRouter
4. If loss (optional): burn dragonRouter shares
5. Update accounting
3. Factory Pattern
contract SkyCompounderStrategyFactory {
function createStrategy(
address _compounderVault, // Yield source
string memory _name,
address _management,
address _keeper,
address _emergencyAdmin,
address _donationAddress, // Where yield goes!
bool _enableBurning,
address _tokenizedStrategyAddress
) external returns (address strategy);
}
Part 3: Building Your First Strategy
Example: Aave v3 USDC Strategy
Let's build a strategy that deposits USDC into Aave and donates the yield!
Step 1: Setup Your Interface
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.18;
interface IAaveV3Pool {
function supply(
address asset,
uint256 amount,
address onBehalfOf,
uint16 referralCode
) external;
function withdraw(
address asset,
uint256 amount,
address to
) external returns (uint256);
}
interface IAToken {
function balanceOf(address account) external view returns (uint256);
}
Step 2: Implement Your Strategy
import {YieldDonatingTokenizedStrategy} from "./YieldDonatingTokenizedStrategy.sol";
contract AaveUsdcYieldDonating is YieldDonatingTokenizedStrategy {
IAaveV3Pool public immutable aavePool;
IAToken public immutable aUsdc;
constructor(
address _aavePool,
address _aUsdc,
address _asset,
string memory _name,
address _management,
address _keeper,
address _emergencyAdmin,
address _donationAddress,
bool _enableBurning,
address _tokenizedStrategyAddress
) YieldDonatingTokenizedStrategy(
_asset,
_name,
_management,
_keeper,
_emergencyAdmin,
_donationAddress,
_enableBurning,
_tokenizedStrategyAddress
) {
aavePool = IAaveV3Pool(_aavePool);
aUsdc = IAToken(_aUsdc);
// Approve Aave to spend our USDC
ERC20(_asset).approve(_aavePool, type(uint256).max);
}
// REQUIRED: Deploy funds into Aave
function _deployFunds(uint256 _amount) internal override {
aavePool.supply(
address(asset),
_amount,
address(this),
0 // no referral code
);
}
// REQUIRED: Withdraw funds from Aave
function _freeFunds(uint256 _amount) internal override {
aavePool.withdraw(
address(asset),
_amount,
address(this)
);
}
// REQUIRED: Calculate total assets
function _harvestAndReport() internal override returns (uint256 _totalAssets) {
// Assets in Aave (aUSDC represents our deposit + yield)
uint256 aaveBalance = aUsdc.balanceOf(address(this));
// Idle assets in strategy
uint256 idleBalance = asset.balanceOf(address(this));
// Total = deployed + idle
_totalAssets = aaveBalance + idleBalance;
// NOTE: Profit is calculated automatically!
// If _totalAssets > lastTotalAssets, profit is minted to dragonRouter
}
// OPTIONAL: Set deposit limit based on Aave capacity
function availableDepositLimit(address) public view override returns (uint256) {
// Could check Aave's supply cap here
return type(uint256).max; // unlimited for this example
}
}
Step 3: Understanding the Magic
// Initial State
deposit(10000 USDC) β user gets 10000 shares
totalAssets = 10000 USDC
// After 1 day (earned 10 USDC in Aave)
keeper.report() calls _harvestAndReport()
β returns 10010 USDC
β profit = 10010 - 10000 = 10 USDC
β mint 10 shares to dragonRouter
β user still has 10000 shares (unchanged!)
β dragonRouter now has 10 shares
// User can still withdraw their full 10000 USDC
// dragonRouter has 10 shares worth 10 USDC to donate
Part 4: Testing Your Strategy
Setup Test Environment
// test/AaveStrategy.t.sol
pragma solidity 0.8.18;
import "forge-std/Test.sol";
import {AaveUsdcYieldDonating} from "../src/AaveStrategy.sol";
contract AaveStrategyTest is Test {
AaveUsdcYieldDonating strategy;
address user = address(0x1);
address dragonRouter = address(0x2);
function setUp() public {
// Fork mainnet for real Aave interaction
vm.createSelectFork(vm.envString("ETH_RPC_URL"));
strategy = new AaveUsdcYieldDonating(
0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2, // Aave V3 Pool
0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c, // aUSDC
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // USDC
"Aave USDC YieldDonating",
address(this), // management
address(this), // keeper
address(this), // emergency admin
dragonRouter,
false, // no burning
TOKENIZED_STRATEGY_ADDRESS
);
}
function testYieldDonation() public {
// 1. User deposits
deal(USDC, user, 10000e6);
vm.startPrank(user);
IERC20(USDC).approve(address(strategy), 10000e6);
strategy.deposit(10000e6, user);
vm.stopPrank();
// 2. Simulate time passing + yield accrual
vm.warp(block.timestamp + 365 days);
// 3. Keeper reports (this mints profit to dragonRouter)
(uint256 profit, ) = strategy.report();
// 4. Verify
assertGt(profit, 0, "Should have generated profit");
assertGt(strategy.balanceOf(dragonRouter), 0, "Dragon should have shares");
assertEq(strategy.balanceOf(user), 10000e6, "User shares unchanged");
}
}
Run Tests
forge test --match-contract AaveStrategyTest -vv
*## Part 5: Deployment *
Create Deployment Script
// script/DeployAaveStrategy.s.sol
pragma solidity 0.8.18;
import "forge-std/Script.sol";
import {AaveUsdcYieldDonating} from "../src/AaveStrategy.sol";
contract DeployAaveStrategy is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
address dragonRouter = vm.envAddress("DRAGON_ROUTER");
vm.startBroadcast(deployerPrivateKey);
AaveUsdcYieldDonating strategy = new AaveUsdcYieldDonating(
0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2, // Aave V3
0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c, // aUSDC
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // USDC
"Aave USDC YieldDonating",
msg.sender,
msg.sender,
msg.sender,
dragonRouter,
false,
TOKENIZED_STRATEGY_ADDRESS
);
console.log("Strategy deployed at:", address(strategy));
vm.stopBroadcast();
}
}
Deploy
forge script script/DeployAaveStrategy.s.sol \
--rpc-url $ETH_RPC_URL \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY
Part 6: Interacting with Your Strategy
Using ethers.js
import { ethers } from 'ethers';
const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
const strategy = new ethers.Contract(STRATEGY_ADDRESS, STRATEGY_ABI, provider);
// 1. Check strategy info
const totalAssets = await strategy.totalAssets();
const dragonRouter = await strategy.dragonRouter();
const pricePerShare = await strategy.pricePerShare();
console.log(`TVL: ${ethers.utils.formatUnits(totalAssets, 6)} USDC`);
// 2. Deposit (as user)
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const strategyWithSigner = strategy.connect(signer);
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, signer);
await usdc.approve(STRATEGY_ADDRESS, ethers.utils.parseUnits("1000", 6));
await strategyWithSigner.deposit(ethers.utils.parseUnits("1000", 6), signer.address);
// 3. Report yield (as keeper)
const tx = await strategyWithSigner.report();
const receipt = await tx.wait();
console.log(`Yield reported! Gas used: ${receipt.gasUsed}`);
*Part 7: Advanced Patterns *
Multi-Protocol Strategy
Deploy across multiple yield sources:
contract MultiProtocolStrategy is YieldDonatingTokenizedStrategy {
IAaveV3Pool public aave;
ICompound public compound;
// Split deposits 50/50
function _deployFunds(uint256 _amount) internal override {
uint256 half = _amount / 2;
aave.supply(address(asset), half, address(this), 0);
compound.supply(address(asset), half);
}
function _harvestAndReport() internal override returns (uint256) {
return aave.balanceOf(address(this))
+ compound.balanceOf(address(this))
+ asset.balanceOf(address(this));
}
}
Auto-Rebalancing Strategy
Move funds to highest yield:
function _tend(uint256 _totalIdle) internal override {
uint256 aaveApy = getAaveApy();
uint256 compoundApy = getCompoundApy();
if (aaveApy > compoundApy * 101 / 100) { // 1% threshold
// Move from Compound to Aave
uint256 compoundBalance = compound.balanceOf(address(this));
compound.withdraw(compoundBalance);
aave.supply(address(asset), compoundBalance, address(this), 0);
}
}
*Part 8: Common Pitfalls *
β Mistake 1: Forgetting Idle Assets
// WRONG
function _harvestAndReport() internal override returns (uint256) {
return aave.balanceOf(address(this)); // Missing idle!
}
// CORRECT
function _harvestAndReport() internal override returns (uint256) {
return aave.balanceOf(address(this))
+ asset.balanceOf(address(this)); // Include idle
}
β Mistake 2: Not Approving Tokens
// Add in constructor:
ERC20(_asset).approve(_yieldSource, type(uint256).max);
β Mistake 3: Wrong Share Accounting
// Octant handles share minting automatically in report()
// DON'T manually mint shares for profit!
Part 9: Resources & Next Steps
Documentation
- Octant Docs: https://docs.v2.octant.build
- Yearn v3 (similar architecture): https://docs.yearn.fi/developers/v3/overview
Example Strategies
- Sky Compounder: See SkyCompounderStrategy.sol
- Lido Strategy: See LidoStrategy.sol
Community
Discord: [https://discord.gg/octant]
GitHub: https://github.com/golemfoundation/octant-v2-core
Challenge Yourself
Try building strategies for:
- Morpho Blue
- Spark Protocol
- Compound v3
- Uniswap v4 hooks
Summary Checklist
β
Understand yield donation concept
β
Know the 3 required functions to implement
β
Can test strategies with Foundry
β
Can deploy strategies to mainnet
β
Understand profit minting mechanism
β
Know common pitfalls to avoid
*You're now ready to build production-grade yield-donating strategies for Octant v2!
*
Top comments (0)