Skip to content
Thyme Docs

Treasury Sweep

Collects what accumulates. On each run the task reads the Safe's balance of a token. If the balance exceeds keep by at least minSweep, it returns one transfer of the excess to treasury. Otherwise it skips.

Typical uses are collecting protocol or integrator fees into a main treasury, or draining a collection address once it holds enough to be worth the gas.

Task

functions/treasury-sweep/index.ts
import { defineTask, z } from '@thyme-labs/sdk'
import { encodeFunctionData, erc20Abi, formatUnits, parseUnits } from 'viem'
 
const decimalString = z.string().regex(/^\d+(\.\d+)?$/)
 
export default defineTask({
  schema: z.object({
    token: z.address(),
    treasury: z.address(),
    keep: decimalString,
    minSweep: decimalString,
  }),
  async run(ctx) {
    const { token, treasury } = ctx.args
 
    const decimals = await ctx.client.readContract({
      address: token,
      abi: erc20Abi,
      functionName: 'decimals',
    })
    const balance = await ctx.client.readContract({
      address: token,
      abi: erc20Abi,
      functionName: 'balanceOf',
      args: [ctx.account],
    })
 
    const keep = parseUnits(ctx.args.keep, decimals)
    const minSweep = parseUnits(ctx.args.minSweep, decimals)
    const excess = balance > keep ? balance - keep : 0n
    ctx.logger.info(`Balance ${formatUnits(balance, decimals)}, excess ${formatUnits(excess, decimals)}`)
 
    if (excess === 0n || excess < minSweep) {
      return { canExec: false, message: 'Nothing worth sweeping' }
    }
 
    return {
      canExec: true,
      calls: [
        {
          to: token,
          data: encodeFunctionData({
            abi: erc20Abi,
            functionName: 'transfer',
            args: [treasury, excess],
          }),
        },
      ],
    }
  },
})

keep is the balance that stays in the Safe, and minSweep stops the task from paying gas to move dust. Both are decimal strings in token units. To sweep several tokens, create one executable per token.

Arguments

functions/treasury-sweep/args.json
{
  "token": "0x...",
  "treasury": "0x...",
  "keep": "0",
  "minSweep": "50"
}

Permissions

functions/treasury-sweep/permissions.json
{
  "calls": [
    { "target": { "arg": "token" }, "function": "transfer(address,uint256)" }
  ]
}

Your Safe profile must allow the same token and transfer(address,uint256). A role allowed to call transfer can send any amount to any address, including one other than treasury. Point treasury at an address you control, and keep only what you intend to sweep in the Safe. See what the role can and cannot limit.

Schedule

A daily cron at 06:00 UTC:

{ "type": "cron", "cronspec": "0 6 * * *" }