Matador Docs
Tutorials

Advanced Policy Authoring

Build a complex, multi-branched security policy for a Yield Farming Bot.

In this tutorial, we will construct a production-grade policy for an automated Yield Farming Bot.

The Scenario

We have a bot that manages a delta-neutral position on Aave. It needs permission to:

  1. Rebalance: Adjust the borrow/supply ratio to maintain a target Health Factor.
  2. Harvest: Claim rewards (e.g., AAVE tokens) and swap them to USDC.
  3. Emergency: Unwind the position if the market crashes.

Constraints:

  • The bot cannot withdraw collateral to its own wallet (only to the Smart Account).
  • Swaps must use the official Uniswap Router.
  • Flash loans are strictly forbidden.

Implementation

Imports & Parameters

We start by importing the necessary ABIs and defining our runtime parameters.

import "abis/AavePool.json" as Aave;
import "abis/UniswapRouter.json" as Uniswap;

permission YieldManager -> 1.0.0 {
    parameters: {
        pool: Aave,
        router: address,
        account: address,
        borrowAmount: uint256,
        minAmountOut: uint256,
        minHealthFactor: uint256
    }
    // ...

Branching Logic

The bot performs three distinct types of actions. Use explicit control flow so each allowed selector is visible to reviewers.

    fn main() -> bool {
        // Path 1: Rebalance
        // Path 2: Harvest & Swap
        // Path 3: Emergency Unwind
        return false;
    }
}

Path 1: Rebalance

Allow supplying collateral, borrowing, or repaying assets, but require the evaluated Aave health factor to stay above the configured floor.

if (context.target == parameters.pool) {
    if (context.selector == Aave.supply) {
        if (Aave.supply.onBehalfOf != parameters.account) {
            return false;
        }
    } else if (context.selector == Aave.borrow) {
        if (Aave.borrow.onBehalfOf != parameters.account) {
            return false;
        }

        if (Aave.borrow.amount != parameters.borrowAmount) {
            return false;
        }
    } else if (context.selector == Aave.repay) {
        if (Aave.repay.onBehalfOf != parameters.account) {
            return false;
        }
    } else {
        return false;
    }

    let healthFactor = parameters.pool.getUserAccountData(parameters.account).healthFactor;
    return healthFactor >= parameters.minHealthFactor;
}

Path 2: Harvest & Swap

Allow claiming rewards and swapping them to stablecoins.

if (context.target == parameters.router) {
    if (context.selector == Uniswap.exactInputSingle) {
        if (Uniswap.exactInputSingle.params.recipient != parameters.account) {
            return false;
        }

        return Uniswap.exactInputSingle.params.amountOutMinimum >= parameters.minAmountOut;
    }
}

Path 3: Emergency Unwind

Permit controlled withdrawals only to the managed account. The complete policy below still applies the same health-factor floor after matching the allowed Aave action.

if (context.target == parameters.pool) {
    if (context.selector == Aave.withdraw) {
        return Aave.withdraw.to == parameters.account;
    }
}

Complete Policy

import "abis/AavePool.json" as Aave;
import "abis/UniswapRouter.json" as Uniswap;

permission YieldManager -> 1.0.0 {
    parameters: {
        authorizer: address,
        pool: Aave,
        router: address,
        account: address,
        borrowAmount: uint256,
        minAmountOut: uint256,
        minHealthFactor: uint256
    }

    pub fn authorize() -> bool {
        return authorizedBy(parameters.authorizer);
    }

    fn main() -> bool {
        if (context.target == parameters.pool) {
            if (context.selector == Aave.supply) {
                if (Aave.supply.onBehalfOf != parameters.account) {
                    return false;
                }
            } else if (context.selector == Aave.borrow) {
                if (Aave.borrow.onBehalfOf != parameters.account) {
                    return false;
                }

                if (Aave.borrow.amount != parameters.borrowAmount) {
                    return false;
                }
            } else if (context.selector == Aave.repay) {
                if (Aave.repay.onBehalfOf != parameters.account) {
                    return false;
                }
            } else if (context.selector == Aave.withdraw) {
                if (Aave.withdraw.to != parameters.account) {
                    return false;
                }
            } else {
                return false;
            }

            let healthFactor = parameters.pool.getUserAccountData(parameters.account).healthFactor;
            return healthFactor >= parameters.minHealthFactor;
        }

        if (context.target == parameters.router) {
            if (context.selector == Uniswap.exactInputSingle) {
                if (Uniswap.exactInputSingle.params.recipient != parameters.account) {
                    return false;
                }

                return Uniswap.exactInputSingle.params.amountOutMinimum >= parameters.minAmountOut;
            }
        }

        return false;
    }
}

Optimization Tip

Place cheap selector and target checks before external reads. The interpreter short-circuits on explicit return false paths, saving gas on rejected calls.

On this page