Skip to content
3F Docs

Roles

The Grunt protocol uses Solady’s OwnableRoles for role-based access control. Each contract defines a set of roles as bitmask flags. An address can hold multiple roles, and each function is gated by one or more required roles.

The numeric bit value of every role, per contract, is listed on the Deployments API page and published in /deployments.json.

Contract Role Typical Holder Permissions
Facility Owner Protocol Admin Create intents, update target asset, set descriptor
Facility Facilitator Operations Bot Create intents, lock, resolve, set caps, set fund/request, all fund/request/PM/swap operations
Facility Guardian Signers (EOA) Sign privileged-action authorizations (multi-sig for quorum)
Facility Pauser Emergency Admin Pause/unpause facility
Request Owner Protocol Admin Mark loan as repaid, authorize minting
Request Puller Facility Pull bridge loan funds, repay funds
Request Consumer Protocol Admin Consume signed offers
Fund Depositor Facility Create/cancel/commit/unlock/recover orders
Fund Settler Operations Bot Settle fund state after external operations
PositionManager Owner Protocol Admin Add modules, set LLTV, set fees
PositionManager Minter Facility Deposit, withdraw, burn shares
PositionManager Curator Operations Bot Set supply/withdrawal queues
PositionManager Rebalancer Rebalancer Contract Execute rebalancing operations
BorrowPosition Owner PositionManager All borrow/supply operations
Retargetter Owner Protocol Admin Configuration, whitelists, position manager binding, force repay
Retargetter Rebalancer Operations Bot Start, fund, order steps, rebalance, repay and resolve an operation
Retargetter Consumer Lender-facing surface Consume signed offers, authorize minting
BorrowOffersRegistry Owner Protocol Admin Grant offer roles, set per-collateral timelock and bonus floor
BorrowOffersRegistry Proposer Offer proposer Post offers on any borrow position
BorrowOffersRegistry Guardian Emergency Admin Revoke any offer on any borrow position
TransferGuard Owner Protocol Admin Set token config, grant roles
TransferGuard Pauser Emergency Admin Pause/unpause tokens
TransferGuard Compliance Compliance Bot Set address blocklist/whitelist status

The Facility is the central hub and has the most granular role system.

The protocol administrator who configures intents and global settings.

// Create a new intent
facility.createIntent(
depositAsset,
targetAsset,
depositCap,
guardKey,
resolveStart,
quorum,
transferableIntent
);
// Update the target asset (only during DEPOSITING)
facility.updateTarget(intentId, newTargetAsset);
// Set the intent descriptor for ERC-6909 metadata
facility.setDescriptor(descriptorAddress);

The operations role that drives the intent lifecycle forward. This is the most broadly permissioned role and is typically held by an automated operations bot.

// Intent lifecycle
facility.createIntent(...); // Also available to Owner
facility.setDepositCap(id, newCap); // Adjust deposit cap
facility.lock(id); // Close deposits, enter RESOLVING
facility.resolve(id); // Enable claims, enter RESOLVED
// Bind external contracts to an intent
facility.setFund(id, fundAddress);
facility.setRequest(id, requestAddress);
// Fund operations (delegated to IFund)
facility.createFundOrder(id, mode, amount);
facility.commitFundOrder(id);
facility.unlockFundOrder(id, receiver);
facility.recoverFundOrder(id, receiver);
facility.cancelFundOrder(id);
// Request operations
facility.pullFromRequest(id, amount, data);
facility.repayRequest(id, amount);
// Position Manager operations
facility.depositToPositionManager(id, pm, collateral, debt);
facility.withdrawFromPositionManager(id, pm, collateral, debt);
facility.burnPositionManagerShares(id, pm, shares);
// Guardian-gated swap
facility.swap(id, swapData, guardianSignatures);

Guardians are EOA signers whose EIP-712 signatures authorize privileged Facility actions — binding a Request to a Facility, binding a Fund to a Facility, and Position Manager swaps. These actions require guardian signatures when the configured quorum is nonzero, forming a multi-signature authorization scheme. Signatures can be assembled from a quorum out of band or produced automatically by the guardian coordinator.

// Guardian signs an EIP-712 typed data message off-chain
// The swap function verifies the quorum is met
facility.swap(
intentId,
swapData,
guardianSignatures // Must meet the intent's quorum requirement
);

Emergency role that can halt all Facility operations.

// Pause the entire Facility
facility.pause();
// Resume operations
facility.unpause();

Manages the Request lifecycle and authorizes funding.

