AllowanceManager

Git Source

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:

  1. A freshly deployed AllowanceHolder (preferred – no approvals necessary after the initial permit)
  2. 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

NameTypeDescription
_permit2addressAddress of the Permit2 contract.
factoryAddraddressCanonical 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

NameTypeDescription
owneraddressThe owner of the allowance (signer of the permit given to the holder).
subaccountaddressSubaccount that the allowance applies to.
tokenaddressERC-20 token address.
amountuint256Allowance amount.
timeframeuint256Time 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

NameTypeDescription
owneraddressThe token owner.
tokenaddressThe ERC-20 token managed by the holder.
holderaddressThe 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

NameTypeDescription
holderaddressThe 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

NameTypeDescription
owneraddressThe token owner.
tokenaddressThe ERC-20 token.

Returns

NameTypeDescription
<none>addressholder 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

NameTypeDescription
factoryAddressaddressThe address of the canonical SubaccountFactory.

permit2

The Permit2 contract address

function permit2() external view returns (address);

Returns

NameTypeDescription
<none>addresspermit2Addr 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

NameTypeDescription
owneraddressThe owner of the subaccount.
subaccountaddressThe subaccount that the allowance is set for.
tokenaddressThe token that the allowance is set for.

Returns

NameTypeDescription
<none>Allowanceallowance The allowance struct.

allowanceRequestBatchTypehash

EIP-712(x) typehash for the AllowanceRequestBatch struct (per-subaccount)

function allowanceRequestBatchTypehash() external pure returns (bytes32);

Returns

NameTypeDescription
<none>bytes32typeHash The struct type-hash.

allowanceRequestTypehash

EIP-712(x) typehash for the AllowanceRequest struct (per-subaccount)

function allowanceRequestTypehash() external pure returns (bytes32);

Returns

NameTypeDescription
<none>bytes32typeHash The struct type-hash.

setAllowances

Sets allowances for a batch of subaccounts

function setAllowances(address owner, AllowanceRequestBatch calldata requests, bytes calldata signature) public;

Parameters

NameTypeDescription
owneraddressThe owner of the subaccounts
requestsAllowanceRequestBatchThe AllowanceRequestBatch struct containing the requests
signaturebytesThe 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

NameTypeDescription
owneraddressThe owner of the subaccount
tokenaddressThe token to spend the allowance for
amountuint256The 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

NameTypeDescription
batchAllowanceRequestBatchThe AllowanceRequestBatch struct to hash.

Returns

NameTypeDescription
structHashbytes32The 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:

  1. The function is internal and only reached via spendAllowance, after* the allowance window and per-subaccount quota checks have passed.
  2. 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.
  3. 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

NameTypeDescription
owneraddressThe owner of the allowance.
subaccountaddressThe subaccount that the allowance is transferred to.
tokenaddressThe token that the allowance is transferred for.
amountuint256The 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

NameTypeDescription
owneraddressThe owner address.
tokenaddressThe ERC-20 token address.

Returns

NameTypeDescription
holderaddressThe 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

NameTypeDescription
namestringThe domain name.
versionstringThe 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

NameTypeDescription
holderaddressThe holder contract that was called.
subaccountaddressThe subaccount the funds were destined to.
amountuint256The 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();