Security
This page documents the security mechanisms built into the Grunt protocol, the key invariants the system maintains, and the centralization risks to be aware of when deploying or integrating.
Security Mechanisms
Section titled “Security Mechanisms”| Mechanism | Purpose |
|---|---|
| Virtual Share Offset | Prevents first-depositor inflation attacks in PositionManager |
| Conservative Rounding | Debt rounds up, collateral rounds down to protect vaults |
| LLTV Enforcement | Withdrawals check available collateral to maintain position health |
| Fee Accrual Ordering | Fees always accrued before operations for fair accounting |
| Role-Based Access | Operations restricted to specific roles via OwnableRoles |
| Reentrancy Guards | ReentrancyGuardTransient on all state-changing operations |
| Whitelisted Positions | Only positions in borrowModules set can be used in queues |
| Max Rebalance Loss | Rebalancing reverts if total assets decrease beyond threshold |
| ERC-7201 Storage | Namespaced storage prevents collisions in proxy deployments |
| EIP-712 Signatures | Typed data signing for secure off-chain offer validation |
Virtual Share Offset
Section titled “Virtual Share Offset”The Position Manager uses a virtual share offset to prevent the classic first-depositor inflation attack. When computing share conversions, a virtual supply and virtual assets offset are applied:
shares = assets * (totalSupply + 1e6) / (totalAssets + 1)assets = shares * (totalAssets + 1) / (totalSupply + 1e6)The 1e6 virtual supply ensures that an attacker cannot donate a large amount of assets to inflate the share price before other depositors enter. The cost of the attack exceeds any profit from rounding.
Conservative Rounding
Section titled “Conservative Rounding”Across all Position Manager and Borrow Position operations, rounding consistently favors the protocol over the individual caller:
- Debt rounds up – borrowers always owe at least as much as they borrowed, never less.
- Collateral rounds down – withdrawers receive at most their entitled share, never more.
This ensures that rounding errors cannot be exploited to drain value from the vault over many small operations.
// On burn, debt rounds up to protect remaining shareholdersdebt = totalDebt.mulDivUp(shares, totalSupply);
// On burn, collateral rounds down to protect remaining shareholderscollateral = totalCollateral.mulDivDown(shares, totalSupply);LLTV Enforcement
Section titled “LLTV Enforcement”Every borrow and withdrawal operation on a MorphoBorrowPosition checks the position’s health against its custom LLTV. The available collateral is computed as:
availableCollateral = totalCollateral - requiredCollateral;requiredCollateral = debt * ORACLE_PRICE_SCALE / (lltv * collateralPrice);Withdrawals that would push the position below the custom LLTV revert. This is enforced per-position, not just at the aggregate level, meaning each individual lending venue maintains healthy collateralization.
Fee Accrual Ordering
Section titled “Fee Accrual Ordering”Fees are always accrued before any deposit, withdraw, burn, or rebalance operation:
function deposit(uint256 collateral, uint256 debt) external returns (int256 shares) { _accrueFees(); // Always first // ... deposit logic}This ordering ensures that:
- Incoming depositors pay their fair share of accumulated fees
- Exiting users cannot front-run fee accrual to avoid dilution
- Fee calculations reflect the true state before any operation modifies it
Reentrancy Guards
Section titled “Reentrancy Guards”All state-changing functions across the protocol use ReentrancyGuardTransient from Solady. This uses transient storage (EIP-1153) for gas-efficient reentrancy protection:
function deposit(uint256 collateral, uint256 debt) external nonReentrant returns (int256 shares){ // ...}Transient storage is cleared at the end of each transaction, eliminating the warm/cold SSTORE costs associated with traditional reentrancy guards.
Whitelisted Positions
Section titled “Whitelisted Positions”The Position Manager maintains a borrowModules set. Only addresses registered in this set can appear in supply or withdrawal queues, and only registered modules can be targeted during rebalancing:
// Only the owner can add modulesfunction addModule(address module) external onlyOwner { // ...}
// Queue validation ensures only registered modules are usedfunction setSupplyQueue(SupplyQueueEntry[] calldata queue) external onlyCurator { for (uint256 i = 0; i < queue.length; i++) { require(borrowModules.contains(queue[i].position), "Unknown module"); } // ...}This prevents unauthorized contracts from being injected into the Position Manager’s operational flow.
Max Rebalance Loss
Section titled “Max Rebalance Loss”Rebalancing operations (redistributing collateral and debt across positions) are subject to a maximum allowed loss threshold:
uint256 totalAssetsBefore = totalAssets();// ... execute rebalancing operationsuint256 totalAssetsAfter = totalAssets();
require( totalAssetsAfter >= totalAssetsBefore - maxRebalanceLoss, "ExcessiveRebalanceLoss");If the total assets decrease by more than maxRebalanceLoss during a rebalance, the entire transaction reverts. The owner sets this threshold to limit the damage from misconfigured or malicious rebalancing operations.
EIP-712 Signatures
Section titled “EIP-712 Signatures”The Request contract uses EIP-712 typed data signing for off-chain offer validation. Bridge facilitator offers are signed off-chain and verified on-chain when consumed:
struct Offer { address maker; // Funder address uint256 amount; // Principal amount uint256 expectedReturn; // Expected yield uint256 nonce; // Sequential replay protection uint256 expiration; // Validity deadline bool useCallback; // Whether to call onRequestConsumed}Offers support both EOA signatures and EIP-1271 contract-based signatures (e.g., multisigs, smart wallets). Nonce management allows hard cancellation of outstanding offers by advancing the on-chain nonce.
Key Invariants
Section titled “Key Invariants”These are properties the protocol is designed to maintain at all times. Any violation of these invariants indicates a bug or attack. For a comprehensive list of all invariants with their verification methods, see the Invariants documentation.
Facility: Per-Intent Token Accounting (FAC-5)
Section titled “Facility: Per-Intent Token Accounting (FAC-5)”The Facility tracks token balances independently for each intent. The sum of all per-intent balances for a given token must never exceed the Facility’s actual balance of that token:
for each token T: sum(intentBalance[id][T] for all id) <= T.balanceOf(facility)This ensures that one intent’s operations cannot consume tokens belonging to another intent. Deposits, withdrawals, and claims all maintain this invariant.
Position Manager: Share Price Integrity (PM-1)
Section titled “Position Manager: Share Price Integrity (PM-1)”The Position Manager’s share price must accurately reflect the net equity of the aggregated positions:
sharePrice = (totalAssets + 1) / (totalSupply + 1e6)totalAssets = sum(collateralQuoted[i]) - sum(debt[i]) for all positions iShare price can only change through:
- External market movements (oracle price changes, interest accrual)
- Fee accrual (which mints shares to the fee recipient)
- Deposits/withdrawals that proportionally adjust assets and shares
No operation should arbitrarily inflate or deflate the share price.
Borrow Position: Custom LLTV Health (PM-7)
Section titled “Borrow Position: Custom LLTV Health (PM-7)”Every borrow position enforces its custom LLTV on every operation. After any borrow() or withdrawCollateral() call, the position must remain healthy:
isHealthy = (collateral * oraclePrice * lltv) / ORACLE_PRICE_SCALE >= totalBorrowedThe custom LLTV is immutable after initialization and must be less than or equal to the underlying market’s LLTV. This provides a safety buffer above the liquidation threshold.
WrappedAsset: 1:1 Backing (WA-1)
Section titled “WrappedAsset: 1:1 Backing (WA-1)”The WrappedAsset contract maintains a strict 1:1 backing with its underlying token:
wrappedAsset.totalSupply() == underlyingToken.balanceOf(wrappedAsset)Every wrap operation mints exactly 1 wrapped token per underlying token deposited, and every unwrap burns exactly 1 wrapped token per underlying token returned.
Fund and Request Exclusivity
Section titled “Fund and Request Exclusivity”Each Fund and Request instance is exclusive to a single intent. Once bound to an intent via setFund() or setRequest(), that Fund or Request cannot be reused by any other intent:
for each Fund F: count(intents bound to F) <= 1
for each Request R: count(intents bound to R) <= 1This prevents cross-contamination of assets between intents and ensures clean accounting for each funding flow.
Centralization Risks
Section titled “Centralization Risks”The protocol includes several privileged roles that present centralization risks if not properly managed. In the mainnet deployment these roles are held by the multisigs of the Verification Council, each a 2 of 3 across independent organizations, with implementation upgrades additionally gated by a 24 hour timelock.
Guard Owner
Section titled “Guard Owner”The TransferGuard owner can:
- Set any address to
BLOCKLISTstatus, blocking all transfers for that address - Switch a token between blocklist and whitelist modes
- Pause all transfers for a specific token
Mitigation: In production deployments, the guard owner should be a multisig or governance contract with a timelock. Monitor AddressStatusSet and TokenConfigSet events for unauthorized changes.
Beacon Owner
Section titled “Beacon Owner”Each factory’s UpgradeableBeacon owner can:
- Replace the implementation contract for all proxies created by that factory
- Effectively modify the behavior of every deployed instance in a single transaction
Mitigation: The beacon owner should be a multisig with a timelock delay. Implementation upgrades should go through a governance process with auditing. Consider using a two-step upgrade pattern where the new implementation is proposed and then executed after a delay.
Facilitator
Section titled “Facilitator”The Facility’s Facilitator role has broad operational control over intent lifecycle:
- Create intents and set deposit caps
- Lock intents (closing deposits) and resolve them (enabling claims)
- Execute fund operations, request operations, position manager operations, and swaps
- Set which Fund and Request contracts are bound to each intent
Mitigation: The facilitator is typically an operations bot. Constrain its key management through hardware security modules or threshold signature schemes. All facilitator operations are on-chain and auditable. The protocol’s state machine design limits what the facilitator can do at each phase – for example, it cannot resolve an intent while a Fund order is active or a Request is unpaid.
Audit Considerations
Section titled “Audit Considerations”When reviewing or auditing the Grunt codebase, pay particular attention to:
- Storage layout compatibility – ensure that ERC-7201 storage structs are only modified by appending new fields, never by reordering or removing existing ones.
- Cross-module interactions – the Facility calls into Fund, Request, and Position Manager contracts. Verify that state changes in one module cannot create inconsistencies in another.
- Oracle dependency – Position Manager valuations depend on Morpho Blue oracles. Consider the impact of oracle failures, manipulation, or stale prices on position health calculations.
- Token handling – the protocol interacts with multiple ERC20 tokens. Verify correct handling of fee-on-transfer tokens, rebasing tokens, and tokens with non-standard decimals.
- Signature replay – verify that EIP-712 signatures in the Request contract are properly scoped to the contract address (via domain separator) and protected by nonce management.