Loading…
Contract
Native balance
0 tZROAR
Live from the node · no monetary value
Activity
8 transactions
First seen block 16,963 · last · nonce 1
Contract
Verified — partial match
The compiled bytecode matches the deployed bytecode except for the metadata hash (comments or file paths may differ).0x0000000000000000000000008fd1accb358c6f2551c6ca0f590b709c0653e0e4000000000000000000000000de8fc3b73e75987a7ff93300dc0b8abb6c26d4b1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {IYieldStrategy} from "./IYieldStrategy.sol";
import {IPrincipalProtected} from "./IPrincipalProtected.sol";
import {IMortgageLoan} from "./interfaces/IMortgageLoan.sol";
/**
* @title CollateralVault
* @notice Holds pledged USDC collateral for a mortgage on Base.
*
* @dev The borrower's collateral substitutes for a cash down payment. Unlike
* cash, it is PLEDGED rather than spent, so it stays the borrower's
* property and comes back at payoff.
*
* Deploying collateral into a yield market is opt-in per borrower and off
* by default. That is not a UI preference — depositing someone's
* collateral into a lending market puts it at smart-contract and
* counterparty risk, so it requires their explicit on-chain consent, which
* this contract records. An administrator cannot deploy on a borrower's
* behalf without it.
*/
contract CollateralVault is AccessControl, ReentrancyGuard, Pausable {
using SafeERC20 for IERC20;
bytes32 public constant SERVICER_ROLE = keccak256("SERVICER_ROLE");
bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");
IERC20 public immutable usdc;
/// @notice Lending adapter. Unset means no deployment is possible at all.
IYieldStrategy public yieldStrategy;
/**
* @notice The loan book this vault's collateral secures.
*
* @dev Needed only so a borrower can settle their own loan from their own
* collateral. Unset means {payoffLoanFromCollateral} is unavailable;
* every other function is unaffected.
*/
IMortgageLoan public mortgageVault;
/**
* @notice Performance fee on realized yield, in basis points.
*
* @dev VaultToHome is paid out of what the borrower's collateral EARNS, not
* out of how long their mortgage lasts. That choice is the whole
* commercial model and it is why there is no early-payoff fee anywhere
* in this contract: a charge for repaying principal before it is due is
* a prepayment penalty, and this product's entire purpose is to make
* that repayment happen sooner.
*
* Charged only when yield is realized to the borrower or to their
* principal. Never on pledged collateral, and never on a hardship
* draw — see {coverPaymentFromYield}.
*
* Zero by default. A fee is a decision an operator must make
* explicitly and publish, not something a deployment carries silently.
*/
uint256 public performanceFeeBps;
/// @notice Hard ceiling on {performanceFeeBps}, so it cannot be widened away.
uint256 public constant MAX_PERFORMANCE_FEE_BPS = 2000; // 20%
/// @notice Where performance fees are sent. Required before any fee applies.
address public feeRecipient;
struct Position {
address borrower;
uint256 pledged; // principal pledged by the borrower
uint256 deployed; // principal supplied to the yield strategy
uint256 yieldApplied; // yield already directed to principal
/**
* This loan's claim on the pooled strategy.
*
* The strategy pools every borrower's money, so `deployed` alone cannot
* say who earned what: if one loan supplies 10,000 and another 90,000
* and the pool gains 5,000, nothing in a plain balance distinguishes
* their claims, and yield could be credited to whichever loan a
* servicer named. Shares are minted at the prevailing value on deposit
* and burned on recall, so gains and losses land pro-rata by
* construction rather than by instruction.
*/
uint256 yieldShares;
/**
* Highest yield this loan has ever been shown to have earned.
*
* Live share value alone is not a usable entitlement: recalling
* principal or harvesting gains moves value out of the strategy, and
* the loan's live claim drops to zero along with it. The earnings were
* real and the borrower is still owed them, so the peak is recorded and
* entitlement is measured against that, less whatever has been paid.
*/
uint256 yieldHighWater;
/**
* Hardship support.
*
* A borrower in difficulty can have their earned yield cover monthly
* payments until they recover, rather than falling behind while money
* they own sits in the vault. Drawing is bounded by the same
* entitlement as every other payout, so hardship can never reach
* pledged principal — the point is to help them keep the home, not to
* quietly spend the collateral securing it.
*/
bool hardshipActive;
uint256 hardshipDraws;
uint256 hardshipDrawn;
bool yieldConsent; // borrower's explicit consent to deploy
bool locked; // pledged and securing a live loan
bool exists;
}
mapping(bytes32 => Position) private _positions;
/// @notice Total pledged across all positions, excluding yield.
uint256 public totalPledged;
/// @notice Shares outstanding against the pooled yield strategy.
uint256 public totalYieldShares;
/// @notice Principal currently out in the yield strategy, across all loans.
uint256 public totalDeployed;
/**
* @notice Most monthly payments hardship support may cover for one loan.
* @dev Matches the servicing workout window. A cap exists because yield
* covering payments indefinitely hides a loan that is not affordable,
* which helps nobody.
*/
uint256 public maxHardshipDraws = 6;
/**
* @notice Addresses yield may be paid to — the loan servicers that reduce a
* borrower's balance when they receive it.
*
* @dev An allowlist, not a free parameter. `applyYieldToPrincipal` used to
* take any address the caller supplied, so a servicer could direct a
* borrower's earnings to a wallet of their choosing and the event would
* faithfully record it going there. A borrower cannot audit that.
*/
mapping(address => bool) public approvedYieldDestinations;
/**
* @dev Scales the first deposit so early rounding cannot let one loan's
* share price drift measurably from another's.
*/
uint256 private constant SHARE_PRECISION = 1e12;
event CollateralPledged(bytes32 indexed loanId, address indexed borrower, uint256 amount);
event CollateralReleased(bytes32 indexed loanId, address indexed borrower, uint256 amount);
event YieldConsentSet(bytes32 indexed loanId, address indexed borrower, bool consented);
event CollateralDeployed(bytes32 indexed loanId, uint256 amount, string market);
event CollateralRecalled(bytes32 indexed loanId, uint256 amount);
event YieldAppliedToPrincipal(
bytes32 indexed loanId,
address indexed borrower,
uint256 amount,
address indexed servicer
);
event YieldWithdrawnByBorrower(bytes32 indexed loanId, address indexed borrower, uint256 amount);
event HardshipDeclared(bytes32 indexed loanId, address indexed borrower);
event HardshipEnded(bytes32 indexed loanId, address indexed borrower, uint256 drawsUsed);
event HardshipPaymentCovered(
bytes32 indexed loanId,
address indexed borrower,
uint256 amount,
uint256 drawNumber,
address indexed servicer
);
event MaxHardshipDrawsChanged(uint256 draws);
event YieldDestinationApprovalChanged(address indexed destination, bool approved);
event YieldStrategyChanged(address indexed strategy, string market);
event MortgageVaultChanged(address indexed mortgageVault);
event PerformanceFeeChanged(uint256 bps, address indexed recipient);
event PerformanceFeeCharged(
bytes32 indexed loanId,
address indexed borrower,
uint256 grossYield,
uint256 fee,
address indexed recipient
);
event LoanPaidFromCollateral(
bytes32 indexed loanId,
address indexed borrower,
uint256 amountApplied,
uint256 remainingPledged
);
error PositionExists();
error PositionNotFound();
error ZeroAmount();
error ZeroAddress();
error NotBorrower();
error CollateralLocked();
error NoYieldConsent();
error NoStrategySet();
/// @dev The strategy does not declare principal protection, or declares none.
error StrategyNotPrincipalProtected(address strategy);
/// @dev The strategy reports principal already exposed to market risk.
error StrategyHasPrincipalAtRisk(address strategy, uint256 atRisk);
/// @dev Yield may only be sent to an approved servicer destination.
error DestinationNotApproved(address destination);
/// @dev More was applied than this loan actually earned.
error ExceedsLoanEarnings(bytes32 loanId, uint256 requested, uint256 earned);
/// @dev Hardship support was drawn without hardship being active.
error HardshipNotActive(bytes32 loanId);
error HardshipAlreadyActive(bytes32 loanId);
/// @dev The loan has used its full hardship allowance.
error HardshipDrawsExhausted(bytes32 loanId, uint256 used, uint256 allowed);
error InsufficientAvailable(uint256 requested, uint256 available);
error NoMortgageVault();
error LoanAlreadySettled(bytes32 loanId);
error FeeOutOfRange(uint256 requested, uint256 maximum);
constructor(address usdcAddress, address admin) {
if (usdcAddress == address(0) || admin == address(0)) revert ZeroAddress();
usdc = IERC20(usdcAddress);
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(GUARDIAN_ROLE, admin);
}
// ------------------------------------------------------------- pledge
/// @notice Pledges USDC as collateral for a loan.
function pledgeCollateral(bytes32 loanId, uint256 amount)
external
nonReentrant
whenNotPaused
{
if (amount == 0) revert ZeroAmount();
Position storage p = _positions[loanId];
if (p.exists && p.borrower != msg.sender) revert NotBorrower();
uint256 before = usdc.balanceOf(address(this));
usdc.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = usdc.balanceOf(address(this)) - before;
if (!p.exists) {
p.borrower = msg.sender;
p.exists = true;
}
p.pledged += received;
totalPledged += received;
emit CollateralPledged(loanId, msg.sender, received);
}
/// @notice Marks collateral as securing a live loan. Blocks withdrawal.
function lock(bytes32 loanId) external onlyRole(SERVICER_ROLE) {
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
p.locked = true;
}
/// @notice Releases the lock at payoff, or when the agreement permits.
function unlock(bytes32 loanId) external onlyRole(SERVICER_ROLE) {
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
p.locked = false;
}
/**
* @notice Withdraws released collateral back to the borrower.
* @dev Only the borrower can call this, and only once the servicer has
* unlocked the position. Deployed funds must be recalled first, so a
* withdrawal can never be blocked by money sitting in a lending market
* without that being visible.
*/
function withdrawCollateral(bytes32 loanId, uint256 amount)
external
nonReentrant
{
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (p.borrower != msg.sender) revert NotBorrower();
if (p.locked) revert CollateralLocked();
uint256 available = p.pledged - p.deployed;
if (amount == 0) revert ZeroAmount();
if (amount > available) revert InsufficientAvailable(amount, available);
p.pledged -= amount;
totalPledged -= amount;
usdc.safeTransfer(p.borrower, amount);
emit CollateralReleased(loanId, p.borrower, amount);
}
/**
* @notice Settles the borrower's own loan out of their own collateral.
*
* @dev THE EXIT THIS PRODUCT IS FOR. Collateral is pledged rather than
* spent, so as the balance falls and the collateral compounds the two
* converge. Once they meet, a borrower should be able to finish the
* mortgage with the asset that was standing behind it all along —
* without requalifying, without an appraisal, and without depending on
* what rates happen to be that year. For a loan carrying a balloon
* that turns the balloon from an event into a choice.
*
* NO FEE IS CHARGED HERE, and none can be added: a charge for repaying
* principal early is a prepayment penalty however it is labelled. This
* contract is paid from yield instead (see {performanceFeeBps}), which
* is why the two interests point the same way — the platform earns more
* when the collateral earns more, not when the borrower pays longer.
*
* BORROWER-SIGNED ONLY. No servicer, role or admin path reaches this
* function, because it moves a borrower's collateral. The lock that
* secures the loan is deliberately NOT checked: the funds are going to
* extinguish the very debt that lock exists to secure, so refusing on
* that basis would be refusing the loan's own repayment.
*
* Only idle collateral is spendable. Anything sitting in the yield
* strategy must be recalled first, so settlement can never be blocked
* by, or silently unwind, a market position.
*
* The amount actually applied is MEASURED from this contract's balance
* rather than assumed: MortgageVault refunds anything beyond the payoff
* figure to its caller, which is this vault, and that refund stays
* pledged as the borrower's collateral rather than being lost.
*
* `maxAmount` is a hard ceiling and is never exceeded, not even to
* round a loan to zero. That matters because a payoff quote is stale
* the moment it is read — interest accrues by time, so the figure has
* already grown by the next block — and a borrower who sends exactly
* the quote closes all but a few units of the debt. Paper mortgages
* have the same problem and solve it the same way: the payoff
* statement carries a per-diem and the borrower sends more than the
* quote, with the excess refunded. {payoffFromCollateralAvailable}
* returns that already-margined figure as `recommendedAmount`, and
* anything unused comes straight back into the pledge.
*
* Second-guessing the ceiling here instead would mean spending more of
* a borrower's collateral than they authorised, which is not a trade
* worth making to save them one extra call.
*
* @param loanId the loan to settle
* @param maxAmount the most the borrower is willing to have applied. Pass
* `recommendedAmount` to close the loan outright; pass less
* to make a partial reduction.
*/
function payoffLoanFromCollateral(bytes32 loanId, uint256 maxAmount)
external
nonReentrant
returns (uint256 applied)
{
if (maxAmount == 0) revert ZeroAmount();
if (address(mortgageVault) == address(0)) revert NoMortgageVault();
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (p.borrower != msg.sender) revert NotBorrower();
uint256 payoff = mortgageVault.payoffAmount(loanId);
if (payoff == 0) revert LoanAlreadySettled(loanId);
uint256 idle = p.pledged - p.deployed;
// What the borrower has actually committed to spending, and what must
// therefore be affordable out of idle collateral.
uint256 intended = maxAmount < payoff ? maxAmount : payoff;
if (intended > idle) revert InsufficientAvailable(intended, idle);
// Approve exactly what is being spent, and only for the duration of
// this call. forceApprove tolerates tokens that refuse a non-zero
// rewrite; the reset afterwards leaves no standing allowance over
// borrower collateral.
uint256 before = usdc.balanceOf(address(this));
usdc.forceApprove(address(mortgageVault), intended);
mortgageVault.makePayment(loanId, intended);
usdc.forceApprove(address(mortgageVault), 0);
applied = before - usdc.balanceOf(address(this));
p.pledged -= applied;
totalPledged -= applied;
emit LoanPaidFromCollateral(loanId, p.borrower, applied, p.pledged);
}
/**
* @notice What a borrower could settle right now from idle collateral.
*
* @dev Reported as three separate numbers rather than one verdict, because
* "not yet" has two different causes and they call for different
* actions: not enough collateral, or enough collateral sitting in the
* yield strategy and needing recall first.
*/
function payoffFromCollateralAvailable(bytes32 loanId)
external
view
returns (
uint256 payoff,
uint256 recommendedAmount,
uint256 idleCollateral,
uint256 deployedCollateral,
bool canSettleInFull
)
{
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
payoff = address(mortgageVault) == address(0) ? 0 : mortgageVault.payoffAmount(loanId);
idleCollateral = p.pledged - p.deployed;
deployedCollateral = p.deployed;
// The payoff figure plus a per-diem margin — 10 bps, roughly five days
// of interest at a typical rate — so the settling transaction still
// covers the debt after it has accrued a little further. Capped at what
// is actually there, and the unused part is refunded either way.
if (payoff > 0) {
// Integer division rounds a small balance's margin to nothing, which
// would quote a figure that cannot close the loan it is quoting for.
// The margin is therefore never less than one unit.
uint256 margin = payoff / 1000;
if (margin == 0) margin = 1;
uint256 margined = payoff + margin;
recommendedAmount = margined > idleCollateral ? idleCollateral : margined;
}
canSettleInFull = payoff > 0 && idleCollateral >= payoff;
}
// -------------------------------------------------------------- yield
/**
* @notice Records the borrower's consent to deploy their collateral.
* @dev Only the borrower may call this. Consent is required before any
* deployment and can be withdrawn at any time; withdrawing it does not
* by itself recall funds, so the servicer must call recall().
*/
function setYieldConsent(bytes32 loanId, bool consented) external {
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (p.borrower != msg.sender) revert NotBorrower();
p.yieldConsent = consented;
emit YieldConsentSet(loanId, msg.sender, consented);
}
/// @notice Supplies part of a position's collateral to the yield market.
function deployToYield(bytes32 loanId, uint256 amount)
external
onlyRole(SERVICER_ROLE)
nonReentrant
whenNotPaused
{
if (address(yieldStrategy) == address(0)) revert NoStrategySet();
if (amount == 0) revert ZeroAmount();
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
// The borrower's consent is checked on-chain, not assumed upstream.
if (!p.yieldConsent) revert NoYieldConsent();
uint256 available = p.pledged - p.deployed;
if (amount > available) revert InsufficientAvailable(amount, available);
// Shares are priced BEFORE the deposit lands, so this loan buys in at
// the value the pool had, not one inflated by its own money.
uint256 sharesMinted = _sharesForDeposit(amount);
p.deployed += amount;
p.yieldShares += sharesMinted;
totalYieldShares += sharesMinted;
totalDeployed += amount;
usdc.forceApprove(address(yieldStrategy), amount);
yieldStrategy.deposit(amount);
emit CollateralDeployed(loanId, amount, yieldStrategy.marketName());
}
/// @notice Pulls collateral back out of the yield market.
function recallFromYield(bytes32 loanId, uint256 amount)
external
onlyRole(SERVICER_ROLE)
nonReentrant
{
if (address(yieldStrategy) == address(0)) revert NoStrategySet();
if (amount == 0) revert ZeroAmount();
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (amount > p.deployed) revert InsufficientAvailable(amount, p.deployed);
// Capture what this loan has earned before the recall removes it from
// view; the entitlement must survive the withdrawal.
_recordYieldHighWater(loanId);
// Burn shares worth exactly the USDC being withdrawn — the inverse of
// how they were minted.
//
// Burning in proportion to PRINCIPAL instead would silently transfer
// value between borrowers: recalling half the principal from a position
// that had gained would burn half the shares, and those shares are worth
// more than half the cash taken out. The difference would be left behind
// for the other holders. Priced against pool value, a withdrawal takes
// out precisely what it is worth.
uint256 poolValue = yieldStrategy.totalValue();
uint256 sharesBurned = poolValue == 0
? p.yieldShares
: (amount * totalYieldShares) / poolValue;
if (sharesBurned > p.yieldShares) sharesBurned = p.yieldShares;
p.deployed -= amount;
p.yieldShares -= sharesBurned;
totalYieldShares -= sharesBurned;
totalDeployed = totalDeployed > amount ? totalDeployed - amount : 0;
yieldStrategy.withdraw(amount);
emit CollateralRecalled(loanId, amount);
}
/**
* @notice Sends realized yield to the servicer to apply to principal.
* @dev Only yield is movable this way. `pledged` is never touched, so this
* path cannot be used to take a borrower's collateral.
*/
function applyYieldToPrincipal(bytes32 loanId, uint256 amount, address servicer)
external
onlyRole(SERVICER_ROLE)
nonReentrant
{
if (amount == 0) revert ZeroAmount();
if (servicer == address(0)) revert ZeroAddress();
// Where it goes is constrained, so a borrower can verify the
// destination against a published list rather than trusting the caller.
if (!approvedYieldDestinations[servicer]) revert DestinationNotApproved(servicer);
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (!p.yieldConsent) revert NoYieldConsent();
// Bounded by what THIS loan earned, not just by the vault's surplus.
// The surplus is pooled across every borrower, so checking it alone
// allowed one borrower's earnings to be applied against another's loan.
uint256 remaining = unpaidYield(loanId);
if (amount > remaining) revert ExceedsLoanEarnings(loanId, amount, remaining);
uint256 surplus = _payableSurplus();
if (amount > surplus) revert InsufficientAvailable(amount, surplus);
p.yieldApplied += amount;
uint256 net = _takePerformanceFee(loanId, p.borrower, amount);
usdc.safeTransfer(servicer, net);
// Carries the borrower so they can find their own payments by address,
// without needing to know their loan id. The amount reported is what
// actually reached the loan, after any fee.
emit YieldAppliedToPrincipal(loanId, p.borrower, net, servicer);
}
/**
* @notice Lets a borrower take their earned yield to their own wallet.
*
* @dev The answer to "how do I know my yield reached me": they move it
* themselves, to the address that pledged. No servicer, no allowlist,
* no instruction — the funds land in the wallet that owns the position
* or the transaction reverts.
*
* Bounded the same way as the servicer path, so the two cannot be used
* together to draw more than the loan earned.
*/
function withdrawYieldToBorrower(bytes32 loanId, uint256 amount)
external
nonReentrant
{
if (amount == 0) revert ZeroAmount();
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (p.borrower != msg.sender) revert NotBorrower();
uint256 remaining = unpaidYield(loanId);
if (amount > remaining) revert ExceedsLoanEarnings(loanId, amount, remaining);
uint256 surplus = _payableSurplus();
if (amount > surplus) revert InsufficientAvailable(amount, surplus);
p.yieldApplied += amount;
uint256 net = _takePerformanceFee(loanId, p.borrower, amount);
usdc.safeTransfer(p.borrower, net);
emit YieldWithdrawnByBorrower(loanId, p.borrower, net);
}
// ------------------------------------------------------------ hardship
/**
* @notice Puts a loan into hardship support.
* @dev Servicer-initiated, because hardship is assessed off-chain against
* documentation. Requires the borrower's existing yield consent: their
* earnings cannot be spent on their behalf without it.
*/
function declareHardship(bytes32 loanId) external onlyRole(SERVICER_ROLE) {
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (p.hardshipActive) revert HardshipAlreadyActive(loanId);
if (!p.yieldConsent) revert NoYieldConsent();
p.hardshipActive = true;
emit HardshipDeclared(loanId, p.borrower);
}
/// @notice Ends hardship support. Callable by the servicer or the borrower.
function endHardship(bytes32 loanId) external {
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (!p.hardshipActive) revert HardshipNotActive(loanId);
// A borrower who has recovered should not have to wait on a servicer to
// stop spending their yield.
if (p.borrower != msg.sender && !hasRole(SERVICER_ROLE, msg.sender)) {
revert NotBorrower();
}
p.hardshipActive = false;
emit HardshipEnded(loanId, p.borrower, p.hardshipDraws);
}
/**
* @notice Draws earned yield to cover one monthly payment during hardship.
*
* @dev Bounded exactly as every other payout is: by what this loan has
* earned and by the vault's surplus. Pledged principal is unreachable
* from here, so a borrower in difficulty cannot have their collateral
* spent down to cover payments — they would exit hardship owing the
* same loan against the same collateral.
*/
function coverPaymentFromYield(bytes32 loanId, uint256 amount, address servicer)
external
onlyRole(SERVICER_ROLE)
nonReentrant
{
if (amount == 0) revert ZeroAmount();
if (!approvedYieldDestinations[servicer]) revert DestinationNotApproved(servicer);
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (!p.hardshipActive) revert HardshipNotActive(loanId);
if (p.hardshipDraws >= maxHardshipDraws) {
revert HardshipDrawsExhausted(loanId, p.hardshipDraws, maxHardshipDraws);
}
uint256 remaining = unpaidYield(loanId);
if (amount > remaining) revert ExceedsLoanEarnings(loanId, amount, remaining);
uint256 surplus = _payableSurplus();
if (amount > surplus) revert InsufficientAvailable(amount, surplus);
p.yieldApplied += amount;
p.hardshipDraws += 1;
p.hardshipDrawn += amount;
usdc.safeTransfer(servicer, amount);
emit HardshipPaymentCovered(loanId, p.borrower, amount, p.hardshipDraws, servicer);
}
/**
* @notice Takes the platform's share of yield being realized.
*
* @dev Charged on the GROSS amount realized, so the caller's bound checks
* (which are against this loan's earnings and the vault's surplus)
* already cover it and no fee can push a payout past either limit.
* Returns what is left to send on.
*
* Reaches only yield. `pledged` is not an input here and cannot be,
* which is what makes "we are paid from earnings, never from your
* collateral" a property of the code rather than a policy.
*/
function _takePerformanceFee(bytes32 loanId, address borrower, uint256 grossAmount)
private
returns (uint256 net)
{
uint256 bps = performanceFeeBps;
address recipient = feeRecipient;
if (bps == 0 || recipient == address(0)) return grossAmount;
uint256 fee = (grossAmount * bps) / 10_000;
if (fee == 0) return grossAmount;
usdc.safeTransfer(recipient, fee);
emit PerformanceFeeCharged(loanId, borrower, grossAmount, fee, recipient);
return grossAmount - fee;
}
/// @notice How much hardship support a loan could still draw right now.
function hardshipAvailable(bytes32 loanId)
external
view
returns (bool active, uint256 drawsLeft, uint256 yieldAvailable)
{
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
return (
p.hardshipActive,
p.hardshipDraws >= maxHardshipDraws ? 0 : maxHardshipDraws - p.hardshipDraws,
unpaidYield(loanId)
);
}
// -------------------------------------------------------------- admin
/**
* @notice Points the vault at a yield strategy, or clears it with address(0).
*
* @dev A strategy must declare that it never exposes pledged principal to
* price movement. Without this check, wiring the vault to a strategy
* that sells collateral is one transaction away and silent until a
* drawdown — the borrower discovers it when they are recalled less
* than they pledged.
*
* The declaration is read through a try/catch so a strategy that does
* not implement the interface at all is refused with a message naming
* the reason, rather than reverting somewhere unrelated later.
*
* Clearing the strategy is always permitted: removing market exposure
* must never be blocked.
*/
/// @notice Approves or revokes an address that yield may be paid to.
function setYieldDestinationApproval(address destination, bool approved)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
if (destination == address(0)) revert ZeroAddress();
approvedYieldDestinations[destination] = approved;
emit YieldDestinationApprovalChanged(destination, approved);
}
/**
* @notice Points the vault at the loan book its collateral secures.
*
* @dev Only enables {payoffLoanFromCollateral}. It grants this contract no
* authority over loans and grants the loan book no authority here —
* the payoff path is borrower-signed and the payment call it makes is
* permissionless on the other side, so no trust passes either way.
*
* Clearing it with address(0) is always permitted.
*/
function setMortgageVault(address vault) external onlyRole(DEFAULT_ADMIN_ROLE) {
mortgageVault = IMortgageLoan(vault);
emit MortgageVaultChanged(vault);
}
/**
* @notice Sets the performance fee on realized yield and where it goes.
*
* @dev Capped at {MAX_PERFORMANCE_FEE_BPS}. A fee above that would start to
* consume the borrower's earnings rather than share them, which is the
* point at which the acceleration this product exists to deliver stops
* being real.
*
* Setting a non-zero fee requires a recipient; a fee with nowhere to go
* would silently not be charged, and a fee schedule that does not match
* what is published is worse than no fee at all.
*/
function setPerformanceFee(uint256 bps, address recipient)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
if (bps > MAX_PERFORMANCE_FEE_BPS) revert FeeOutOfRange(bps, MAX_PERFORMANCE_FEE_BPS);
if (bps > 0 && recipient == address(0)) revert ZeroAddress();
performanceFeeBps = bps;
feeRecipient = recipient;
emit PerformanceFeeChanged(bps, recipient);
}
function setMaxHardshipDraws(uint256 draws) external onlyRole(DEFAULT_ADMIN_ROLE) {
maxHardshipDraws = draws;
emit MaxHardshipDrawsChanged(draws);
}
function setYieldStrategy(address strategy) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (strategy != address(0)) {
try IPrincipalProtected(strategy).isPrincipalProtected() returns (bool protected) {
if (!protected) revert StrategyNotPrincipalProtected(strategy);
} catch {
revert StrategyNotPrincipalProtected(strategy);
}
// A strategy may declare protection and still be holding exposure
// from an earlier configuration. Checked rather than assumed.
try IPrincipalProtected(strategy).principalAtMarketRisk() returns (uint256 atRisk) {
if (atRisk != 0) revert StrategyHasPrincipalAtRisk(strategy, atRisk);
} catch {
revert StrategyNotPrincipalProtected(strategy);
}
}
yieldStrategy = IYieldStrategy(strategy);
emit YieldStrategyChanged(strategy, strategy == address(0) ? "none" : IYieldStrategy(strategy).marketName());
}
function pause() external onlyRole(GUARDIAN_ROLE) {
_pause();
}
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}
// -------------------------------------------------------------- views
function getPosition(bytes32 loanId)
external
view
returns (
address borrower,
uint256 pledged,
uint256 deployed,
uint256 yieldApplied,
bool yieldConsent,
bool locked
)
{
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
return (p.borrower, p.pledged, p.deployed, p.yieldApplied, p.yieldConsent, p.locked);
}
/**
* @notice What this loan's claim on the pooled strategy is worth now.
* @dev Pro-rata by shares, so it reflects this borrower's own gains and
* losses rather than the pool's average.
*/
function positionValue(bytes32 loanId) public view returns (uint256) {
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (totalYieldShares == 0 || p.yieldShares == 0) return 0;
if (address(yieldStrategy) == address(0)) return 0;
return (yieldStrategy.totalValue() * p.yieldShares) / totalYieldShares;
}
/**
* @notice Yield this loan has earned but not yet applied to principal.
* @dev Zero when the position is under water — never negative, and never a
* figure a borrower could be shown as earnings they do not have.
*/
function earnedYield(bytes32 loanId) public view returns (uint256) {
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
uint256 value = positionValue(loanId);
uint256 live = value > p.deployed ? value - p.deployed : 0;
// The recorded peak survives a recall or a harvest; the live figure
// does not. Entitlement is the greater of the two.
return live > p.yieldHighWater ? live : p.yieldHighWater;
}
/// @notice Yield earned but not yet paid out, by either route.
function unpaidYield(bytes32 loanId) public view returns (uint256) {
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
uint256 earned = earnedYield(loanId);
return earned > p.yieldApplied ? earned - p.yieldApplied : 0;
}
/**
* @dev USDC in the vault beyond the principal it is actually holding.
*
* Principal that has been deployed is sitting in the strategy, not
* here, so measuring against `totalPledged` alone understates the
* surplus — and once any principal was deployed it read as zero,
* making every yield payout impossible.
*/
function _payableSurplus() private view returns (uint256) {
uint256 held = usdc.balanceOf(address(this));
uint256 principalHeldHere = totalPledged > totalDeployed
? totalPledged - totalDeployed
: 0;
return held > principalHeldHere ? held - principalHeldHere : 0;
}
/**
* @dev Records the loan's live earnings if they are a new peak. Called
* before any action that moves value out of the strategy, so the
* entitlement is captured while it is still visible.
*/
function _recordYieldHighWater(bytes32 loanId) private {
Position storage p = _positions[loanId];
if (!p.exists || p.yieldShares == 0) return;
uint256 value = positionValue(loanId);
uint256 live = value > p.deployed ? value - p.deployed : 0;
if (live > p.yieldHighWater) p.yieldHighWater = live;
}
/**
* @notice One call with everything a borrower or an administrator needs to
* judge an account: what was pledged, what is at work, what it is
* worth now, what has been earned, and whether principal is whole.
*/
function accountSummary(bytes32 loanId)
external
view
returns (
address borrower,
uint256 pledged,
uint256 deployed,
uint256 currentValue,
uint256 unrealizedYield,
uint256 yieldApplied,
bool principalIntact,
bool yieldConsent,
bool locked
)
{
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
uint256 value = positionValue(loanId);
return (
p.borrower,
p.pledged,
p.deployed,
value,
value > p.deployed ? value - p.deployed : 0,
p.yieldApplied,
// False is the signal that matters: this loan's claim is worth less
// than the principal put in.
p.deployed == 0 || value >= p.deployed,
p.yieldConsent,
p.locked
);
}
/**
* @dev Shares for a deposit, priced against the pool's value beforehand.
* The first deposit sets the share price; later ones buy in at
* whatever the pool is worth, so nobody inherits another's gains.
*/
function _sharesForDeposit(uint256 amount) private view returns (uint256) {
uint256 poolValue = address(yieldStrategy) == address(0)
? 0
: yieldStrategy.totalValue();
if (totalYieldShares == 0 || poolValue == 0) {
return amount * SHARE_PRECISION;
}
return (amount * totalYieldShares) / poolValue;
}
/// @notice Collateral that could be withdrawn right now.
function availableToWithdraw(bytes32 loanId) external view returns (uint256) {
Position storage p = _positions[loanId];
if (!p.exists) revert PositionNotFound();
if (p.locked) return 0;
return p.pledged - p.deployed;
}
}
Bytecode hash 0xb082a9c506e80ec02d0d971ac3941c14ccfe3f19d7c6c79d06adbd4edab27622
[
{
"type": "constructor",
"inputs": [
{
"name": "usdcAddress",
"type": "address",
"internalType": "address"
},
{
"name": "admin",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "nonpayable"
},
{
"name": "AccessControlBadConfirmation",
"type": "error",
"inputs": []
},
{
"name": "AccessControlUnauthorizedAccount",
"type": "error",
"inputs": [
{
"name": "account",
"type": "address",
"internalType": "address"
},
{
"name": "neededRole",
"type": "bytes32",
"internalType": "bytes32"
}
]
},
{
"name": "CollateralLocked",
"type": "error",
"inputs": []
},
{
"name": "DestinationNotApproved",
"type": "error",
"inputs": [
{
"name": "destination",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "EnforcedPause",
"type": "error",
"inputs": []
},
{
"name": "ExceedsLoanEarnings",
"type": "error",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "requested",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "earned",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "ExpectedPause",
"type": "error",
"inputs": []
},
{
"name": "FeeOutOfRange",
"type": "error",
"inputs": [
{
"name": "requested",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "maximum",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "HardshipAlreadyActive",
"type": "error",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
]
},
{
"name": "HardshipDrawsExhausted",
"type": "error",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "used",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "allowed",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "HardshipNotActive",
"type": "error",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
]
},
{
"name": "InsufficientAvailable",
"type": "error",
"inputs": [
{
"name": "requested",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "available",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "LoanAlreadySettled",
"type": "error",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
]
},
{
"name": "NoMortgageVault",
"type": "error",
"inputs": []
},
{
"name": "NoStrategySet",
"type": "error",
"inputs": []
},
{
"name": "NoYieldConsent",
"type": "error",
"inputs": []
},
{
"name": "NotBorrower",
"type": "error",
"inputs": []
},
{
"name": "PositionExists",
"type": "error",
"inputs": []
},
{
"name": "PositionNotFound",
"type": "error",
"inputs": []
},
{
"name": "ReentrancyGuardReentrantCall",
"type": "error",
"inputs": []
},
{
"name": "SafeERC20FailedOperation",
"type": "error",
"inputs": [
{
"name": "token",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "StrategyHasPrincipalAtRisk",
"type": "error",
"inputs": [
{
"name": "strategy",
"type": "address",
"internalType": "address"
},
{
"name": "atRisk",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "StrategyNotPrincipalProtected",
"type": "error",
"inputs": [
{
"name": "strategy",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "ZeroAddress",
"type": "error",
"inputs": []
},
{
"name": "ZeroAmount",
"type": "error",
"inputs": []
},
{
"name": "CollateralDeployed",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "market",
"type": "string",
"indexed": false,
"internalType": "string"
}
],
"anonymous": false
},
{
"name": "CollateralPledged",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "CollateralRecalled",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "CollateralReleased",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "HardshipDeclared",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "HardshipEnded",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "drawsUsed",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "HardshipPaymentCovered",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "drawNumber",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "servicer",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "LoanPaidFromCollateral",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amountApplied",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "remainingPledged",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "MaxHardshipDrawsChanged",
"type": "event",
"inputs": [
{
"name": "draws",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "MortgageVaultChanged",
"type": "event",
"inputs": [
{
"name": "mortgageVault",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "Paused",
"type": "event",
"inputs": [
{
"name": "account",
"type": "address",
"indexed": false,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "PerformanceFeeChanged",
"type": "event",
"inputs": [
{
"name": "bps",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "recipient",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "PerformanceFeeCharged",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "grossYield",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "fee",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "recipient",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "RoleAdminChanged",
"type": "event",
"inputs": [
{
"name": "role",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "previousAdminRole",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "newAdminRole",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
}
],
"anonymous": false
},
{
"name": "RoleGranted",
"type": "event",
"inputs": [
{
"name": "role",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "sender",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "RoleRevoked",
"type": "event",
"inputs": [
{
"name": "role",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "sender",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "Unpaused",
"type": "event",
"inputs": [
{
"name": "account",
"type": "address",
"indexed": false,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "YieldAppliedToPrincipal",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "servicer",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "YieldConsentSet",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "consented",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"anonymous": false
},
{
"name": "YieldDestinationApprovalChanged",
"type": "event",
"inputs": [
{
"name": "destination",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"anonymous": false
},
{
"name": "YieldStrategyChanged",
"type": "event",
"inputs": [
{
"name": "strategy",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "market",
"type": "string",
"indexed": false,
"internalType": "string"
}
],
"anonymous": false
},
{
"name": "YieldWithdrawnByBorrower",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "DEFAULT_ADMIN_ROLE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "GUARDIAN_ROLE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "MAX_PERFORMANCE_FEE_BPS",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "SERVICER_ROLE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "accountSummary",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "borrower",
"type": "address",
"internalType": "address"
},
{
"name": "pledged",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "deployed",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "currentValue",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "unrealizedYield",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "yieldApplied",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "principalIntact",
"type": "bool",
"internalType": "bool"
},
{
"name": "yieldConsent",
"type": "bool",
"internalType": "bool"
},
{
"name": "locked",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "applyYieldToPrincipal",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "servicer",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "approvedYieldDestinations",
"type": "function",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "availableToWithdraw",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "coverPaymentFromYield",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "servicer",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "declareHardship",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "deployToYield",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "earnedYield",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "endHardship",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "feeRecipient",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "getPosition",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "borrower",
"type": "address",
"internalType": "address"
},
{
"name": "pledged",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "deployed",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "yieldApplied",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "yieldConsent",
"type": "bool",
"internalType": "bool"
},
{
"name": "locked",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "getRoleAdmin",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "grantRole",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "hardshipAvailable",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "active",
"type": "bool",
"internalType": "bool"
},
{
"name": "drawsLeft",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "yieldAvailable",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "hasRole",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "lock",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "maxHardshipDraws",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "mortgageVault",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IMortgageLoan"
}
],
"stateMutability": "view"
},
{
"name": "pause",
"type": "function",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "paused",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "payoffFromCollateralAvailable",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "payoff",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "recommendedAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "idleCollateral",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "deployedCollateral",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "canSettleInFull",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "payoffLoanFromCollateral",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "maxAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "applied",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "performanceFeeBps",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "pledgeCollateral",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "positionValue",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "recallFromYield",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "renounceRole",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "callerConfirmation",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "revokeRole",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setMaxHardshipDraws",
"type": "function",
"inputs": [
{
"name": "draws",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setMortgageVault",
"type": "function",
"inputs": [
{
"name": "vault",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setPerformanceFee",
"type": "function",
"inputs": [
{
"name": "bps",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "recipient",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setYieldConsent",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "consented",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setYieldDestinationApproval",
"type": "function",
"inputs": [
{
"name": "destination",
"type": "address",
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setYieldStrategy",
"type": "function",
"inputs": [
{
"name": "strategy",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "supportsInterface",
"type": "function",
"inputs": [
{
"name": "interfaceId",
"type": "bytes4",
"internalType": "bytes4"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "totalDeployed",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "totalPledged",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "totalYieldShares",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "unlock",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "unpaidYield",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "unpause",
"type": "function",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "usdc",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IERC20"
}
],
"stateMutability": "view"
},
{
"name": "withdrawCollateral",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "withdrawYieldToBorrower",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "yieldStrategy",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IYieldStrategy"
}
],
"stateMutability": "view"
}
]