DEV Community

Cover image for Octant v2 Developer Tutorial: Building Yield-Donating Strategies
Miracle656
Miracle656

Posted on

Octant v2 Developer Tutorial: Building Yield-Donating Strategies

🎯 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!
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

Run Tests

forge test --match-contract AaveStrategyTest -vv
Enter fullscreen mode Exit fullscreen mode

*## 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Deploy

forge script script/DeployAaveStrategy.s.sol \
    --rpc-url $ETH_RPC_URL \
    --broadcast \
    --verify \
    --etherscan-api-key $ETHERSCAN_API_KEY
Enter fullscreen mode Exit fullscreen mode

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}`);
Enter fullscreen mode Exit fullscreen mode

*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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

*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
}
Enter fullscreen mode Exit fullscreen mode

❌ Mistake 2: Not Approving Tokens

// Add in constructor:
ERC20(_asset).approve(_yieldSource, type(uint256).max);
Enter fullscreen mode Exit fullscreen mode

❌ Mistake 3: Wrong Share Accounting

// Octant handles share minting automatically in report()
// DON'T manually mint shares for profit!
Enter fullscreen mode Exit fullscreen mode

Part 9: Resources & Next Steps

Documentation

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)