Grove Allocator
The Grove Allocator is the protocol's core infrastructure for deploying stablecoin capital into diversified credit strategies across multiple chains. It operates as a vault-based, non-custodial system where all funds remain in onchain custody while the protocol routes capital into institutional credit opportunities.
The protocol consists of four core contracts:
- ALMProxy — Custody contract that holds tokens and routes calls on behalf of Controllers.
- MainnetController — Main Controller for the hub (e.g. Ethereum Mainnet).
- ForeignController — Foreign Controller for spoke chains (referred to as "foreign" domains).
- RateLimits — Stateful rate limiting engine that enforces caps on all controller operations.
Architecture
The Grove Allocator follows a three-layer architecture that separates custody, business logic, and risk management:
- The Relayer (an offchain component operated by the ALM Planner) submits transactions to the Controller.
- The Controller validates the operation, enforces rate limits via the RateLimits contract, and forwards the call to the ALMProxy.
- The ALMProxy executes the call against external protocols. All resulting funds remain in the ALMProxy's custody.
This separation enables the controller logic to be upgraded independently — a new controller can be granted the CONTROLLER role on the ALMProxy and RateLimits contracts, enabling new strategies without migrating funds.
Permissions
All contracts inherit OpenZeppelin's AccessControl for role-based permissioning. Four roles govern the protocol:
| Role | Purpose |
|---|---|
DEFAULT_ADMIN_ROLE | Grants and revokes all other roles. Configures parameters such as rate limits, slippage tolerances, mint recipients, and exchange rate thresholds. |
RELAYER | Assigned to the ALM Planner's offchain component. Can invoke DeFi operations on the controller (deposits, withdrawals, swaps, bridging, etc.). |
FREEZER | Emergency role that can revoke any relayer's access, effectively halting all automated operations. |
CONTROLLER | Assigned to controller contracts. Authorizes calls to the ALMProxy's execution functions and rate limit state updates on the RateLimits contract. |
ALMProxy
File: src/ALMProxy.sol
The ALMProxy is a minimal, stateless contract that holds custody of all funds managed by the Grove Allocator. It acts as the execution layer — receiving instructions from an authorized controller and forwarding them to external protocols.
Design
The proxy is intentionally simple. Its only state is the access control configuration inherited from OpenZeppelin's AccessControl. This statelessness means the proxy never needs to be upgraded or migrated; instead, new controllers can be granted the CONTROLLER role to introduce new logic while all funds remain in place.
Functions
| Function | Description |
|---|---|
doCall(target, data) | Executes a standard call to target with the given data. Returns the result bytes. |
doCallWithValue(target, data, value) | Executes a call to target with attached ETH value. Used for operations requiring native token payments (e.g., cross-chain messaging fees). |
doDelegateCall(target, data) | Executes a delegate call to target. The called code runs in the context of the ALMProxy. |
All three functions are restricted to the CONTROLLER role. The proxy also includes a receive() function to accept ETH transfers.
MainnetController
File: src/MainnetController.sol
The MainnetController manages liquidity operations on Ethereum mainnet. It orchestrates interactions with a broad set of DeFi protocols through the ALMProxy, with each operation subject to rate limiting. All functions that change the balance of funds in the ALMProxy are only callable by the RELAYER role.
Admin Configuration
Administrators can configure the following parameters:
- Bridging recipients — Destination addresses per domain/endpoint/chain for CCTP, LayerZero, and Centrifuge transfers.
- Max slippage — Per-pool slippage tolerance (1e18 precision) for lending and DEX operations.
- Max exchange rates — Per-token exchange rate ceilings (1e36 precision) for ERC4626 deposit protection against manipulation.
- DEX pool parameters — Per-pool configuration for tick bounds, max tick deltas, and TWAP observation windows.
Supported Operations
The MainnetController provides relayer-callable functions spanning the following categories:
- USDS Mint/Burn — Drawing and repaying USDS through the Sky allocation vault.
- Stablecoin Conversion — DAI/USDS swaps and USDS/USDC conversion via the mainnet PSM.
- ERC4626 Vaults — Depositing, withdrawing, and redeeming from standard yield vaults, with exchange rate validation on deposits.
- ERC7540 Async Vaults — Submitting and claiming asynchronous deposit and redemption requests.
- Centrifuge (RWA Vaults) — Managing async deposit/redeem requests, cancellations, and cross-chain share transfers for real-world asset vaults.
- Lending (Aave V3) — Supplying and withdrawing assets, with slippage checks on deposits.
- DEX Operations (Curve, Uniswap V3) — Token swaps and liquidity provision/removal with slippage protection, tick bounds enforcement, and TWAP oracle validation.
- Ethena (USDe/sUSDe) — Preparing USDe mints/burns, initiating sUSDe cooldowns, and unstaking.
- Pendle Finance — Redeeming principal tokens after market expiry.
- Cross-Chain Bridging (CCTP, LayerZero) — Transferring tokens to foreign chains, with dual rate limiting (global and per-destination).
- ERC20 Transfers — Moving tokens from the ALMProxy to specified destinations, rate limited per asset-destination pair.
- Merkl Rewards — Managing operator permissions for reward claims.
Freezer
The FREEZER role can call removeRelayer to revoke the RELAYER role from any address, enabling emergency shutdown of all automated operations.
ForeignController
File: src/ForeignController.sol
The ForeignController manages liquidity operations on non-mainnet chains. It shares many of the same protocol integrations as the MainnetController but replaces mainnet-specific operations (USDS minting, Ethena, DAI/USDS conversion, mainnet PSM) with the Spark PSM3 for stablecoin operations on foreign chains.
Admin Configuration
The ForeignController supports the same admin configuration as the MainnetController: bridging recipients, max slippage, max exchange rates, DEX pool parameters, and a configurable Merkl distributor address.
Supported Operations
- PSM3 (Foreign Chain PSM) — Depositing assets into and withdrawing assets from the Spark PSM3 contract deployed on the respective foreign chain.
- ERC4626 Vaults — Same deposit, withdraw, and redeem operations as the MainnetController.
- ERC7540 Async Vaults — Same async deposit/redeem request lifecycle.
- Centrifuge (RWA Vaults) — Same request management and cross-chain share transfers.
- Lending (Aave V3) — Same supply and withdraw operations.
- DEX Operations (Curve, Uniswap V3) — Same swaps and liquidity management.
- Pendle Finance — Same token redemption.
- Cross-Chain Bridging (CCTP, LayerZero) — Same token transfer operations.
- ERC20 Transfers, Merkl Rewards, Freezer — Same functionality as the MainnetController.
RateLimits
File: src/RateLimits.sol
The RateLimits contract is the risk management layer of the Grove Allocator. It enforces configurable, time-based rate limits on all controller operations, preventing excessive capital movement within short timeframes.
Design
Rate limits are identified by bytes32 keys — typically keccak256 hashes that encode the operation type and optionally an asset, destination, or domain. This key-based approach decouples rate limits from specific function signatures, providing flexibility as controller logic evolves.
Composite keys are generated by RateLimitHelpers:
| Helper | Key Format | Use Case |
|---|---|---|
makeAssetKey(key, asset) | keccak256(key, asset) | Per-asset limits (e.g., deposits to a specific vault) |
makeAssetDestinationKey(key, asset, destination) | keccak256(key, asset, destination) | Per-asset, per-destination limits (e.g., asset transfers) |
makeDomainKey(key, domain) | keccak256(key, domain) | Per-domain limits (e.g., CCTP bridging per destination chain) |
Rate Limit Data
Each rate limit stores four values:
| Field | Description |
|---|---|
maxAmount | The ceiling — the maximum available capacity at any point in time. |
slope | Refill rate in tokens per second. Determines how quickly the limit replenishes after usage. |
lastAmount | The remaining capacity at the time of the last update. |
lastUpdated | The block timestamp of the last update. |
Linear Refill Formula
The current available limit at any moment is calculated as:
currentRateLimit = min(slope * (block.timestamp - lastUpdated) + lastAmount, maxAmount)
This produces a linear refill curve: after a portion of the limit is consumed, it gradually recovers over time at the configured slope rate, up to maxAmount.
Example: A rate limit configured with maxAmount = 10,000,000 and slope = 115 (tokens/second, approximately 10M per day) allows up to 10M tokens to be moved immediately. After the full amount is consumed, capacity refills at approximately 115 tokens per second, reaching full capacity again after roughly 24 hours.
Unlimited Rate Limits
Setting maxAmount to type(uint256).max creates an unlimited rate limit that bypasses all checks. This is useful for operations that should not be constrained (e.g., 1:1 stablecoin conversions with no risk).
State Updates
Controllers interact with the RateLimits contract through two functions:
| Function | Description |
|---|---|
triggerRateLimitDecrease(key, amount) | Called when an operation consumes capacity. Reverts if the requested amount exceeds the current limit. |
triggerRateLimitIncrease(key, amount) | Called when an operation reverses a previous consumption (e.g., burning USDS increases the mint limit). Capped at maxAmount. |
Both functions are restricted to the CONTROLLER role and update lastAmount and lastUpdated atomically.
Admin Functions
| Function | Description |
|---|---|
setRateLimitData(key, maxAmount, slope, lastAmount, lastUpdated) | Configures a rate limit with full control over all parameters. |
setRateLimitData(key, maxAmount, slope) | Convenience overload that initializes a rate limit at full capacity (lastAmount = maxAmount, lastUpdated = now). |
setUnlimitedRateLimitData(key) | Sets a rate limit to unlimited (bypasses all checks). |