Skip to main content

Strategy Development Tutorial

This R&D prototype is presented for informational purposes only. It is not a product or service offering and should not be considered financial or legal advice.

The following outlinesSavingsUsdcStrategya rewards-donating strategy that deploys USDC into a Spark savings vault, earns from the Sky Savings Rate, and directs all rewards to a configurable external address. Shares in our vault are always redeemable 1:1 for their underlying USDC.

The Modular Architecture

Before diving into the strategy code, a quick reminder about the Octant v2 vault architecture: through an inheritance chain, we get sophisticated vault functionality "for free".

SavingsUsdcStrategy ← Our contract that users interact with
↓ inherits from
BaseHealthCheck ← Adds configurable safety bounds checking
↓ inherits from
BaseStrategy ← Delegates to YieldDonatingTokenizedStrategy for vault functionality
↓ uses
YieldDonatingTokenizedStrategy ← Mints new shares to Donation Address
↓ inherits from
TokenizedStrategy ← Contains all ERC-4626 implementation

Think of this like building with Lego blocks.

  • TokenizedStrategy is a sophisticated base that handles all the complex vault operations—share accounting, deposits, withdrawals, ERC-4626 compliance, access control, and emergency functions.
  • YieldDonatingTokenizedStrategy is a specialized implementation of TokenizedStrategy that mints new shares to the donation address based on all the rewards calculated on report()
  • BaseStrategy adds the delegation pattern that lets us customize reward generation logic.
  • BaseHealthCheck adds configurable safety rails. It allows us to set upper and lower bounds on reward harvests. While losses could indicate a problem with our strategy, large unexpected gains might signal oracle manipulation, MEV attacks, or other unforeseen circumstances. If these bounds are triggered, management can manually intervene.
  • We just plug in our rewards source integration, SavingsUsdcStrategy, on top.

This inheritance structure means we only need to implement four functions to get a fully functional strategy that can handle millions in deposits, automatically mint/burn shares, and reliably distribute rewards.

Setting Up Our Strategy

Let's see how this modular approach works in practice, starting with setup. Our constructor defines the key roles and ensures we're working with the right assets:

constructor(
address _asset,
string memory _name,
address _management,
address _keeper,
address _emergencyAdmin,
address _donationAddress,
address _tokenizedStrategyAddress
) BaseHealthCheck(/* parameters */) {
if (_asset != USDC) {
revert UnsupportedAsset(_asset);
}

IERC20(_asset).approve(USDC_VAULT, type(uint256).max);
}

The role assignments follow Octant v2's standardized pattern for operational security:

  • Management is the primary administrator who can change other roles, intervene when safety bounds are hit, and redirect reward destinations
  • Keeper calls the report() function to harvest and distribute accumulated rewards—this role can be automated and/or outsourced to 3rd-party services like Gelato
  • Emergency Administrator can shut down the strategy and perform emergency withdrawals—perfect for delegation to 24/7 monitoring services
  • Donation Address receives newly minted shares, which can be redeemed for the underlying USDC rewards
  • Enable Burning is a boolean used in the event the strategy has a loss, based on its value the recovery mechanism will burn some of the donation address shares in order to compensate for the loss

We also give infinite ERC20 approval to Spark's trusted vault (a common gas optimization pattern) which will be needed in order to deposit the assets into the vault.

Integration in Four Functions

Spark's USDC savings vault is an ERC-4626 vault—just like Octant v2 Strategies. This means that integrating with external DeFi sources becomes almost trivial.

Our strategy needs to handle four core actions. Each one leverages the ERC-4626 standard at both ends:

1. Deploying User Funds

When users deposit USDC into our strategy, we need to put that money to work immediately:

function _deployFunds(uint256 _amount) internal override {
IERC4626(USDC_VAULT).deposit(_amount, address(this));
}

That's it—one simple line to deploy funds because both our strategy and Spark's vault speak the same ERC-4626 language. When someone deposits into our strategy, the TokenizedStrategy base contract handles all the complexity of minting shares/stablecoin, updating accounting, and tracking user balances. Our job is simply to move the USDC from our strategy into Spark's vault.

2. Freeing User Funds

When users want their money back, we reverse the process:

function _freeFunds(uint256 _amount) internal override {
IERC4626(USDC_VAULT).withdraw(_amount, address(this), address(this));
}

Again, the ERC-4626 standard makes withdrawal as simple as a deployment. The base contract handles burning user shares/stablecoin and calculating exactly how much USDC they should receive. We just pull the requested amount from Spark's vault.

3. Calculating and Distributing Rewards

This is where the magic happens—converting rewards into public goods funding:

function _harvestAndReport() internal view override returns (uint256 _totalAssets) {
uint256 shares = IERC4626(USDC_VAULT).balanceOf(address(this));
uint256 vaultAssets = IERC4626(USDC_VAULT).convertToAssets(shares);
uint256 idleAssets = IERC20(USDC).balanceOf(address(this));

_totalAssets = vaultAssets + idleAssets;
}

We don't need to manually track rewards or handle complex accounting. We simply report our current position value, and the TokenizedStrategy calculates the difference between what we have now and what we started with. Any rewards are automatically converted to new shares and donated to the specified donation address. The base contract handles all the mathematical complexity of share distribution.

4. Respecting Upstream Limits

Finally, we need to respect the Spark vault's deposit capacity:

function availableDepositLimit(address) public view override returns (uint256) {
return IERC4626(USDC_VAULT).maxDeposit(address(this));
}

This prevents failed transactions by checking the upstream vault's available capacity before users attempt deposits. We're essentially proxying Spark's limits to our users transparently.

How It All Flows Together

Let's trace through how these functions work together in practice:

When a User Deposits:

  1. User calls deposit() on our SavingsUsdcStrategy
  2. TokenizedStrategy handles all the vault logic—validating amounts, calculating shares, checking limits
  3. TokenizedStrategy calls our _deployFunds() to put the money to work in Spark's vault
  4. Shares are minted to the user automatically based on their deposit

When a User Withdraws:

  1. User calls withdraw() on our SavingsUsdcStrategy
  2. TokenizedStrategy calculates how many shares to burn and how much USDC they should receive
  3. TokenizedStrategy calls our _freeFunds() to pull the exact amount from Spark's vault
  4. User receives their USDC, and their shares are burned

When Rewards Are Reported:

  1. Keeper calls report() on our SavingsUsdcStrategy (this can be automated)
  2. TokenizedStrategy calls our _harvestAndReport() to get the current total value
  3. Rewards are calculated as the difference between the current value and the previous value
  4. New shares representing the rewards are minted and sent to the donation address
  5. BaseHealthCheck ensures rewards are within acceptable lower and upper bounds

A Million Experiments Await

This strategy demonstrates the power of Octant v2's modular design. You don't need hundreds of lines of code or deep protocol expertise to become an Octant Strategist. The inheritance chain gives you vault functionality, safety measures, and operational infrastructure so you can focus purely on your unique DeFi integration.