src/
├── facility/ # Core orchestration hub
│ ├── Facility.sol # Main entry point combining all modules
│ ├── IntentDescriptor.sol # ERC-6909 metadata provider
│ └── base/ # Abstract base contracts
│ ├── FacilityIntents.sol # Intent lifecycle management
│ ├── FacilityLP.sol # Liquidity provider operations
│ ├── FacilityFunds.sol # Fund order operations
│ ├── FacilityRequests.sol # Request contract coordination
│ ├── FacilityPositionManager.sol # Position manager integration
│ ├── FacilitySwap.sol # Token swapping
│ └── FacilityRoles.sol # Access control
├── request/ # Dual-token PT/YT vault system
│ ├── Request.sol # Main request contract
│ ├── RequestFactory.sol # Beacon proxy factory
│ ├── Vault.sol # ERC4626-style redemption vault
│ └── abstract/ # Base contracts and token controllers
├── manager/ # Multi-position aggregator
│ ├── PositionManager.sol # Main position manager
│ ├── PositionManagerFactory.sol # Beacon proxy factory
│ ├── base/ # Admin, fees, shares, rebalancing
│ └── rebalancer/ # Flash-loan based rebalancer
├── funds/ # External asset wrappers
│ ├── centrifuge/
│ │ ├── CentrifugeFund.sol # Centrifuge ERC-7540 integration
│ │ └── CentrifugeFundFactory.sol # Beacon proxy factory
│ ├── pareto/
│ │ ├── ParetoFund.sol # Pareto (Idle Finance) CDO integration
│ │ └── ParetoFundFactory.sol # Beacon proxy factory
│ ├── USCC/
│ │ ├── USCCFund.sol # Superstate USCC integration
│ │ ├── USCCFundFactory.sol # Beacon proxy factory
│ │ └── SuperstateRestrictedWrappedAsset.sol # WrappedAsset with Superstate allowlist
│ └── WrappedAsset.sol # Wrapper token (wUSCC, etc.) with virtual isAllowed hook
├── borrow/ # Lending protocol integrations
│ ├── MorphoBorrowPosition.sol # Morpho Blue position
│ └── MorphoBorrowPositionFactory.sol # Beacon proxy factory
├── guard/ # Compliance controls
│ ├── TransferGuard.sol # Multi-mode transfer guard (blocklist/whitelist/native)
│ └── TransferGuardFactory.sol # Beacon proxy factory
├── interfaces/ # All interface definitions
└── libs/ # Shared libraries
├── facility/ # Facility storage, intent, errors
├── manager/ # Position manager operations
├── request/ # Minting auth, token controller
└── borrow/ # Morpho shares math
The protocol consists of interconnected modules orchestrated through the Facility contract:
flowchart TB
subgraph Facility["Facility (Central Hub)"]
FI[FacilityIntents]
FLP[FacilityLP]
FF[FacilityFunds]
FR[FacilityRequests]
FPM[FacilityPositionManager]
FS[FacilitySwap]
end
subgraph External["External Integrations"]
Request[Request<br/>PT/YT Tokens]
PM[PositionManager<br/>Multi-Position Aggregator]
Fund[Fund<br/>Asset Wrapper]
BP[BorrowPosition<br/>Morpho Blue]
end
subgraph Compliance["Compliance"]
TG[TransferGuard]
end
LP((LPs)) --> FLP
FLP --> FI
FF --> Fund
FR --> Request
FPM --> PM
PM --> BP
TG -.-> PM
TG -.-> Facility
This section provides a consolidated view of all roles across contracts and how they connect in a typical deployment.
| Contract | Role | Typical Holder | Permissions |
|---|---|---|---|
| Facility | Owner | Protocol Admin | Create intents, update target asset, set descriptor |
| Facilitator | Operations Bot | Create intents, lock, resolve, set caps, set fund/request, all fund/request/PM/swap operations | |
| Guardian | Signers (EOA) | Sign swap authorizations (multi-sig for quorum) | |
| Compliance | Emergency Admin / Compliance Bot | Pause/unpause facility, revert deposits | |
| Request | Owner | Protocol Admin | Mark loan as repaid, authorize minting |
| Puller | Facility | Pull bridge loan funds, repay funds | |
| Consumer | Protocol Admin | Consume signed offers | |
| USCCFund | Depositor | Facility | Create/cancel/commit/unlock/recover orders |
| Operator | Operations Bot | Settle fund state after external operations | |
| CentrifugeFund | Owner | Protocol Admin | Cancel vault requests via cancelRequest() |
| Operator | Operations Bot | Cancel vault requests via cancelRequest() |
|
| Depositor | Facility | Create/cancel/commit/unlock/recover orders | |
| ParetoFund | Owner | Protocol Admin | Resolve stuck orders |
| Operator | Operations Bot | Resolve stuck orders | |
| Depositor | Facility | Create/cancel/commit/unlock orders | |
| PositionManager | Owner | Protocol Admin | Add modules, set LLTV, set fees |
| Minter | Facility | Deposit, withdraw, burn shares | |
| Curator | Operations Bot | Set supply/withdrawal queues | |
| Rebalancer | Rebalancer Contract | Execute rebalancing operations | |
| BorrowPosition | Owner | PositionManager | All borrow/supply operations |
| TransferGuard | Owner | Protocol Admin | Set token config (mode, collateral check), grant roles |
| Pauser | Emergency Admin | Pause/unpause tokens | |
| Compliance | Compliance Bot | Set address status (NONE/WHITELIST/BLOCKLIST/NATIVE) |
flowchart TB
subgraph Facility["Facility"]
direction TB
FO[Owner]
FF[Facilitator]
FG[Guardian]
FC[Compliance]
end
subgraph Users["Users"]
LP[LPs]
end
subgraph Request["Request Contract"]
RO[Owner: Admin]
RPuller[Puller: Facility]
end
subgraph Fund["Fund Contract"]
FDep[Depositor: Facility]
FSet[Operator: Bot]
end
subgraph PM["Position Manager"]
PMO[Owner: Admin]
PMM[Minter: Facility]
end
subgraph BP["Borrow Position"]
BPO[Owner: PositionManager]
end
%% Owner operations
FO -->|createIntent<br/>updateTarget| Facility
%% Facilitator operations
FF -->|lock, resolve<br/>setFund, setRequest<br/>fund/request/PM ops<br/>swap| Facility
%% Guardian operations
FG -->|sign swaps| Facility
%% Compliance operations
FC -->|pauseFor<br/>revertDeposit| Facility
%% User operations
LP -->|deposit<br/>withdraw<br/>claim| Facility
%% Facility to external contracts
Facility -->|pull/repay| RPuller
Facility -->|create/commit<br/>unlock/recover| FDep
Facility -->|deposit/withdraw<br/>burn| PMM
%% PM to Borrow Position
PMM --> BPO
Notes:
- Multiple Funds/Requests can be attached to a Facility (one per intent)
- Multiple Intents can share the same PositionManager
- Each Fund should only serve one intent to avoid conflicts
The Facility enforces strict state transitions for each intent:
stateDiagram-v2
[*] --> DEPOSITING: createIntent()
DEPOSITING --> RESOLVING: lock() or<br/>resolveStart reached
RESOLVING --> RESOLVED: resolve()
RESOLVED --> [*]
state DEPOSITING {
[*] --> dep_active
dep_active: Users can deposit/withdraw
note right of dep_active
Entry: createIntent()
Requirement: resolveStart > now
Allowed Operations:
• deposit() [any user]
• withdraw() [owner/operator]
• setDepositCap() [facilitator]
• setFund() [facilitator]
• setRequest() [facilitator]
• updateTarget() [owner]
end note
}
state RESOLVING {
[*] --> res_active
res_active: Facilitator executes operations
note right of res_active
Entry: lock() or resolveStart reached
Allowed Operations:
• setFund() [facilitator]
• setRequest() [facilitator]
• Fund: create/cancel/commit/unlock/recover
• Request: pull/repay
• PM: depositManager/withdrawManager/burnManager
• swap() [facilitator + guardian sigs]
• resolve() [facilitator]
end note
}
state RESOLVED {
[*] --> resolved_active
resolved_active: Users can claim
note right of resolved_active
Entry: resolve()
Requirements:
• No active Fund order
• Request marked repaid (if set)
Allowed Operations:
• claim() [owner/operator]
end note
}
| Function | Required Role | Required State | Additional Checks |
|---|---|---|---|
createIntent |
Owner/Facilitator | Any | resolveStart > now |
updateTarget |
Owner | DEPOSITING / RESOLVING | - |
setDepositCap |
Facilitator | DEPOSITING | - |
lock |
Facilitator | DEPOSITING | - |
setFund |
Facilitator | Any | No active order |
setRequest |
Facilitator | Any | Request repaid (if previously set) |
resolve |
Facilitator | RESOLVING | No active order, request repaid |
deposit |
Any | DEPOSITING | Within deposit cap |
withdraw |
Owner/Operator | DEPOSITING | Sufficient balance |
claim |
Owner/Operator | RESOLVED | Sufficient balance |
create (fund) |
Facilitator | RESOLVING | Fund set, no active order |
cancel (fund) |
Facilitator | RESOLVING | Active order exists |
commit (fund) |
Facilitator | RESOLVING | Active order exists |
unlock (fund) |
Facilitator | RESOLVING | Order in unlocking state |
recover (fund) |
Facilitator | RESOLVING | Order in recovering state |
pull (request) |
Facilitator | RESOLVING | Request set |
repay (request) |
Facilitator | RESOLVING | Request set |
depositManager |
Facilitator | RESOLVING | Asset is PositionManager |
withdrawManager |
Facilitator | RESOLVING | Asset is PositionManager |
burnManager |
Facilitator | RESOLVING | Asset is PositionManager |
swap |
Facilitator | RESOLVING | Valid signatures, quorum met |
revertDeposit |
Owner/Compliance | DEPOSITING | Deposit asset balance >= totalSupply; only owner can set receiver != from |
pauseFor |
Owner/Compliance | Any | duration=0 to unpause |
The Facility contract is the central orchestration hub that manages intents - configurable funding requests that coordinate deposits, fund operations, and claims.
Each intent tracks:
- Deposit Asset: The asset LPs deposit (can be a PositionManager)
- Target Asset: The target for fund operations (can be a PositionManager)
- Fund: Optional fund wrapper for external asset processing
- Request: Optional request contract for PT/YT issuance
- Guard Key: PositionManager used for transfer compliance checks
stateDiagram-v2
[*] --> DEPOSITING: createIntent()
DEPOSITING --> DEPOSITING: deposit() / withdraw()
DEPOSITING --> RESOLVING: lock() or resolveStart reached
RESOLVING --> RESOLVING: Fund/Request/PM operations
RESOLVING --> RESOLVED: resolve()
RESOLVED --> RESOLVED: claim()
RESOLVED --> [*]
note right of DEPOSITING
LPs deposit assets
Receive ERC-6909 LP tokens
end note
note right of RESOLVING
Facilitator executes operations
No LP deposits/withdrawals
end note
note right of RESOLVED
LPs claim proportional tokens
LP tokens are burned
end note
| Phase | Function | Description |
|---|---|---|
| Depositing | deposit(id, amount) |
Deposit asset, receive LP tokens 1:1 |
| Depositing | withdraw(id, from, receiver, amount) |
Burn LP tokens, receive asset 1:1 |
| Depositing | revertDeposit(id, from, receiver) |
Owner/Compliance force-withdraws a user's full deposit. Only owner can set receiver != from |
| Resolved | claim(id, from, receiver, shares) |
Burn LP tokens, receive proportional share of all accumulated tokens. Returns (tokens[], amounts[]) for easy tracking of claimed assets |
| Function | Description |
|---|---|
intentBalances(id) |
Returns all tokens and their balances held by an intent as parallel arrays (tokens[], amounts[]) |
getIntent(id) |
Returns the full intent properties and current state |
totalSupply(id) |
Returns total LP token supply for an intent |
The facilitator role can:
lock(id)- Force intent into resolving phasesetDepositCap(id, cap)- Update deposit capsetFund(id, fund)- Attach/detach fund wrappersetRequest(id, request)- Attach/detach request contractresolve(id)- Mark intent as resolved, enabling claims
| Role | Permission |
|---|---|
| Owner | Create intents, update target, set descriptor, revert deposits (custom receiver) |
| Facilitator | Lock, resolve, set caps, attach fund/request, execute operations |
| Compliance | Pause/unpause facility, revert deposits |
The Request contract implements a dual-token (PT/YT) funding mechanism for structured products.
When depositing into a request, funders receive:
- Principal Tokens (PT): Represent the deposited principal (1:1 with assets)
- Yield Tokens (YT): Represent expected yield (based on expectedReturn)
Example: Depositing 1,000,000 USDC with 10% expected return:
- Receive: 1,000,000 PT + 100,000 YT
principalAssets = min(totalAssets, ptSupply)
yieldAssets = totalAssets - principalAssets
pricePerPT = principalAssets / ptSupply
pricePerYT = yieldAssets / ytSupply
| Total Assets | Principal Assets | Yield Assets | PT Price | YT Price |
|---|---|---|---|---|
| 900,000 | 900,000 | 0 | 0.9 | 0 |
| 1,000,000 | 1,000,000 | 0 | 1.0 | 0 |
| 1,050,000 | 1,000,000 | 50,000 | 1.0 | 0.5 |
| 1,200,000 | 1,000,000 | 200,000 | 1.0 | 2.0 |
Key Properties:
- PT holders are prioritized (up to 1:1 redemption)
- YT holders capture any upside beyond principal
- If assets < principal, PT holders share the loss proportionally
flowchart TB
subgraph Funding["Funding Phase (canWithdraw = false)"]
direction TB
C1[consume offer] --> M1[Callback → Transfer → Mint PT/YT]
C2[authorizeMinting + mint] --> M2[Transfer → Mint PT/YT]
end
subgraph Utilization["Fund Utilization"]
P[pullFunds] --> U[Use funds]
U --> R[repay]
end
subgraph Redemption["Redemption Phase (canWithdraw = true)"]
SR[setRepaid / deadline] --> RD[PT/YT holders redeem]
end
Deploy[Factory deploys Request + PT + YT] --> Funding
Funding --> Utilization
Utilization --> Redemption
struct Offer {
address maker; // Funder receiving PT/YT
uint256 amount; // Reference principal
uint256 expectedReturn; // Expected yield
uint256 nonce; // Sequential (must be > stored)
uint256 expiration; // Validity deadline
bool useCallback; // Whether to call onRequestConsumed
}
// YT calculated proportionally
ytAmount = offer.expectedReturn * ptAmount / offer.amountCallback Interface (when useCallback = true):
interface IRequestCallback {
function onRequestConsumed(
Offer calldata offer,
bytes calldata signature,
uint256 principal, // PT amount being minted
uint256 yield // YT amount being minted
) external;
}The callback is invoked before funds are pulled, allowing the maker to:
- Withdraw from DeFi positions
- Move funds from internal accounting
- Set ERC20 allowances for the Request contract
// Owner authorizes
request.authorizeMinting(funder, 1_000_000e6, 100_000e6);
// Funder mints (after approving asset)
asset.approve(address(request), 1_000_000e6);
request.mint(); // Receives 1M PT + 100k YTAfter funding is complete:
- Pull Funds: Puller calls
pullFunds(amount, data)to transfer assets to themselves - Callback: If
data.length > 0, invokesonPullFunds(amount, data)on the puller - Utilization: Puller uses funds for intended purpose
- Repayment: Transfer assets back via
repay(amount)or direct transfer - Enable Redemptions: Owner calls
setRepaid(uint256 minBalance)or wait forrepaymentDeadline
Puller Callback Interface:
interface IRequestInteractionsCallback {
function onPullFunds(uint256 amount, bytes calldata data) external;
}The callback is invoked after funds are transferred, allowing automated strategies.
Nonces enable flexible offer lifecycle:
- Starting Value: Must start at 1 (nonce 0 is invalid)
- Soft Cancel: Coordinate off-chain to ignore offers
- Hard Cancel: Set nonce on-chain to invalidate all offers at or below that nonce
| Role | Permission |
|---|---|
| Owner | setRepaid, authorizeMinting, consume |
| Consumer | authorizeMinting, consume |
| Puller | pullFunds |
The fund module standardizes wrapping external assets through a state machine interface.
stateDiagram-v2
[*] --> EMPTY
EMPTY --> ACCEPTED: create()
EMPTY --> PENDING: create()
PENDING --> ACCEPTED: cleared
ACCEPTED --> EMPTY: cancel()
PENDING --> EMPTY: cancel()
ACCEPTED --> PROCESSING: commit()
PROCESSING --> UNLOCKING: success
PROCESSING --> RECOVERING: failure
UNLOCKING --> PROCESSING: partial unlock
RECOVERING --> PROCESSING: partial recover
UNLOCKING --> ENDED: unlock()
RECOVERING --> ENDED: recover()
ENDED --> [*]
| Mode | Input | Output |
|---|---|---|
| DEPOSIT | Asset (e.g., USDC) | Shares (e.g., wUSCC) |
| REDEEM | Shares (e.g., wUSCC) | Asset (e.g., USDC) |
USCCFund wraps Superstate USCC tokens with a wrapper token (wUSCC):
Deposit Flow (USDC → wUSCC):
create(DEPOSIT)- Initialize ordercommit()- Transfer USDC to Superstate recipientunlock()- Mint wUSCC to receiver once USCC is minted
Redeem Flow (wUSCC → USDC):
create(REDEEM)- Initialize ordercommit()- Burn wUSCC, trigger off-chain redemptionunlock()- Release USDC when settled (orrecover()if failed)
CentrifugeFund wraps Centrifuge ERC-7540 async vaults. Shares are represented by WrappedAsset tokens wrapping the vault's share token.
Key Design Decisions:
- Uses an internal state pattern: the stored
internalStatemay differ from whatstate()returns, becausestate()queries the Centrifuge vault for claimable amounts to detect async transitions (e.g., PROCESSING → UNLOCKING). - All vault calls use requestId = 0, the Centrifuge convention for "the current request for this controller" (each controller has at most one active request).
- Supports partial fills: Centrifuge processes requests across epochs, so
unlock()/recover()can be called multiple times, returning to PROCESSING between partial claims.
Deposit Flow (Asset → WrappedShare):
create(DEPOSIT)- Initialize order (validates slippage against current exchange rate)commit()- Pull assets, approve vault, callrequestDeposit()- Wait for Centrifuge epoch processing
unlock()- Claim shares viamint(), wrap into WrappedAsset, send to receiver
Redeem Flow (WrappedShare → Asset):
create(REDEEM)- Initialize ordercommit()- Burn WrappedAsset (unwrap), approve share tokens, callrequestRedeem()- Wait for Centrifuge epoch processing
unlock()- Claim assets viawithdraw(), send to receiver
Recovery Flow (cancel a pending request):
cancelRequest()- Owner/operator submits cancellation to Centrifuge vault- Wait for Centrifuge to process the cancellation
recover()- Claim returned assets/shares, send to receiver
ParetoFund wraps an IdleCDOEpochVariant (the Pareto/Idle credit vault). Shares are represented by WrappedAsset tokens wrapping the AA (senior) tranche token.
Key Design Decisions:
- Uses an internal state pattern (like Centrifuge): the stored
internalStatemay differ from whatstate()returns, becausestate()queries the CDO and its strategy to detect async transitions (e.g., PROCESSING → UNLOCKING). - Deposits are synchronous —
depositAA()succeeds or reverts atomically. No epoch wait is needed for deposits. - Withdrawals are epoch-gated —
requestWithdraw()queues a withdrawal that completes after the CDO epoch ends, thenclaimWithdrawRequest()delivers the underlying assets. - No recovery flow —
recover()always reverts withRecoverNotSupported(). Deposits are atomic (no stuck intermediate state) and withdrawals always complete after epoch processing. resolve()allows the operator/owner to override input/output amounts for an order stuck in PROCESSING when received amounts differ from expected values.
Deposit Flow (Asset → WrappedShare):
create(DEPOSIT)- Initialize order (validates Keyring wallet allowance)commit()- Pull assets, calldepositAA()atomically — AA tranche tokens are received immediatelyunlock()- Wrap AA tranche tokens into WrappedAsset, send to receiver
Redeem Flow (WrappedShare → Asset):
create(REDEEM)- Initialize ordercommit()- Burn WrappedAsset (unwrap to AA tranche), callrequestWithdraw()on CDO- Wait for CDO epoch to end
unlock()- CallclaimWithdrawRequest(), send underlying assets to receiver
The PositionManager aggregates multiple IBorrowPosition contracts into a single vault with ERC20 share-based accounting.
flowchart TB
PM[PositionManager<br/>ERC20 Shares]
subgraph Queues["Queues"]
SQ[Supply Queue<br/>position + maxBorrow]
WQ[Withdrawal Queue<br/>position addresses]
end
subgraph Positions["IBorrowPosition Pool"]
P1[MorphoPosition 1]
P2[MorphoPosition 2]
P3[MorphoPosition N]
end
PM --> SQ
PM --> WQ
SQ --> P1
SQ --> P2
WQ --> P1
WQ --> P2
WQ --> P3
Total Assets: Net value of all positions:
totalAssets = Σ(collateralQuoted) - Σ(debt)
Where collateralQuoted is collateral value in debt asset terms using each position's oracle.
Supply Queue: Ordered list of positions with borrow caps for deposits. Each entry contains:
position: The IBorrowPosition contract addressmaxBorrow: Maximum amount to borrow from this position per deposit
Withdrawal Queue: Ordered list of position addresses for withdrawals and burns.
Uses virtual offset to prevent inflation attacks (similar to ERC4626):
shares = assets × (totalSupply + 1e6) / (totalAssets + 1)
assets = shares × (totalAssets + 1) / (totalSupply + 1e6)
Deposits collateral and borrows debt across positions in the supply queue, respecting the position manager's target LTV.
function deposit(uint256 collateral, uint256 debt) external returns (int256 shares);Flow:
- Pull collateral from caller
- If
debt == 0: supply all collateral to first position - If
debt > 0: iterate through supply queue:- For each position, calculate the initial borrow:
min(availableLiquidity, maxBorrow, remainingDebt) - Query the position for required collateral at the target LTV via
collateralForBorrow(toBorrow, ltv) - If not enough collateral remains, reduce the borrow to what the remaining collateral supports via
borrowForCollateral(remainingCollateral, ltv) - Supply collateral and execute borrow
- For each position, calculate the initial borrow:
- Deposit any leftover collateral (not needed for borrowing) into the first supply queue position
- Transfer borrowed debt to caller
- Mint/burn shares based on total assets change
LTV Enforcement: Each position is individually constrained to the target LTV. The collateralForBorrow and borrowForCollateral functions account for the position's existing collateral and debt, so positions with excess collateral may not need additional collateral for new borrows.
Example:
Supply Queue: [(PositionA, maxBorrow=1000), (PositionB, maxBorrow=2000)]
Deposit: collateral=1500, debt=2000, ltv=70%
Position A: available=800 → collateralForBorrow(800, 0.7) = 1143 → supplies 1143, borrows 800
Position B: remaining collateral=357 → borrowForCollateral(357, 0.7) = 250 → supplies 357, borrows 250
Remaining debt = 2000 - 800 - 250 = 950 → reverts InsufficientBorrowCapacity
With enough collateral (e.g., 3000):
Position A: borrows 800, needs 1143 collateral
Position B: borrows 1200, needs 1714 collateral → total 2857, leftover 143 → first position
Withdraws collateral and repays debt across positions in the withdrawal queue.
function withdraw(uint256 collateral, uint256 debt) external returns (int256 shares);Flow:
- Pull debt from caller for repayment
- First pass - Repay debt through withdrawal queue
- Second pass - Withdraw collateral through withdrawal queue (respects available collateral at LLTV)
- Transfer collateral to caller
- Mint/burn shares based on total assets change
Available Collateral:
availableCollateral = totalCollateral - requiredCollateral
requiredCollateral = debt × ORACLE_PRICE_SCALE / (lltv × collateralPrice)
Only "available" collateral can be withdrawn without repaying debt, ensuring positions remain healthy.
Burns shares to exit proportionally, maintaining average LTV across all positions.
function burn(uint256 shares) external returns (uint256 collateral, uint256 debt);Flow:
- Calculate proportional amounts:
collateral = totalCollateral × shares / totalSupply (round down) debt = totalDebt × shares / totalSupply (round up) - Burn shares from caller
- Pull debt from caller for repayment
- Process through withdrawal queue proportionally
- Transfer collateral to caller
The rebalance function allows redistributing collateral and debt across positions without affecting shares.
Position Validation: All positions referenced in rebalancing operations must be registered in the borrowModules set (added via addBorrowModule). Attempting to rebalance with unregistered positions reverts with UnauthorizedPosition().
struct RebalancingData {
uint256 collateral; // Collateral to pull from caller
uint256 debt; // Debt to pull from caller
RebalancingOperation[] operations;
}
struct RebalancingOperation {
address position;
RebalancingOperationType operationType; // REPAY, WITHDRAW, BORROW, SUPPLY
uint256 amount;
}Example - Move liquidity from Position A to Position B:
RebalancingData({
collateral: 0,
debt: 1000, // Need debt token to repay on A
operations: [
(positionA, REPAY, 1000),
(positionA, WITHDRAW, 2000),
(positionB, SUPPLY, 2000),
(positionB, BORROW, 1000)
]
})
// Returns excess collateral and debt to callerFees are accrued before every operation:
Management Fee: Annual fee on the aggregate collateral of non-bad-debt positions (basis points/year)
managementFeeAssets = currentCollat × managementFee × elapsedTime / (BPS × SECONDS_PER_YEAR)
where currentCollat is the sum of quoted collateral across positions whose collateral covers
their debt. The resulting fee assets are capped at totalAssets so the post-fee base stays
non-negative. For an unlevered vault this matches the prior NAV-based basis exactly.
Performance Fee: Fee on the performance of the levered slice only (basis points). The basis is
LTV_prev · Δcollat - Δdebt where LTV_prev = lastDebt / lastCollat is the LTV at the previous
snapshot. Algebraically this simplifies to:
basis = mulDivUp(lastDebt, currentCollat, lastCollat) - currentDebt
performanceFeeAssets = max(0, basis - managementFeeAssets) × performanceFee / BPS
where lastCollat = lastTotalAssets + lastDebt and currentCollat = currentTotalAssets + currentDebt.
Anchoring on LTV_prev rather than the live LTV defines the unlevered baseline at the start of the
period (the natural comparison for "extra return from leverage") and fixes the multiplier at snapshot
time. mulDivUp biases the basis slightly larger, consistent with the conservative-to-protocol
rounding used elsewhere.
lastDebt == 0 is the bootstrap sentinel: the first accrual after deployment (or after upgrading
from v1.1.0 of the contracts) skips the performance fee and seeds lastDebt from the current debt.
Subsequent accruals charge the new basis normally.
Fees are minted as shares to the fee recipient, diluting existing shareholders.
| Role | Permission |
|---|---|
| Owner | Add/remove modules, set LLTV, set fees, set max rebalance loss |
| Minter | deposit, withdraw, burn |
| Curator | Set supply/withdrawal queues |
| Rebalancer | Execute rebalancing operations |
The Position Manager supports an optional TransferGuard for compliance controls. When set:
- All share transfers are validated through the guard
- Deposits/withdrawals are blocked when paused
- Rebalancing operations revert with
Paused()
A standalone rebalancer using Morpho flash loans:
flowchart LR
Owner -->|1. rebalance| MR[MorphoRebalancer]
MR -->|2. flash loan| Morpho
Morpho -->|3. callback| MR
MR -->|4. rebalance| PM[PositionManager]
MR -->|5. repay| Morpho
Requires PM_ROLE_REBALANCER on the Position Manager.
Individual position wrapper for Morpho Blue with custom pre-liquidation:
flowchart LR
subgraph MorphoBorrowPosition
C[Collateral]
D[Debt]
L[Custom LLTV]
end
MorphoBorrowPosition --> Morpho[Morpho Blue Market]
Key Features:
- Custom LLTV per position (immutable after init, must be > 0 and ≤ market LLTV)
- Proportional pre-liquidation mechanism
- ERC-7201 namespaced storage for proxy compatibility
Initialization:
function initialize(
IMorpho morpho, // Morpho Blue protocol contract
Id marketId, // Morpho market ID
address positionManager, // Owner controlling this position
uint256 lltv // Custom LLTV (immutable)
) external;| Function | Description |
|---|---|
supplyCollateral(amount) |
Add collateral to increase borrowing capacity |
withdrawCollateral(amount) |
Remove collateral (enforces custom LLTV) |
borrow(amount) |
Borrow against collateral (enforces custom LLTV) |
repay(amount) |
Repay borrowed assets |
preLiquidate(...) |
Liquidate unhealthy positions |
| Function | Description |
|---|---|
totalBorrowed() |
Current debt including accrued interest |
totalCollateral() |
Current collateral amount in position |
totalCollateralQuoted() |
Collateral value in debt asset terms (using oracle) |
isHealthy(lltv) |
Whether position is above specified LLTV |
maxBorrow(lltv) |
Maximum borrowable at given LLTV |
availableLiquidity() |
Available liquidity in market |
availableCollateral(lltv) |
Withdrawable collateral while maintaining health |
collateralForBorrow(amount, ltv) |
Additional collateral needed to borrow amount at ltv |
borrowForCollateral(amount, ltv) |
Additional borrow capacity from supplying amount at ltv |
Position health is determined by:
collateralValue = collateral × oraclePrice / ORACLE_PRICE_SCALE
maxBorrow = collateralValue × lltv
isHealthy(lltv) = maxBorrow ≥ totalBorrowed
Unlike Morpho's native liquidation (with liquidation incentive factor), MorphoBorrowPosition uses proportional pre-liquidation - liquidators receive collateral proportional to debt repaid.
Liquidation Bonus Formula:
Liquidation Bonus = 1 - LTV (at liquidation time)
Example at 80% LTV with 100 collateral ($100) and 80 debt ($80):
Liquidating 50% of debt ($40) seizes 50% of collateral ($50):
- Liquidator pays: $40 (debt)
- Liquidator receives: $50 (collateral)
- Profit: $10 = 20% bonus (1 - 0.80)
Key Properties:
- Liquidators receive proportional share of collateral
- Bonus scales with how underwater the position is
- No cap on seized collateral
function preLiquidate(
address borrower, // The MorphoBorrowPosition address
uint256 seizedAssets, // Collateral to seize (0 to calculate from repaidShares)
uint256 repaidShares, // Debt shares to repay (0 to calculate from seizedAssets)
bytes calldata data // Callback data (empty for no callback)
) external returns (uint256 seizedAssets, uint256 repaidAssets);Input Options:
seizedAssets > 0, repaidShares = 0→ specify collateral amountseizedAssets = 0, repaidShares > 0→ specify debt shares- Both non-zero or both zero → reverts with
InconsistentInput
Callback Interface:
interface IPreLiquidationCallback {
function onPreLiquidate(uint256 repaidAssets, bytes calldata data) external;
}Invoked (if data non-empty) after collateral transfer but before debt is pulled.
Deploys positions using beacon proxy pattern:
address bp = factory.createBorrowPosition(
morpho, // IMorpho contract
marketId, // Morpho market ID
positionManager, // Owner address
0.72e18 // Custom LLTV (72%)
);Monitor BorrowPositionCreated events to track deployments and their LLTV thresholds.
Compliance controls for token transfers with four modes and collateral-layer allowlist delegation.
Each mode is a combination of two properties: whether NONE status addresses are blocked, and whether at least one NATIVE party is required.
| Mode | Behavior |
|---|---|
BLOCKLIST |
All addresses allowed EXCEPT those with BLOCKLIST status (default) |
WHITELIST |
Only WHITELIST or NATIVE addresses allowed |
NATIVE_ONLY |
At least one party must be NATIVE, no BLOCKLIST allowed. Mints/burns bypass NATIVE requirement |
NATIVE_WHITELIST |
All parties must be WHITELIST/NATIVE, at least one NATIVE. Mints/burns bypass NATIVE requirement |
| Status | BLOCKLIST Mode | WHITELIST Mode | NATIVE_ONLY Mode | NATIVE_WHITELIST Mode |
|---|---|---|---|---|
NONE |
Allowed | Blocked | Allowed (but not NATIVE) | Blocked |
WHITELIST |
Allowed | Allowed | Allowed (but not NATIVE) | Allowed |
BLOCKLIST |
Blocked | Blocked | Blocked | Blocked |
NATIVE |
Allowed | Allowed | Allowed + satisfies NATIVE req | Allowed + satisfies NATIVE req |
When checkCollateralAllowed is enabled on a token's config, the guard additionally calls PositionManager(token).assets() to get the collateral asset, then calls WrappedAsset(collateral).isAllowed(account, amount) for each non-null party. This delegates compliance enforcement (e.g., Superstate allowlist) to the WrappedAsset layer.
flowchart TB
Start[canTransfer] --> Paused{Token Paused?}
Paused -->|Yes| Block[BLOCK]
Paused -->|No| BL{BLOCKLIST?}
BL -->|Either party| Block
BL -->|No| Mode{Token Mode}
Mode -->|BLOCKLIST| Allow[ALLOW]
Mode -->|WHITELIST| WL{NONE status?}
WL -->|Yes| Block
WL -->|No| Allow
Mode -->|NATIVE_ONLY| NO{At least one NATIVE?}
NO -->|No, regular transfer| Block
NO -->|Yes or mint/burn| Allow
Mode -->|NATIVE_WHITELIST| NW{All WHITELIST/NATIVE?}
NW -->|No| Block
NW -->|Yes| NW2{At least one NATIVE?}
NW2 -->|No, regular transfer| Block
NW2 -->|Yes or mint/burn| Collateral
Allow --> Collateral{checkCollateral?}
Collateral -->|No| Pass[ALLOW]
Collateral -->|Yes| CA{WrappedAsset.isAllowed?}
CA -->|Yes| Pass
CA -->|No| Block
| Role | Permission |
|---|---|
| Owner | Set token config (paused, mode, checkCollateralAllowed), grant roles |
| Pauser | Pause/unpause tokens |
| Compliance | Set address statuses |
// Deploy guard via factory
TransferGuardFactory factory = new TransferGuardFactory(beaconOwner);
address guard = factory.createTransferGuard(guardOwner);
// Configure (NATIVE_ONLY mode with collateral check)
TransferGuard(guard).setTokenConfig(
address(positionManager), false, TokenMode.NATIVE_ONLY, true
);
// Set address statuses
TransferGuard(guard).setAddressStatus(facility, AddressStatus.NATIVE);
TransferGuard(guard).setAddressStatus(blockedUser, AddressStatus.BLOCKLIST);
TransferGuard(guard).setAddressStatus(allowedUser, AddressStatus.WHITELIST);
// Batch updates
address[] memory accounts = new address[](2);
accounts[0] = user1;
accounts[1] = user2;
TransferGuard(guard).setAddressStatusBatch(accounts, AddressStatus.WHITELIST);
// Connect to Position Manager
positionManager.setTransferGuard(guard);All major contracts use the beacon proxy pattern for gas-efficient, upgradeable deployments:
flowchart TB
Factory[Factory Contract]
Beacon[UpgradeableBeacon]
Impl[Implementation]
Factory -->|owns| Beacon
Beacon -->|points to| Impl
subgraph Proxies["Deployed Proxies"]
P1[Proxy 1]
P2[Proxy 2]
P3[Proxy N]
end
Factory -->|creates| P1
Factory -->|creates| P2
Factory -->|creates| P3
P1 -->|delegates to| Beacon
P2 -->|delegates to| Beacon
P3 -->|delegates to| Beacon
Factories:
RequestFactory- Deploys Request + PT/YT vaultsPositionManagerFactory- Deploys PositionManager instancesMorphoBorrowPositionFactory- Deploys borrow positionsUSCCFundFactory- Deploys USCC fund wrappersCentrifugeFundFactory- Deploys Centrifuge ERC-7540 fund wrappersParetoFundFactory- Deploys Pareto CDO fund wrappersTransferGuardFactory- Deploys transfer guards
Upgrading: The beacon owner can upgrade all proxies by updating the beacon's implementation.
| Mechanism | Purpose |
|---|---|
| Virtual Share Offset | Prevents first-depositor inflation attacks in PositionManager |
| Conservative Rounding | Debt rounds up, collateral rounds down to protect vaults |
| LTV Enforcement | Deposits and withdrawals respect the target LTV per position |
| 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 |
Centralization Risks:
- Guard owner can blocklist any address (use multisig/timelock for production)
- Beacon owner can upgrade all proxy implementations
- Facilitator has broad operational control over intent lifecycle
