Allowances
An allowance is a quota that permissions consume. Use allowances for spending limits and rate limits. An allowance can refill on a schedule.
In the Zodiac App , you set spending limits directly on a policy, with intervals such as daily, weekly, and monthly. The app manages allowances for Roles Modifiers that a Zodiac account owns. This page explains the record behind those limits.
The allowance record
The contract stores each allowance under a bytes32 key:
struct Allowance {
uint128 refill; // amount added per period
uint128 maxRefill; // balance ceiling for refills
uint64 period; // refill interval in seconds; 0 = one-time allowance
uint128 balance; // unused amount, available now
uint64 timestamp; // start of the last refilled interval
}The owner sets an allowance with:
setAllowance(bytes32 key, uint128 balance, uint128 maxRefill, uint128 refill, uint64 period, uint64 timestamp)Two inputs have defaults: maxRefill = 0 becomes “no ceiling”, and timestamp = 0 becomes
the current block time.
How refills work
The balance does not grow per block. It grows in steps, one step per full period:
- When a member uses the allowance, the contract counts the full periods since
timestamp. - It adds
refillfor each full period, up tomaxRefill. - It moves
timestampforward to the start of the current period. - It subtracts the consumed amount from
balance.
The consumption is final only when the whole transaction succeeds. A reverted transaction consumes nothing.
Worked example: 10,000 USDC per day
USDC has 6 decimals. The values for “10,000 USDC each day, no roll-over” are:
| Field | Value | Reason |
|---|---|---|
balance | 10_000_000_000 | The member can spend today. |
refill | 10_000_000_000 | Each day adds 10,000 USDC. |
maxRefill | 10_000_000_000 | The balance never exceeds one day of budget. Unused budget does not accumulate. |
period | 86_400 | One day, in seconds. |
timestamp | 0 | Start now. |
Set maxRefill to a multiple of refill to let unused budget accumulate up to that
ceiling. Set period = 0 for a one-time budget that never refills.
The three allowance operators
| Operator | What it meters |
|---|---|
WithinAllowance | An integer parameter, for example a transfer amount. |
EtherWithinAllowance | The Ether value of the call. |
CallWithinAllowance | The number of calls. Each call consumes 1. |
Several permissions can share one allowance key. They then draw from the same budget. This also works across targets: one “stablecoin budget” can meter USDC and DAI transfers together — but note that the raw integer amounts must be comparable.
Define allowances with the SDK
Declare the allowance and reference it from a permission. In a template project, c is
a global, so nothing needs an import:
const USDC_DECIMALS = 6n
const DAILY = 10_000n * 10n ** USDC_DECIMALS
export const usdc_payouts = {
key: 'usdc_payouts',
refill: DAILY,
maxRefill: DAILY,
period: 60n * 60n * 24n,
balance: DAILY,
timestamp: 0n,
}The key is a plain label, the way a role key is, and is encoded to the contract’s
bytes32 when the constellation is deployed. It has to fit in 32 bytes. In the record
form of allowances, the record key is the identity — it decides what a partial update
replaces and what null deletes — so key restates it and can be omitted.
The transfer() action meters itself with the allowance
you hand it:
transfer({
label: 'Grant payouts',
tokens: [USDC],
to: [payee],
allowance: usdc_payouts,
})A hand-written permission goes to the chain exactly as authored, so a condition that references an allowance needs the encoded key:
import { encodeKey } from '@zodiaceco/sdk'
allow.eth.usdc.transfer(
undefined,
c.withinAllowance(encodeKey(usdc_payouts.key)),
)encodeKey returns an already-encoded key unchanged, so wrapping is always
safe — and declaring key: encodeKey('usdc_payouts') stays valid if you
prefer the key usable in conditions without wrapping.
Register the allowance on the Roles Modifier node of your constellation:
export const backendRoles = eth.roles['Backend Operator']({
// ...
allowances: { usdc_payouts },
})An allowance limits amounts, not destinations. Combine WithinAllowance with
a condition on the recipient, or the member can send the full budget to any
address.
Allowances and DeFi Kit presets
A DeFi Kit preset writes its own conditions, and most presets leave the amount parameters open. You cannot attach an allowance to a preset afterwards: a second permission for the same function widens access instead of tightening it — see One function, several conditions.
To meter an amount, author that function permission yourself with the allow kit, with
WithinAllowance on the amount parameter. Do not put a preset that allows the same
function openly into the same role: the open condition wins.