Tokens rebalancing

The goal of this task is to rebalance a three-token portfolio to reach specific target weights (measured in basis points), using USD valuations and slippage-protected swaps.

For example, suppose the targets are:

  • 50% BTC

  • 30% ETH

  • 20% DAI

If the portfolio currently holds:

  • $6,500 in BTC (65%)

  • $2,500 in ETH (25%)

  • $1,000 in DAI (10%)

Since BTC is overweight while ETH and DAI are underweight, the task will swap $500 worth of BTC into ETH and $1,000 worth of BTC into DAI to restore the target balance.

Task

import { Address, BigInt, environment, ERC20Token, log, Swap, TokenAmount, USD } from '@mimicprotocol/lib-ts'

import { ERC20 } from './types/ERC20'
import { inputs } from './types'

const BPS_DENOMINATOR = BigInt.fromI32(10_000)

function usdMin(left: USD, right: USD): USD {
  return left.lt(right) ? left : right
}

function shareByBps(amountUSD: USD, bps: i32): USD {
  const numerator = amountUSD.times(BigInt.fromI32(bps))
  return numerator.div(BPS_DENOMINATOR)
}

function getTokenAmount(chainId: u32, tokenAddress: Address): TokenAmount {
  const me = environment.getContext().user
  const contract = new ERC20(tokenAddress, chainId)
  const balance = contract.balanceOf(me)
  const token = ERC20Token.fromAddress(tokenAddress, chainId)
  return TokenAmount.fromBigInt(token, balance)
}

class Bucket {
  constructor(
    public index: i32,
    public amountUSD: USD
  ) {}
}

export default function main(): void {
  const tokenAddresses = [inputs.tokenA, inputs.tokenB, inputs.tokenC]
  const targetBps = [inputs.targetBpsA as i32, inputs.targetBpsB as i32, inputs.targetBpsC as i32]

  const totalTargetBps = targetBps[0] + targetBps[1] + targetBps[2]
  if (totalTargetBps != 10_000) throw new Error('Targets BPS must sum to 10000')

  const tokensMetadata = [
    ERC20Token.fromAddress(tokenAddresses[0], inputs.chainId),
    ERC20Token.fromAddress(tokenAddresses[1], inputs.chainId),
    ERC20Token.fromAddress(tokenAddresses[2], inputs.chainId),
  ]

  const tokenAmounts = [
    getTokenAmount(inputs.chainId, tokenAddresses[0]),
    getTokenAmount(inputs.chainId, tokenAddresses[1]),
    getTokenAmount(inputs.chainId, tokenAddresses[2]),
  ]

  const currentBalancesUsd = [tokenAmounts[0].toUsd(), tokenAmounts[1].toUsd(), tokenAmounts[2].toUsd()]
  const totalPortfolioUSD = currentBalancesUsd[0].plus(currentBalancesUsd[1]).plus(currentBalancesUsd[2])
  if (totalPortfolioUSD.le(USD.zero())) {
    log.info('No rebalance needed (total USD is zero)')
    return
  }

  const desiredBalancesUsd = [
    shareByBps(totalPortfolioUSD, targetBps[0]),
    shareByBps(totalPortfolioUSD, targetBps[1]),
    shareByBps(totalPortfolioUSD, targetBps[2]),
  ]

  const surpluses = new Array<Bucket>()
  const deficits = new Array<Bucket>()
  for (let i: i32 = 0; i < 3; i++) {
    if (currentBalancesUsd[i].gt(desiredBalancesUsd[i])) {
      surpluses.push(new Bucket(i, currentBalancesUsd[i].minus(desiredBalancesUsd[i])))
    } else if (desiredBalancesUsd[i].gt(currentBalancesUsd[i])) {
      deficits.push(new Bucket(i, desiredBalancesUsd[i].minus(currentBalancesUsd[i])))
    }
  }

  if (surpluses.length == 0 || deficits.length == 0) {
    log.info('No rebalance needed (target ratios matched)')
    return
  }

  let surplusIndex: i32 = 0
  let deficitIndex: i32 = 0
  while (surplusIndex < surpluses.length && deficitIndex < deficits.length) {
    const movedUSD = usdMin(surpluses[surplusIndex].amountUSD, deficits[deficitIndex].amountUSD)

    const surplusTokenIndex = surpluses[surplusIndex].index
    const deficitTokenIndex = deficits[deficitIndex].index

    const amountInToken = movedUSD.toTokenAmount(tokensMetadata[surplusTokenIndex])
    const expectedOutToken = movedUSD.toTokenAmount(tokensMetadata[deficitTokenIndex])

    const slippageFactor = BPS_DENOMINATOR.minus(BigInt.fromI32(inputs.slippageBps as i32))
    const minimumOutAmount = expectedOutToken.amount.times(slippageFactor).div(BPS_DENOMINATOR)

    Swap.create(
      inputs.chainId,
      tokensMetadata[surplusTokenIndex],
      amountInToken.amount,
      tokensMetadata[deficitTokenIndex],
      minimumOutAmount
    ).send()

    surpluses[surplusIndex].amountUSD = surpluses[surplusIndex].amountUSD.minus(movedUSD)
    deficits[deficitIndex].amountUSD = deficits[deficitIndex].amountUSD.minus(movedUSD)

    if (surpluses[surplusIndex].amountUSD.le(USD.zero())) surplusIndex++
    if (deficits[deficitIndex].amountUSD.le(USD.zero())) deficitIndex++
  }

  log.info('Rebalance executed')
}

Manifest

version: 1.0.0
name: Rebalance to USD target ratios (3 tokens)
description: Automated task that rebalances a 3-token portfolio to target basis-point weights using USD valuations and slippage-protected swaps
inputs:
  - chainId: uint32
  - tokenA: address
  - tokenB: address
  - tokenC: address
  - targetBpsA: uint16      # e.g., 5000 = 50%
  - targetBpsB: uint16      # must sum with A & C to 10000
  - targetBpsC: uint16
  - slippageBps: uint16     # e.g., 50 = 0.50%
abis:
  - ERC20: ./abis/ERC20.json

Last updated