// Mark the bridge loan as repaid, enabling PT/YT redemptions
request.setRepaid();
// Authorize a funder to mint PT/YT directly
request.authorizeMinting(funder, principalAmount, yieldAmount);
// Consume a signed offer (also available to Consumer)
request.consume(offer, signature, amount);

Can consume signed offers and authorize minting, enabling delegation of offer processing.

// Consume a bridge facilitator's signed offer
request.consume(offer, signature, amount);
// Authorize direct minting
request.authorizeMinting(funder, principalAmount, yieldAmount);

The address authorized to draw down bridge loan capital. This is always the Facility contract and is set at deployment.

// Pull funds from the Request (called by Facility)
request.pullFunds(amount, data);
// Repay funds to the Request
request.repay(amount);

Drives the Fund’s order state machine. This is typically the Facility contract.

// Create a new order
fund.create(mode, amount); // mode: DEPOSIT or REDEEM
// Cancel an accepted order
fund.cancel();
// Commit assets to the order
fund.commit();
// Unlock resulting assets after processing
fund.unlock(receiver);
// Recover assets if processing failed
fund.recover(receiver);

Updates fund state after off-chain or async operations complete.

// Settle the fund state (e.g., after USCC minting completes)
fund.settle(settlementData);

Configures the Position Manager’s modules, risk parameters, and fees.

// Add a borrow position as a module
positionManager.addBorrowModule(borrowPositionAddress);
// Set the target LTV of the position manager
positionManager.setLtv(0.5e18); // 50%, a 2x leverage tier
// Configure fees in one call
// `managementFee` is the base fee — see the Fees page for the naming
positionManager.setFeeData(
feeRecipientAddress,
100, // 1% annual base fee (in basis points)
1000 // 10% performance fee (in basis points)
);
// Maximum allowed loss during rebalancing, and the cooldown between rebalances
positionManager.setRebalanceConfig(200, 0); // bps, seconds
// Set the transfer guard for compliance
positionManager.setTransferGuard(guardAddress);

Can deposit collateral, withdraw collateral, and burn shares. This is the Facility contract.

// Deposit collateral and optionally borrow
int256 shares = positionManager.deposit(collateralAmount, debtAmount);
// Withdraw collateral by repaying debt
int256 shares = positionManager.withdraw(collateralAmount, debtAmount);
// Burn shares for pro-rata exit
(uint256 collateral, uint256 debt) = positionManager.burn(shareAmount);

Manages the ordering of positions for deposits and withdrawals.

// Set the supply queue (determines deposit routing order)
PositionManager.SupplyQueueEntry[] memory queue =
new PositionManager.SupplyQueueEntry[](2);
queue[0] = PositionManager.SupplyQueueEntry({
position: positionA,
maxBorrow: 2_000_000e6
});
queue[1] = PositionManager.SupplyQueueEntry({
position: positionB,
maxBorrow: 5_000_000e6
});
positionManager.setSupplyQueue(queue);
// Set the withdrawal queue (determines withdrawal routing order)
address[] memory withdrawQueue = new address[](2);
withdrawQueue[0] = positionA;
withdrawQueue[1] = positionB;
positionManager.setWithdrawalQueue(withdrawQueue);

Executes rebalancing operations to redistribute collateral and debt across positions without affecting shares.

PositionManager.RebalancingData memory data = PositionManager.RebalancingData({
collateral: 0,
debt: 0,
operations: new PositionManager.RebalancingOperation[](2)
});
// Withdraw from position A
data.operations[0] = PositionManager.RebalancingOperation({
position: positionA,
operationType: PositionManager.RebalancingOperationType.WITHDRAW,
amount: 500_000e18
});
// Supply to position B
data.operations[1] = PositionManager.RebalancingOperation({
position: positionB,
operationType: PositionManager.RebalancingOperationType.SUPPLY,
amount: 500_000e18
});
positionManager.rebalance(data);

The Position Manager is the sole owner of each borrow position and controls all lending operations.

// Supply collateral
borrowPosition.supplyCollateral(amount);
// Withdraw collateral (enforces custom LLTV)
borrowPosition.withdrawCollateral(amount);
// Borrow against collateral (enforces custom LLTV)
borrowPosition.borrow(amount);
// Repay debt
borrowPosition.repay(amount);

Each Retargetter instance is bound to one asset pair and runs one retargeting operation at a time against one owner-curated Position Manager.

Fully trusted. Holds every power below, plus the configuration surface, and bypasses the direction checks on rebalance.

