Everyone wants to earn interest on their crypto. The problem is, there are so many different places across many blockchains offering different rates, with various levels of risk. What if you could have an on-chain, automated investment manager that could optimize your yield for you? Let’s learn how to create this kind of system using the native AI and chain abstraction capabilities of Ritual.

In this tutorial, we will:

  1. Read yield data from multiple EVM chains via Wormhole queries
  2. Execute a Classical ML model to optimize yield
  3. Schedule periodic position rebalancing via scheduled transactions

1

Initial assumptions

For sake of example, assume that:

  1. For each of our yield sources, we have an on-chain read function that returns data including APY, liquidity, and 24H volume.
  2. We can black-box a function for updating our cross-chain yield positions, also executed via Wormhole queries
  3. We have already created a Classical ML model that consumes on-chain yield metrics and returns an allocation as output
2

Execute initial setup

First, we can setup our smart contract by inheriting the IScheduler interface:

// Setup interface
IScheduler public scheduler;

// Initialize interface
constructor(..., address schedulerAddress) {
    // ...
    scheduler = IScheduler(schedulerAddress);
    // ...
}

We can also upload our Classical ML model to Hugging Face or Arweave via our infernet-ml toolkit.

3

Read and update yield allocations

Next, we can:

  1. Tap into our multi-chain yield sources to collect yield data
  2. Process the yield data by executing our Classical ML model
  3. Update our position allocations

We can assume a sample interface for yield data as follows:

struct YieldSourceInfo {
    uint16 chainId, // chain ID of the chain this yield source resides on
    string blockTag, // block to query at
    address to, // address of contract for this yield source's data
    bytes data // encoded bytes data of the read function you are calling
}

Using this interface, we can proceed to build a function that chains together the Wormhole and Classical ML inference precompiles to run our desired pipeline:

import "./RitualLib.sol";
// ...

public YieldSourceInfo yieldSources;
public uint128[] allocation;

function updateYieldAllocation() public {
    bytes[] memory yieldSourcesMetrics;

    // Collect data via Wormhole cross-chain reads
    for (uint i = 0; i < yieldSources.length; i++) {
        YieldSourceInfo yieldSource = yieldSources[i];
        (bool whSuccess, bytes memory whOutput) =
        RitualLib.requestWormholeQuery(
                1,
            0,
            yieldSource.chainId,
            yieldSource.blockTag,
            yieldSource.to,
            yieldSource.data
        );
        require(whSuccess, "wormhole precompile call failed");
        yieldSourcesMetrics.push(abi.decode(whOutput, [bytes]);
    }

    // Prepare data for Classical ML inference
    (RitualLib.DataType dataType, uint16[] memory shape, uint128 values)
    = prepareMetricsForONNX(yieldSourcesMetrics);

    RitualLib.ModelId memory modelId = RitualLib.ModelId({
        storageId: 2,
        owner: "model_owner",
        name: "yield_allocation_model",
        version: "1.0.0",
        files: "yield_allocation_model.onnx"
    });

    // Execute ONNX precompile call
    (bool success,bytes memory result) = RitualLib.requestOnnx(
        modelId,
        dataType,
        shape,
        values
    );
    require(success, "ONNX precompile call failed");
    (uint8 dtype, uint16[] memory shape, int128[] memory newAllocation) =
    abi.decode(output, (uint8, uint16[], int128[]));

    // Update yield positions according to the new allocation returned by
    // Classical ML inference operation
    updateAllocation(newAllocation);
}

As you can see above, we:

  1. Iterate through our yield sources, collecting their yield data
  2. Parse and package this data for execution by an ONNX model
  3. Use the returned array from the ONNX model to update our allocations
4

Setup rebalancing automatically

Finally, with the updateYieldAllocation() function setup, we can easily schedule our position allocations to rebalance automatically through the use of scheduled transactions:

function scheduleYieldAllocationUpdate(
    uint32 gasLimit,
    uint48 gasPrice,
    uint32 frequency,
    uint32 numBlocks
) public payable {
    uint256 amount = uint256(gasLimit) * uint256(gasPrice) * uint256(numBlocks / frequency);

    require(msg.value >= amount, "Insufficient funds");

    uint32 deadline = uint32(block.number + numBlocks);

    scheduler.schedule{value: msg.value}(
        // Notice we pass in the `updateYieldAllocation()` fn signature
        bytes4(abi.encodeCall(this.updateYieldAllocation, ())),
        gasLimit, // max gas limit
        gasPrice, // max gas price
        deadline, // max block number
        frequency // frequency
    );
}