AllowanceManager
Inherits: EIP712x, IAllowanceManager, ReentrancyGuard
Author: Ultrasound Labs
Manages time-boxed token allowances that can be pulled by subaccounts.
*Each allowance is namespaced by (owner, subaccount, token) and protected from cross-chain replay via the EIP-712x domain separator (chainId hard-fixed to 1). The manager supports two transfer back-ends:
- A freshly deployed
AllowanceHolder(preferred – no approvals necessary after the initial permit) - Fallback to a direct ERC-20 allowance or Permit2 allowance given to this contract*
Note: security-contact: security@ultrasoundlabs.org
State Variables
_ALLOWANCE_REQUEST_BATCH_TYPEHASH
Private constant used internally for EIP-712 struct hashing.
bytes32 private constant _ALLOWANCE_REQUEST_BATCH_TYPEHASH = keccak256(
"AllowanceRequestBatch(AllowanceRequest[] requests,uint256[] chainIds)AllowanceRequest(address subaccount,address token,uint256 amount,uint256 timeframe,uint256 nonce)"
);
_ALLOWANCE_REQUEST_TYPEHASH
Private constant used internally for EIP-712 struct hashing.
bytes32 private constant _ALLOWANCE_REQUEST_TYPEHASH =
keccak256("AllowanceRequest(address subaccount,address token,uint256 amount,uint256 timeframe,uint256 nonce)");
_PERMIT2
Address of the Permit2 contract used as a back-up transfer mechanism.
address private immutable _PERMIT2;
_factory
Address of the canonical SubaccountFactory allowed to pre-commit holder addresses (set at deploy time or lazily on the first commit if zero).
address private _factory;
_allowances
mapping(address => mapping(address => mapping(address => Allowance))) internal _allowances;
allowanceNonces
Per-(owner,subaccount,token) nonce incremented every time an allowance is (re-)set.
mapping(address => mapping(address => mapping(address => uint256))) public allowanceNonces;
_holderFor
mapping(address => mapping(address => address)) private _holderFor;
_committedHolder
mapping(address => mapping(address => address)) private _committedHolder;
_allowedHolder
Addresses pre-authorised by the canonical factory to call bootstrapAllowance during their constructor. The factory MUST set the flag before deploying the holder via CREATE2 so that the constructor call can pass the check below.
mapping(address => bool) private _allowedHolder;
Functions
constructor
Initializes the AllowanceManager with the Permit2 contract address and the SubaccountFactory.
constructor(address _permit2, address factoryAddr);
Parameters
| Name | Type | Description |
|---|---|---|
_permit2 | address | Address of the Permit2 contract. |
factoryAddr | address | Canonical SubaccountFactory that may call commitHolder. |
bootstrapAllowance
Bootstrap an allowance once immediately after AllowanceHolder deployment.
Can only be called by the canonical AllowanceHolder for (owner, token) and only when nonce == 0. After a successful call, allowanceNonces[owner][subaccount][token] will be incremented to 1 so that any subsequent changes require a signed setAllowances().
function bootstrapAllowance(address owner, address subaccount, address token, uint256 amount, uint256 timeframe)
external;
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the allowance (signer of the permit given to the holder). |
subaccount | address | Subaccount that the allowance applies to. |
token | address | ERC-20 token address. |
amount | uint256 | Allowance amount. |
timeframe | uint256 | Time window in seconds for the allowance resets. |
commitHolder
Commits the expected AllowanceHolder address for a given (owner, token) pair.
Must be called once before the first bootstrapAllowance. Only allowed while no canonical
holder has been observed yet.
function commitHolder(address owner, address token, address holder) external;
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The token owner. |
token | address | The ERC-20 token managed by the holder. |
holder | address | The deterministic address where the AllowanceHolder will be deployed. |
registerHolder
Registers an additional holder for an (owner, token) pair after the canonical one has already been observed. Must be called by the canonical factory before deploying the new holder so that its constructor can call bootstrapAllowance successfully.
function registerHolder(address holder) external;
Parameters
| Name | Type | Description |
|---|---|---|
holder | address | The deterministic address where the new AllowanceHolder will be deployed. |
holderAddress
Deterministically computes the canonical AllowanceHolder address for a pair (owner, token).
function holderAddress(address owner, address token) external view returns (address);
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The token owner. |
token | address | The ERC-20 token. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | address | holder The predicted holder address (or address(0) if unknown). |
factory
Returns the canonical factory address permitted to commit holders.
function factory() external view returns (address factoryAddress);
Returns
| Name | Type | Description |
|---|---|---|
factoryAddress | address | The address of the canonical SubaccountFactory. |
permit2
The Permit2 contract address
function permit2() external view returns (address);
Returns
| Name | Type | Description |
|---|---|---|
<none> | address | permit2Addr The Permit2 contract address. |
allowances
Mapping to track allowances for subaccounts.
function allowances(address owner, address subaccount, address token) external view returns (Allowance memory);
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the subaccount. |
subaccount | address | The subaccount that the allowance is set for. |
token | address | The token that the allowance is set for. |
Returns
| Name | Type | Description |
|---|---|---|
<none> | Allowance | allowance The allowance struct. |
allowanceRequestBatchTypehash
EIP-712(x) typehash for the AllowanceRequestBatch struct (per-subaccount)
function allowanceRequestBatchTypehash() external pure returns (bytes32);
Returns
| Name | Type | Description |
|---|---|---|
<none> | bytes32 | typeHash The struct type-hash. |
allowanceRequestTypehash
EIP-712(x) typehash for the AllowanceRequest struct (per-subaccount)
function allowanceRequestTypehash() external pure returns (bytes32);
Returns
| Name | Type | Description |
|---|---|---|
<none> | bytes32 | typeHash The struct type-hash. |
setAllowances
Sets allowances for a batch of subaccounts
function setAllowances(address owner, AllowanceRequestBatch calldata requests, bytes calldata signature) public;
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the subaccounts |
requests | AllowanceRequestBatch | The AllowanceRequestBatch struct containing the requests |
signature | bytes | The EIP-712x (EIP-712 with chainId = 1) signature of the owner |
spendAllowance
Spends an allowance for the caller
function spendAllowance(address owner, address token, uint256 amount) public nonReentrant;
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the subaccount |
token | address | The token to spend the allowance for |
amount | uint256 | The amount to spend |
hashAllowanceRequestBatch
Computes the EIP-712 struct hash for a batch of allowance requests.
function hashAllowanceRequestBatch(AllowanceRequestBatch calldata batch) public pure returns (bytes32 structHash);
Parameters
| Name | Type | Description |
|---|---|---|
batch | AllowanceRequestBatch | The AllowanceRequestBatch struct to hash. |
Returns
| Name | Type | Description |
|---|---|---|
structHash | bytes32 | The EIP-712 struct hash. |
_transferFromOwner
Transfers an allowance from the owner to the subaccount.
*Slither raises an arbitrary-send-erc20 finding for the
safeTransferFrom(owner, subaccount, amount) call inside this helper.
The warning is a false positive in our threat model because:
- The function is
internaland only reached viaspendAllowance, after* the allowance window and per-subaccount quota checks have passed. - Success still depends on the owner having granted an explicit ERC-20 allowance or Permit2 allowance to this contract. Without such opt-in the transfer will revert.
- The semantics mirror the well-audited Gnosis Safe flow
(AllowanceHolder pull, fallback to
safeTransferFrom). For these reasons the transfer cannot be exploited to steal funds from arbitrary users, therefore we silence the detector for the body of the function.*
function _transferFromOwner(address owner, address subaccount, address token, uint256 amount) internal;
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The owner of the allowance. |
subaccount | address | The subaccount that the allowance is transferred to. |
token | address | The token that the allowance is transferred for. |
amount | uint256 | The amount of the allowance to transfer. |
_predictHolderAddress
Fetches the cached canonical AllowanceHolder for an (owner, token) pair if it has been
observed via bootstrapAllowance.
function _predictHolderAddress(address owner, address token) internal view returns (address holder);
Parameters
| Name | Type | Description |
|---|---|---|
owner | address | The owner address. |
token | address | The ERC-20 token address. |
Returns
| Name | Type | Description |
|---|---|---|
holder | address | The cached holder address (zero address if none). |
_domainNameAndVersion
Returns the EIP-712 domain name and version used by this contract (chainId is hard-wired to 1 in the parent).
*Please override this function to return the domain name and version.
function _domainNameAndVersion()
internal
pure
virtual
returns (string memory name, string memory version)
{
name = "Solady";
version = "1";
}
Note: If the returned result may change after the contract has been deployed,
you must override _domainNameAndVersionMayChange() to return true.*
function _domainNameAndVersion() internal pure override returns (string memory name, string memory version);
Returns
| Name | Type | Description |
|---|---|---|
name | string | The domain name. |
version | string | The domain version. |
Events
HolderPullFailed
Emitted when an attempt to pull funds via AllowanceHolder fails and the flow falls back to the approval path.
event HolderPullFailed(address indexed holder, address indexed subaccount, uint256 indexed amount);
Parameters
| Name | Type | Description |
|---|---|---|
holder | address | The holder contract that was called. |
subaccount | address | The subaccount the funds were destined to. |
amount | uint256 | The amount that failed to pull. |
Errors
InvalidNonce
error InvalidNonce();
HolderOwnerMismatch
error HolderOwnerMismatch();
HolderTokenMismatch
error HolderTokenMismatch();
HolderManagerMismatch
error HolderManagerMismatch();
HolderMismatch
error HolderMismatch();
AlreadyInitialised
error AlreadyInitialised();
ZeroAmount
error ZeroAmount();
HolderNotCommitted
error HolderNotCommitted();
HolderCommitMismatch
error HolderCommitMismatch();
HolderAlreadyCommitted
error HolderAlreadyCommitted();
UnauthorizedCaller
error UnauthorizedCaller();