// Sizing and repayment configuration (horizon, ticks, caps, residual tolerance)
retargetter.setConfig(config);
retargetter.setEstimates(estimates);
// Whitelists: only whitelisted funds and flash-loan modules can be used
retargetter.setFund(fundAddress, true);
retargetter.setFlashLoanModule(moduleAddress, true);
// Rotate the bound position manager (same asset pair), or unbind with address(0).
// Only possible while no operation is active.
retargetter.setPositionManager(positionManagerAddress);
// Settle a Request outside the trustless formula, with explicit balance bounds
retargetter.forceRepay(amount, minBalance, maxBalance);

Semi-trusted operator that drives operations. Boxed in by the principal cap, the direction checks, the value-conservation gate and the whitelists.

// Asynchronous: deploy a Request and run across the fund's settlement window
address request = retargetter.startRetargetting(
principal, maxYieldBps, fund, requestName, requestSymbol
);
retargetter.pullRequestFunds(amount); // closes the funding round
retargetter.create(order);
retargetter.commit();
retargetter.unlock();
retargetter.rebalance(rebalancingData);
retargetter.repay();
retargetter.resolve();
// Synchronous: the whole operation inside one flash loan
retargetter.startSyncRetargetting(flashLoanModule, flashLoanAmount, fund, steps);

This role is not the same as the Position Manager’s REBALANCER_ROLE (index 2). A Retargetter needs that one on the Position Manager to call rebalance there, and because the one-operation-at-a-time guarantee is per instance, the Position Manager’s rebalancer role should be held by at most one Retargetter.

The lender-facing surface. Brings capital into an asynchronous operation.

// Consume a lender's signed EIP-712 offer against the operation's Request
uint256 ytAmount = retargetter.consume(offer, signature, ptAmount);
// Register (or revoke, with zero amounts) a mint authorization for a broker
retargetter.authorizeMinting(brokerAddress, ptAmount, ytAmount);

Both are gated on the live principal cap and the operation’s yield cap, and both only work while the consumption window is open. The consumer role must never overlap with the Position Manager’s minters: the principal-cap read trusts them.

The BorrowOffersRegistry is a single protocol-wide contract holding the offer roles and the per-collateral offer configuration for every Borrow Position. Roles are global: a proposer can post offers on any position.

Manages roles and configuration, and is always authorized by the registry’s own check functions, so an ownership handover never leaves the new owner without powers.

registry.setProposer(account, true);
registry.setGuardian(account, true);
// Per-collateral configuration. The timelock change is itself timelocked.
registry.setOfferTimelock(collateralToken, 1 days);
registry.setMinOfferBonus(collateralToken, 150); // 1.5% floor, in basis points

Posts offers on any borrow position, and may revoke its own.

uint8 id = borrowPosition.proposeOffer(collateralAmount, debtShares, expiresAt);

The veto inside an offer’s timelock window, and the kill switch for a standing offer that should no longer be consumable.

uint8[] memory ids = new uint8[](1);
ids[0] = offerId;
borrowPosition.revokeOffers(ids);

See Offer Pre-Liquidation for the mechanism these roles operate.

Configures token compliance modes and manages roles.

// Set token to whitelist mode (paused = false, whitelist = true)
transferGuard.setTokenConfig(tokenAddress, false, true);
// Set token to blocklist mode (paused = false, whitelist = false)
transferGuard.setTokenConfig(tokenAddress, false, false);
// Pause all transfers for a token
transferGuard.setTokenConfig(tokenAddress, true, false);

Can pause and unpause specific tokens in an emergency.

// Pause a token (blocks all transfers)
transferGuard.pause(tokenAddress);
// Unpause a token
transferGuard.unpause(tokenAddress);

Manages address statuses for blocklist/whitelist enforcement.

// Block a specific address
transferGuard.setAddressStatus(userAddress, AddressStatus.BLOCKLIST);
// Whitelist an address
transferGuard.setAddressStatus(userAddress, AddressStatus.WHITELIST);
// Batch update
address[] memory accounts = new address[](3);
accounts[0] = user1;
accounts[1] = user2;
accounts[2] = user3;
transferGuard.setAddressStatusBatch(accounts, AddressStatus.WHITELIST);

Complete mapping of functions to their required roles and state preconditions.

