This contract allows users to set up an allowance for another party, ensuring the contract can only spend up to X amount per year.
This could be used to enable users to subscribe to a service while having the guarantee that they won't pay more than X per year.
In the context of the project, a year is defined as a 365-day interval. The first interval begins at the time defined by the user and then resets every 365 days forever. Leap years are not taken into account.
In each interval, the spending party can spend from the allowance multiple times, but only up to the amount defined by the user.
The spending side of the contract is defined by two roles:
- Owner: Can transfer ownership, update the operator, and update the collector address
- Operator: Can trigger the collection of user funds, as long as there is enough allowance remaining in the current interval (and the interval has started)
Note that this contract is not upgradeable by design.
Users can submit the following account multicall to set up the recurring allowance:
ERC20.approve(spender: YEARLY_ALLOWANCE_CONTRACT, amount: total_approved_amount)
YEARLY_ALLOWANCE_CONTRACT.setup_allowance(max_usage_per_interval, first_interval_start)
ERC20.transfer(to: INITIAL_PAYMENT_COLLECTOR, amount: initial_payment)Users need to give this contract enough ERC20 approval so it can use their funds.
Note that even if users approve a high amount, YEARLY_ALLOWANCE_CONTRACT caps the amount the contract can spend in each 365-day period.
Nevertheless, to be cautious, we recommend approving only enough to cover the payments for a few years.
Defines how much the contract can spend on every 365-day interval. Must be greater than 0.
- When
0: the first interval starts instantly - When a timestamp is passed: The first interval starts at the given time and no spending can occur before it begins. Timestamps must be strictly in the future (greater than the current block time).
Timestamps are Unix timestamps in seconds.
Note: calling setup_allowance always resets the current interval usage to 0, and resets the interval start timestamp to either now or the supplied first_interval_start.
The last call to transfer some funds is optional and allows the user to make a first payment instantly. Note that this contract doesn't guarantee that the destination of this transfer is the same as the collector defined in YEARLY_ALLOWANCE_CONTRACT.
This transfer happens outside of YEARLY_ALLOWANCE_CONTRACT so it will bypass any restriction imposed by the contract and won't count towards the usage tracked by it.
Users can disable the allowance by revoking the ERC20 approval and calling disable_allowance:
ERC20.approve(spender: YEARLY_ALLOWANCE_CONTRACT, amount: 0)
YEARLY_ALLOWANCE_CONTRACT.disable_allowance()The operator can trigger a collection of user funds by calling:
YEARLY_ALLOWANCE_CONTRACT.spend_from_allowance(user, amount)This will transfer tokens from the user to the collector address, as long as there is enough allowance remaining in the current interval and the first interval has started.
Notes:
- Token amounts are
u128in this contract. Even if a token usesu256amounts, the per-interval cap and individual spends cannot exceedu128::MAX.
Note: the contract requires tokens that implement transfer_from (snake_case). Tokens that only expose legacy transferFrom (camelCase) are not supported.
Two events are emitted:
-
YearlyAllowanceSetup: Emitted when a user configures or disables an allowanceuser(indexed): The user addressmax_usage_per_interval: The per-interval limit (0 when disabled)interval_start_timestamp: The interval start timestamp (0 when disabled)
-
TransferFromYearlyAllowance: Emitted when the contract transfers tokens from a user's allowance to the collectoruser(indexed): The user addresscollector(indexed): The collector address that received the tokensinterval_start_timestamp: The start timestamp of the interval this spend belongs toamount: The amount transferred
setup_allowance should not be used as an increase/decrease mechanism for an already active allowance. This method does not just update the allowance, it also prematurely ends the current allowance interval and starts a new one at first_interval_start.
A malicious owner or operator can front-run the user's transaction, spend before the current allowance interval ends, and then spend again in the newly started interval (which may start immediately). This behavior may be unexpected for users.
To reduce this risk, users should first call disable_allowance(), wait for confirmation, verify that no unexpected spend occurred, and only then call setup_allowance with the intended first_interval_start.
See DEVELOPMENT.md for setup and testing instructions.