Budget
Last updated
Last updated
Budget is installed to a Safe as a module and allows the execution of payments bypassing multisig transactions. Authorized entities can directly perform payments using the Safe’s funds if they are within the spending allowance of a specific budget.
Budget uses natural dates (e.g. an allowance can restart the first day of the month) instead of timestamps (e.g. an allowance restarts every 30 * 24 * 3600 seconds) for UX purposes as most business dates revolve around schelling points that are unequal amounts of seconds apart (months, quarters, years).
Budget allows spenders of an allowance (which can only be granted by the Safe itself, more on this below) to create an arbitrary number of sub-allowances from which another set of spenders can spend (and recursively create other sub-allowances as well).
It can be used with a role-based permissioning system (external to this module, see Roles) which allows to authorize groups within the company to spend from a particular allowance (e.g. executives role or operations team role). This enables an easier onboarding/offboarding process (i.e. granting someone one role immediately allows them to spend from all the allowances that the role is the authorized spender).
Budget needs to be set as a module in a Safe in order to function since otherwise it won’t be able to execute transactions through it.
The allowance is the main data structure/primitive that Budget is built around. They are created with this function:
Allowances can have any number of sub-allowances, effectively forming a tree of allowances. Top-level allowances are allowances that aren’t a sub-allowance of any other allowance. Only the Safe itself can create top-level allowances; as they aren’t bound by the controls of a parent allowance, they could potentially spend all the Safe’s assets if the allowance parameters allowed so.
The parameters that an allowance has are:
Parent allowance ID: which allowance this allowance belongs to. This will be zero in case of a top-level allowance. It can’t be modified after creation.
Spender address: address of the account or flag for the role that is authorized to trigger payments from the allowance.
Token: address of the token contract or flag for the native asset that this allowance will spend. It can’t be modified after creation and can only be decided for top-level allowances as all sub-allowances below it must use the same token.
Amount: amount of token in its smallest unit (e.g. wei amount for ETH) that can be spent from this allowance per period.
Recurrency: cadence with which the spent amount in the allowance is reset to zero. It can’t be modified after creation.
Name: human readable name for the allowance being created.
The recurrency of an allowance is defined with a data structure we call TimeShift
. A TimeShift
is comprised of the following two fields:
Time unit: specifies when the amount spent in the allowance will be reset to zero, allowing to spend the full amount of the allowance again. Options:
Daily: resets every day at midnight UTC.
Weekly: resets every Monday at midnight UTC (Monday is considered the first day of the week, and can be modified with an offset).
Monthly: resets the first day of every month at midnight UTC.
Quarterly: resets the first day of every quarter (Jan 1st, Apr 1st, Jul 1st, Oct 1st) at midnight UTC.
Semiyearly: resets on Jan 1st and Jul 1st at midnight UTC.
Yearly: resets on Jan 1st at midnight UTC.
Non-time units:
Non-recurrent: never resets, uses offset value as the date until which the allowance will become inactive. Useful to create time-bounced one-time allowances.
Inherit: flags that the allowance has the same recurrency as its parent (see more on Parameter inheritance)
Offset: delta in seconds to UTC to offset time calculations. This is useful for both handling timezones (e.g. daily allowance which resets at midnight in UTC+2 would have an offset of +2 * 60 * 60
) and exact moments in time (e.g. an allowance that resets the last day of the month would have an offset of -24 * 60 * 60
)
The recurrency argument of Budget.createAllowance(...)
takes a EncodedTimeShift
value which is a custom type over a bytes6
value. The first byte is the encoded time unit and the next 5 are a int40
for the offset. You can see more about the encoding/decoding here.
In order to create a sub-allowance, an account must be allowed to make payments from that particular allowance (it’s address is the spender or has the concrete role). Sub-allowances are created to bucket spending within a larger allowance and grant spending permissions to another set of accounts. The creator of a sub-allowance has full freedom to set any parameters for it (except for the token address which must be the same as its parent’s), but it will always be bound by the limits of its parent.
When spending from a sub-allowance, the amount of the payment is credited not only its own authorized amount, but from the authorized amount of its parent and all ancestors in the chain until getting to the top-level allowance.
Some peculiarities of sub-allowances:
Their recurrency can be different from the parent’s in both directions. It is possible to have a monthly sub-allowance under both a weekly or yearly parent allowance.
The amount of a sub-allowance can be greater than the parent, but since spending controls are applied recursively, a sub-allowance will never be able to spend more than any of its ancestors allow
Since allowances can be paused or disabled temporarily, pausing an allowance will effectively disable spending from its descendants. All ancestor allowances in the chain to its top-level allowances must be enabled for a sub-allowance to be able to spend.
Keeping the allowance primitive simple came with a few short-comings (e.g. a single spender, not modifiable recurrency). These are tolerable because most goals can be achieved with sub-allowances (e.g. adding a spender can be done creating a sub-allowance for the same amount).
Because of how Budget tracks spending at the level of each individual allowance, the use of a deep tree of sub-allowances could become expensive gas wise. This is why we allow for inheriting certain parameters from a parent allowance, which stops the sub-allowance from tracking them itself and just relying on them being checked up the chain (multiple levels of recurrency are allowed).
Inheriting recurrency: a sub-allowance can inherit the recurrency of its parent, reseting its spent amount whenever its parent amount is reset. This can be used to create buckets of spending which are tracked using the same time unit (e.g. a yearly general budget which has sub-budgets for different departments).
Inheriting amount: a sub-allowance which inherits the recurrency of its parent can also be set to inherit its amount. This effectively means that the sub-allowance doesn’t keep track of how much it has been spent through it and just uses its parent spent amount. This is useful to authorize additional accounts to spend from a particular allowance while still keeping control over it.
The authorized spender for an allowance can use Budget:executePayment(uint256 allowanceId, address to, uint256 amount, string description)
or Budget:executeMultiPayment(uint256 allowanceId, address[] tos, uint256[] amounts, string description)
to trigger a payment from the Safe to the specified address of an amount if and only if it is within the spending limit.
When performing a payment, the contract will check whether the allowance (and all its ancestors) need to have their spent amount reset according to their recurrency and will calculate when it will reset next.
It is possible to use Budget:debitAllowance(uint256 allowanceId, uint256 amount, bytes description)
to return a certain amount of tokens (e.g. some funds are returned from a payment) and remove that amount from the spent amount for the period.
Any account can debit a payment to a particular allowance if those tokens can be successfully deposited from the account triggering the debit to the Safe.
Debiting payments only has an effect towards the current period and the spent amount for the allowance can never go negative (meaning at no time it is possible to spend from an allowance more than its amount)
Any spender of the parent allowance (or the Safe itself in the case of top-level allowances) is considered an admin to all its sub-allowances and as such, can modify certain parameters:
Amount (Budget:setAllowanceAmount(uint256 allowanceId, uint256 amount)
): changes the amount of token that can be spent per period. It applies immediately to the current period.
State (Budget:setAllowanceState(uint256 allowanceId, bool isEnabled)
): enable or disable the allowance (allowances are enabled by default on creation)
Spender address (Budget.setAllowanceSpender(uint256 allowanceId, address spender)
): changes who can spend from the allowance.
Name (Budget:setAllowanceName(uint256 allowanceId, string name)
): changes the name of the allowance.
Building on top of the allowance primitive, Budget can be extended with other smart contract modules to do further programmatic spending. Budget modules are simply smart contracts that get set as the spender of an allowance.
There’s a base BudgetModule contract that Budget modules can derive from which provides some common utilities for building modules.
Even though there’s nothing forcing this, the way we envision Budget modules built is so that there’s a single instance per Firm organization that can support using the module from different allowances (with access control deferred to the admins of the particular allowance). This allows minimizing the amount of contracts to be deployed while at the same time keeping agency over upgrades to the organization itself.
LlamaPayStreams module is a module we have built which allows to manage and fund LlamaPay v1 from a Budget allowance.
LlamaPayStreams module allows the admins of an allowance to configure a set of LlamaPay streams from the allowance and set up streamed payments right from Budget. LlamaPayStreams module automatically deposits/withdraws from LlamaPay to fund all the active streams (based on a configurable prepay buffer time)
Due to how LlamaPay v1 works, intermediate forwarder contracts are used to manage deposits in LlamaPay separately for each allowance. These are the contracts that appear as payers on LlamaPay.
As evident from the above, an infinite chain of sub-allowances can be created. When spending from an allowance at depth N
, there’s an unbounded recursive function (_checkAndUpdateAllowanceChain
) which checks and modifies storage for all levels in the allowance chain until it reaches its top-level allowance.
For a Solidity program, this is certainly a fat red flag as things like this can lead to gas griefing attacks or locking the contract making the gas cost to operate something go over the limit. However, there are a few reasons why due to the constraints of this system, this is a non-issue:
Anti-griefing: even though any spender of an allowance can create sub-allowances with custom parameters (and therefore being able to create an ‘infinite’ chain below any allowance in which they are a spender), this doesn’t impact the gas required to interact with its sibling or ancestor sub-allowances.
Contract locking due to forcing gas limit: similarly to the one above, even though it is possible to end up creating a sub-allowance at such depth that it is impossible to execute or debit payments to it, the issue will be localized to those specific allowances the bad actor is creating.
Infinite loops: as the parent of a sub-allowance has to exist when it is created and it cannot be changed, it is impossible to form a loop in which a sub-allowance can have itself in its ancestry.
The way multi-payments are executed is by triggering a module transaction in the Safe that will make it delegatecall
back into the implementation contract at Budget:__safeContext_performMultiTransfer
. When this delegatecall is received, since we are running in the Safe’s context, we can perform the ERC20 transfers directly. This results in considerable gas savings when performing a multi-payment compared to doing multiple individual safe module transactions.
Budget:__safeContext_performMultiTransfer
is an external function since it needs to be accessible via a delegatecall
. Using the onlyForeignContext
modifier we make sure that the call isn’t being executed in the context of a Budget proxy contract nor on its implementation base contract. Since the EIP-1967 upgradeability slot has a value on both instances for Firm contract, but the Safe doesn’t store anything there, we can safely assume that if the value at that slot is zero, we are not running in our context and it might be a Safe.
This is the most gas-efficient way we could think of doing this, however it has two low probability/impact issues:
Another delagatecall
transaction in the Safe could write to that slot and we would no longer identify the Safe as a foreign context making multi-payment break for that Budget instance. Since only Safe signers can trigger such a transaction, it would be a self-grief and they could just remove the Budget module from the Safe if they wished for the module to stop working.
Future Safe versions might start using that slot preventing this feature from working on Safes with that newer version. It would require an upgrade on our end for Budget to be fully compatible with it.