Function Required Role Required State
createIntent Owner or Facilitator Any
updateTarget Owner DEPOSITING
setDescriptor Owner Any
setDepositCap Facilitator DEPOSITING
lock Facilitator DEPOSITING
resolve Facilitator RESOLVING (no active fund order, request repaid)
setFund Facilitator RESOLVING
setRequest Facilitator RESOLVING
deposit Any DEPOSITING
withdraw Token Owner/Operator DEPOSITING
claim Token Owner/Operator RESOLVED
swap Facilitator + Guardian quorum RESOLVING
createFundOrder Facilitator RESOLVING
commitFundOrder Facilitator RESOLVING
unlockFundOrder Facilitator RESOLVING
recoverFundOrder Facilitator RESOLVING
cancelFundOrder Facilitator RESOLVING
pullFromRequest Facilitator RESOLVING
repayRequest Facilitator RESOLVING
depositToPositionManager Facilitator RESOLVING
withdrawFromPositionManager Facilitator RESOLVING
burnPositionManagerShares Facilitator RESOLVING
pause Pauser Any
unpause Pauser Any
Function Required Role Required State
setRepaid Owner Not yet repaid
authorizeMinting Owner or Consumer Before funding deadline
consume Owner or Consumer Before funding deadline
pullFunds Puller Funded
repay Puller Funds pulled
mint Authorized funder Before funding deadline
redeem PT/YT holder After repaid or repayment deadline
Function Required Role Required State
create Depositor EMPTY
cancel Depositor ACCEPTED
commit Depositor ACCEPTED
unlock Depositor UNLOCKING
recover Depositor RECOVERING
settle Settler PROCESSING
Function Required Role Required State
deposit Minter Not paused
withdraw Minter Not paused
burn Minter Not paused
rebalance Rebalancer Not paused
addBorrowModule Owner Any
removeBorrowModule Owner Any
setSupplyQueue Curator Any
setWithdrawalQueue Curator Any
setLtv Owner Any
setFeeData (recipient, base fee, performance fee) Owner Any
resetPerformanceReference Owner Any
setRebalanceConfig (max loss, cooldown) Owner Any
setTransferGuard Owner Any
Function Required Role Required State
supplyCollateral Owner Initialized
withdrawCollateral Owner Healthy after withdrawal
borrow Owner Healthy after borrow
repay Owner Has outstanding debt
preLiquidate Any LTV above the safe LTV (offer band) or above the liquidation LTV (proportional)
proposeOffer Registry Proposer or registry owner Any
revokeOffers Offer’s proposer, registry Guardian, or registry owner Offers live
Function Required Role Required State
startRetargetting Owner or Rebalancer Idle, position manager bound, fund whitelisted, within the principal cap
startSyncRetargetting Owner or Rebalancer Same, plus a whitelisted flash-loan module
consume Owner, Rebalancer or Consumer Active async operation, consumption window open
authorizeMinting Owner, Rebalancer or Consumer Same (revocation stays open once the window closes)
pullRequestFunds Owner or Rebalancer Active async operation
create / commit / unlock / cancelOrder / recoverOrder Owner or Rebalancer Active operation, order state permitting
rebalance Owner or Rebalancer Active operation; direction checks and value conservation
repay Owner or Rebalancer Active async operation, not already repaid
forceRepay Owner Active async operation
resolve Owner or Rebalancer Request repaid, no stored order, residual within tolerance
setConfig / setEstimates / setFund / setFlashLoanModule Owner Any
setPositionManager Owner No active operation
Function Required Role Required State
setProposer Owner Any
setGuardian Owner Any
setOfferTimelock Owner Value within 15 minutes to 7 days; effective after the current timelock
setMinOfferBonus Owner At most 1000 bps; effective immediately
Function Required Role Required State
setTokenConfig Owner Any
pause Owner or Pauser Any
unpause Owner or Pauser Any
setAddressStatus Owner or Compliance Any
setAddressStatusBatch Owner or Compliance Any

Roles are granted using Solady’s OwnableRoles pattern. Only the contract owner can grant and revoke roles:

// Grant the Facilitator role
uint256 FACILITATOR_ROLE = facility.FACILITATOR_ROLE();
facility.grantRoles(facilitatorAddress, FACILITATOR_ROLE);
// Grant multiple roles at once (roles are bitmasks)
uint256 roles = FACILITATOR_ROLE | PAUSER_ROLE;
facility.grantRoles(operatorAddress, roles);
// Revoke a role
facility.revokeRoles(operatorAddress, FACILITATOR_ROLE);
// Check if an address has a role
bool hasFacilitator = facility.hasAllRoles(operatorAddress, FACILITATOR_ROLE);

An address can renounce its own roles:

// Renounce your own roles
facility.renounceRoles(FACILITATOR_ROLE);