Loading…
Contract
Native balance
0 tZROAR
Live from the node · no monetary value
Activity
7 transactions
First seen block 16,965 · last · nonce 1
Contract
Verified — exact match
The compiled bytecode matches the deployed bytecode exactly, including metadata.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 {ICollateralVault} from "./interfaces/ICollateralVault.sol";
import {IMortgageDeed} from "./interfaces/IMortgageDeed.sol";
/**
* @title MortgageVault
* @notice The loan ledger for a VaultToHome mortgage on Base. Replaces the Sui
* Move module `loan_vault` (create_vault, make_payment,
* get_vault_status, refinance_vault, release_collateral).
*
* @dev Three structural differences from the Move original, each deliberate:
*
* CUSTODY LIVES ELSEWHERE. Move's `create_vault` swallowed the collateral
* `Coin` and the vault object held it for the life of the loan. Here the
* borrower pledges to {CollateralVault}, which already implements pledge,
* lock, borrower-only withdrawal and opt-in yield. This contract holds a
* SERVICER_ROLE there and uses exactly two functions — lock at
* origination, unlock at payoff. It has no code path that can move a
* borrower's pledged collateral, and {ICollateralVault} is narrowed to
* make that structural rather than a matter of reading this file
* carefully.
*
* THERE IS NO PRICE ORACLE. Move passed a `PriceOracle` into four of the
* five entry points because collateral was SUI and its dollar value moved.
* Collateral is now USDC, which is the unit of account — one USDC base
* unit is one micro-dollar by definition. CLTV is therefore arithmetic on
* two on-chain numbers, not an oracle read, and there is no oracle to
* manipulate, stale-price, or take down.
*
* INTEREST IS COMPUTED, NOT ASSERTED. A payment is split into interest and
* principal by this contract from elapsed time and the recorded rate, not
* by a servicer-supplied split. If the servicer supplied the split it
* could book every payment as interest and leave principal untouched
* forever; the borrower would have no on-chain recourse. Accrual is simple
* per-diem interest on the outstanding principal and payments apply to
* accrued interest first, then principal — standard servicing order.
*
* Per-loan accounting is explicit. This contract's USDC balance is not
* consulted anywhere, and payments are forwarded to the loan's own lender
* in the same transaction, so no pool of money exists here for one loan's
* accounting to be settled out of another's.
*/
contract MortgageVault is AccessControl, ReentrancyGuard, Pausable {
using SafeERC20 for IERC20;
/// @notice Originates loans, adjusts terms, releases liens.
bytes32 public constant SERVICER_ROLE = keccak256("SERVICER_ROLE");
/// @notice May halt origination and payments in an incident.
bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");
uint256 private constant BPS = 10_000;
uint256 private constant SECONDS_PER_YEAR = 365 days;
/// @notice The only token this ledger settles in: native Circle USDC on Base.
IERC20 public immutable usdc;
/// @notice Custodian of pledged collateral. Required before any origination.
ICollateralVault public collateralVault;
/// @notice Lien record registry. Optional — the ledger works without it.
IMortgageDeed public deed;
/// @dev Terms as supplied at origination. A calldata struct rather than a
/// dozen positional arguments, which is also what keeps `createVault`
/// off the stack limit.
struct LoanTerms {
address borrower;
address lender; // where payments are forwarded
uint256 principal; // USDC base units
uint256 propertyValue; // appraised at origination, USDC base units
uint256 minCollateral; // must already be pledged for this loan id
uint256 balloonAmount; // USDC base units, 0 when not applicable
uint32 interestRateBps;
uint32 termYears;
uint32 balloonDueYears;
bool balloonRequired;
bytes32 propertyRef; // commitment to the property address, never the address itself
}
struct Loan {
address borrower;
address lender;
uint256 principal;
uint256 outstandingPrincipal;
uint256 interestAccrued;
uint256 totalPrincipalPaid;
uint256 totalInterestPaid;
uint256 propertyValue;
uint256 balloonAmount;
uint64 startedAt;
uint64 lastAccrualAt;
uint64 maturesAt;
uint64 balloonDueAt;
uint32 interestRateBps;
uint32 termYears;
bool balloonRequired;
bool exists;
bool closed;
bytes32 propertyRef;
}
/// @dev The mapping IS the Move `VaultRegistry`. Loan ids are assigned
/// off-chain and hashed to bytes32 by the caller.
mapping(bytes32 => Loan) private _loans;
/// @notice Loans ever originated. Enumeration is left to event indexing.
uint256 public totalLoans;
/// @notice Principal outstanding across every open loan.
uint256 public totalOutstandingPrincipal;
/// @dev Everything a status read returns, as one tuple.
struct Status {
uint256 outstandingPrincipal;
uint256 accruedInterest;
uint256 payoffAmount;
uint256 collateralUsdc;
uint256 cltvBps;
uint256 ltvBps;
bool collateralLocked;
bool closed;
}
event VaultCreated(
bytes32 indexed loanId,
address indexed borrower,
address indexed lender,
uint256 principal,
uint32 interestRateBps,
uint32 termYears
);
event CollateralLockedForLoan(bytes32 indexed loanId, uint256 pledged);
event InterestAccrued(bytes32 indexed loanId, uint256 amount, uint256 totalAccrued);
event PaymentMade(
bytes32 indexed loanId,
address indexed payer,
uint256 interestPortion,
uint256 principalPortion,
uint256 remainingPrincipal
);
event OverpaymentRefunded(bytes32 indexed loanId, address indexed payer, uint256 amount);
event VaultRefinanced(
bytes32 indexed loanId,
uint32 oldRateBps,
uint32 newRateBps,
uint32 newTermYears
);
event LoanClosedOut(bytes32 indexed loanId, address indexed borrower);
event CollateralVaultChanged(address indexed collateralVault);
event DeedRegistryChanged(address indexed deed);
error LoanAlreadyExists();
error LoanNotFound();
error LoanIsClosed();
error ZeroAmount();
error ZeroAddress();
error NoCollateralVault();
error CollateralBorrowerMismatch(address expected, address actual);
error InsufficientCollateral(uint256 required, uint256 pledged);
error RateIncreaseNotAllowed(uint32 currentBps, uint32 proposedBps);
error TermTooShort(uint32 elapsedYears, uint32 proposedYears);
error NotBorrowerOrServicer();
error LoanNotPaidOff(uint256 payoffAmount);
error InvalidTerms();
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);
}
// ---------------------------------------------------------- origination
/**
* @notice Originates a loan: records terms, locks the pledged collateral,
* and mints the lien record.
* @dev Replaces `loan_vault::create_vault`.
*
* The collateral checks are the substance of this function. The Move
* version took the coin, so possession was the proof. Here the money
* sits in another contract, and an unchecked origination would happily
* record a fully-collateralised loan against an empty position — so
* the pledge is verified to exist, to belong to this borrower, and to
* meet the required amount, before the loan exists at all.
*/
function createVault(bytes32 loanId, LoanTerms calldata terms)
external
onlyRole(SERVICER_ROLE)
whenNotPaused
{
if (terms.borrower == address(0) || terms.lender == address(0)) revert ZeroAddress();
if (terms.principal == 0) revert ZeroAmount();
if (terms.termYears == 0) revert InvalidTerms();
if (terms.balloonRequired && (terms.balloonAmount == 0 || terms.balloonDueYears == 0)) {
revert InvalidTerms();
}
if (_loans[loanId].exists) revert LoanAlreadyExists();
if (address(collateralVault) == address(0)) revert NoCollateralVault();
(address pledgeOwner, uint256 pledged, , , , ) = collateralVault.getPosition(loanId);
if (pledgeOwner != terms.borrower) {
revert CollateralBorrowerMismatch(terms.borrower, pledgeOwner);
}
if (pledged < terms.minCollateral) {
revert InsufficientCollateral(terms.minCollateral, pledged);
}
Loan storage l = _loans[loanId];
l.borrower = terms.borrower;
l.lender = terms.lender;
l.principal = terms.principal;
l.outstandingPrincipal = terms.principal;
l.propertyValue = terms.propertyValue;
l.balloonAmount = terms.balloonRequired ? terms.balloonAmount : 0;
l.startedAt = uint64(block.timestamp);
l.lastAccrualAt = uint64(block.timestamp);
l.maturesAt = uint64(block.timestamp + uint256(terms.termYears) * SECONDS_PER_YEAR);
l.balloonDueAt = terms.balloonRequired
? uint64(block.timestamp + uint256(terms.balloonDueYears) * SECONDS_PER_YEAR)
: 0;
l.interestRateBps = terms.interestRateBps;
l.termYears = terms.termYears;
l.balloonRequired = terms.balloonRequired;
l.exists = true;
l.propertyRef = terms.propertyRef;
totalLoans += 1;
totalOutstandingPrincipal += terms.principal;
collateralVault.lock(loanId);
if (address(deed) != address(0)) {
deed.mintDeed(loanId, terms.borrower);
}
emit VaultCreated(
loanId,
terms.borrower,
terms.lender,
terms.principal,
terms.interestRateBps,
terms.termYears
);
emit CollateralLockedForLoan(loanId, pledged);
}
// -------------------------------------------------------------- payment
/**
* @notice Pays down a loan. Interest first, then principal.
* @dev Replaces `loan_vault::make_payment`.
*
* Anyone may pay — a co-borrower, a relative, a payoff company wiring
* a refinance — because a payment only ever reduces the named loan's
* debt and can never redirect anything to the payer.
*
* Funds are forwarded to the loan's own lender in this transaction.
* Nothing accumulates here, which is why there is no pool for one
* loan's payment to be spent out of another's balance.
*/
function makePayment(bytes32 loanId, uint256 amount)
external
nonReentrant
whenNotPaused
{
if (amount == 0) revert ZeroAmount();
Loan storage l = _loans[loanId];
if (!l.exists) revert LoanNotFound();
if (l.closed) revert LoanIsClosed();
_accrue(loanId, l);
// Credit the measured delta, not the requested amount, so a
// fee-on-transfer token cannot reduce a debt by more than arrived.
uint256 before = usdc.balanceOf(address(this));
usdc.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = usdc.balanceOf(address(this)) - before;
if (received == 0) revert ZeroAmount();
uint256 payoff = l.interestAccrued + l.outstandingPrincipal;
uint256 applied = received > payoff ? payoff : received;
uint256 refund = received - applied;
uint256 interestPortion = applied > l.interestAccrued ? l.interestAccrued : applied;
uint256 principalPortion = applied - interestPortion;
l.interestAccrued -= interestPortion;
l.totalInterestPaid += interestPortion;
l.outstandingPrincipal -= principalPortion;
l.totalPrincipalPaid += principalPortion;
totalOutstandingPrincipal -= principalPortion;
address lender = l.lender;
uint256 remaining = l.outstandingPrincipal;
if (applied > 0) {
usdc.safeTransfer(lender, applied);
}
// Money beyond the payoff figure was never the lender's. It goes
// straight back to whoever sent it rather than sitting here.
if (refund > 0) {
usdc.safeTransfer(msg.sender, refund);
emit OverpaymentRefunded(loanId, msg.sender, refund);
}
emit PaymentMade(loanId, msg.sender, interestPortion, principalPortion, remaining);
}
// ----------------------------------------------------------- refinance
/**
* @notice Adjusts the rate and remaining term on a live loan.
* @dev Replaces `loan_vault::refinance_vault`.
*
* Interest is accrued at the OLD rate before the new one takes effect.
* Skipping that would silently reprice every day since the last
* payment, in whichever direction suited whoever called.
*
* A refinance here may never raise the rate. In this product a
* refinance is a concession — the workout path ends in one — so a
* servicer-initiated rate increase has no legitimate use, and allowing
* one would mean a role key could unilaterally worsen a borrower's
* terms on a loan already secured by their collateral. A genuine
* repricing upward is a new loan the borrower signs for, which means a
* new loan id.
*
* The additional-collateral argument the Move version took is gone:
* the borrower tops up by calling `pledgeCollateral` on the collateral
* vault directly, which keeps the deposit under their own signature
* instead of the servicer's transaction.
*/
function refinanceVault(bytes32 loanId, uint32 newRateBps, uint32 newTermYears)
external
onlyRole(SERVICER_ROLE)
whenNotPaused
{
Loan storage l = _loans[loanId];
if (!l.exists) revert LoanNotFound();
if (l.closed) revert LoanIsClosed();
if (newTermYears == 0) revert InvalidTerms();
if (newRateBps > l.interestRateBps) {
revert RateIncreaseNotAllowed(l.interestRateBps, newRateBps);
}
uint32 elapsedYears = uint32((block.timestamp - l.startedAt) / SECONDS_PER_YEAR);
if (newTermYears < elapsedYears) revert TermTooShort(elapsedYears, newTermYears);
_accrue(loanId, l);
// Any sub-rounding remainder of elapsed time is discarded here rather
// than carried across the rate change, so nothing from before the
// refinance is ever charged at a rate the borrower did not have.
l.lastAccrualAt = uint64(block.timestamp);
uint32 oldRate = l.interestRateBps;
l.interestRateBps = newRateBps;
l.termYears = newTermYears;
l.maturesAt = uint64(l.startedAt + uint256(newTermYears) * SECONDS_PER_YEAR);
emit VaultRefinanced(loanId, oldRate, newRateBps, newTermYears);
}
// ------------------------------------------------------------- release
/**
* @notice Closes a paid-off loan, unlocks the collateral and releases the lien.
* @dev Replaces `loan_vault::release_collateral`.
*
* Two deliberate departures from a servicer-only admin function:
*
* - The BORROWER can call it. Once the debt is zero, release is owed,
* not granted. If only the servicer could call it, a servicer who
* stopped answering would leave collateral locked forever.
* - It is NOT pausable. A pause is for stopping new money moving, not
* for holding a repaid borrower's collateral hostage. Unlocking here
* does not itself move funds; the borrower then withdraws from the
* collateral vault under their own signature.
*/
function releaseCollateral(bytes32 loanId) external nonReentrant {
Loan storage l = _loans[loanId];
if (!l.exists) revert LoanNotFound();
if (l.closed) revert LoanIsClosed();
if (msg.sender != l.borrower && !hasRole(SERVICER_ROLE, msg.sender)) {
revert NotBorrowerOrServicer();
}
_accrue(loanId, l);
uint256 payoff = l.outstandingPrincipal + l.interestAccrued;
if (payoff != 0) revert LoanNotPaidOff(payoff);
l.closed = true;
collateralVault.unlock(loanId);
if (address(deed) != address(0)) {
deed.burnDeed(loanId);
}
emit LoanClosedOut(loanId, l.borrower);
}
// -------------------------------------------------------------- accrual
/**
* @notice Brings a loan's accrued interest up to the current block.
* @dev Open to anyone: accrual only ever increases what is owed to the
* lender by an amount time has already earned, and every other entry
* point calls it first anyway. Exposing it lets a borrower pin down
* the exact figure before paying.
*/
function accrue(bytes32 loanId) external {
Loan storage l = _loans[loanId];
if (!l.exists) revert LoanNotFound();
if (l.closed) revert LoanIsClosed();
_accrue(loanId, l);
}
function _pendingInterest(Loan storage l) private view returns (uint256) {
if (l.closed || l.outstandingPrincipal == 0) return 0;
uint256 elapsed = block.timestamp - l.lastAccrualAt;
if (elapsed == 0) return 0;
return (l.outstandingPrincipal * l.interestRateBps * elapsed) / (BPS * SECONDS_PER_YEAR);
}
function _accrue(bytes32 loanId, Loan storage l) private {
uint256 pending = _pendingInterest(l);
// The clock only advances when something was actually charged.
// Advancing on a zero result would discard the elapsed time that had
// not yet rounded up to one base unit, which is a free interest
// reduction anyone could trigger by calling this in a tight loop.
if (pending == 0) return;
l.lastAccrualAt = uint64(block.timestamp);
l.interestAccrued += pending;
emit InterestAccrued(loanId, pending, l.interestAccrued);
}
// --------------------------------------------------------------- admin
function setCollateralVault(address vault) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (vault == address(0)) revert ZeroAddress();
collateralVault = ICollateralVault(vault);
emit CollateralVaultChanged(vault);
}
function setDeedRegistry(address registry) external onlyRole(DEFAULT_ADMIN_ROLE) {
deed = IMortgageDeed(registry);
emit DeedRegistryChanged(registry);
}
function pause() external onlyRole(GUARDIAN_ROLE) {
_pause();
}
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}
// --------------------------------------------------------------- views
/**
* @notice Everything the Move `get_vault_status` returned, plus the figures
* that replaced its oracle read.
* @dev A plain view. The Sui version needed `devInspectTransactionBlock`
* because the Move function was an entry function; on an EVM chain a
* view is an `eth_call` and costs nothing.
*/
function getVaultStatus(bytes32 loanId) external view returns (Status memory status) {
Loan storage l = _loans[loanId];
if (!l.exists) revert LoanNotFound();
uint256 accrued = l.interestAccrued + _pendingInterest(l);
uint256 payoff = l.outstandingPrincipal + accrued;
(uint256 collateral, bool locked) = _collateralOf(loanId);
status.outstandingPrincipal = l.outstandingPrincipal;
status.accruedInterest = accrued;
status.payoffAmount = payoff;
status.collateralUsdc = collateral;
status.collateralLocked = locked;
status.closed = l.closed;
// Combined loan-to-value against pledged collateral. With no collateral
// and a live debt the ratio is unbounded — reported as max rather than
// zero, which would read as "safe".
if (payoff == 0) {
status.cltvBps = 0;
} else if (collateral == 0) {
status.cltvBps = type(uint256).max;
} else {
status.cltvBps = (payoff * BPS) / collateral;
}
status.ltvBps = l.propertyValue == 0 ? 0 : (payoff * BPS) / l.propertyValue;
}
/// @notice The exact amount that closes the loan in this block.
function payoffAmount(bytes32 loanId) external view returns (uint256) {
Loan storage l = _loans[loanId];
if (!l.exists) revert LoanNotFound();
return l.outstandingPrincipal + l.interestAccrued + _pendingInterest(l);
}
function getLoan(bytes32 loanId) external view returns (Loan memory) {
Loan storage l = _loans[loanId];
if (!l.exists) revert LoanNotFound();
return l;
}
/// @notice True when the recorded commitment matches this property address.
function matchesProperty(bytes32 loanId, string calldata propertyAddress)
external
view
returns (bool)
{
Loan storage l = _loans[loanId];
if (!l.exists) revert LoanNotFound();
return l.propertyRef == keccak256(bytes(propertyAddress));
}
function _collateralOf(bytes32 loanId) private view returns (uint256 pledged, bool locked) {
if (address(collateralVault) == address(0)) return (0, false);
// A position may not exist yet, and the custodian reverts rather than
// returning zeros. A status read must not fail for that reason.
try collateralVault.getPosition(loanId) returns (
address,
uint256 pledgedAmount,
uint256,
uint256,
bool,
bool isLocked
) {
return (pledgedAmount, isLocked);
} catch {
return (0, false);
}
}
/**
* @notice USDC sitting in this contract.
* @dev Should always be zero: every payment is forwarded within the same
* transaction. A non-zero value means tokens were sent in directly and
* belong to nobody — surfaced rather than quietly applied to a loan.
*/
function unaccountedBalance() external view returns (uint256) {
return usdc.balanceOf(address(this));
}
}
Bytecode hash 0x6a5397acdbea6dcc052b7d2b2087f57a8de7b862c19335393129427d36ae0a57
[
{
"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": "CollateralBorrowerMismatch",
"type": "error",
"inputs": [
{
"name": "expected",
"type": "address",
"internalType": "address"
},
{
"name": "actual",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "EnforcedPause",
"type": "error",
"inputs": []
},
{
"name": "ExpectedPause",
"type": "error",
"inputs": []
},
{
"name": "InsufficientCollateral",
"type": "error",
"inputs": [
{
"name": "required",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "pledged",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "InvalidTerms",
"type": "error",
"inputs": []
},
{
"name": "LoanAlreadyExists",
"type": "error",
"inputs": []
},
{
"name": "LoanIsClosed",
"type": "error",
"inputs": []
},
{
"name": "LoanNotFound",
"type": "error",
"inputs": []
},
{
"name": "LoanNotPaidOff",
"type": "error",
"inputs": [
{
"name": "payoffAmount",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "NoCollateralVault",
"type": "error",
"inputs": []
},
{
"name": "NotBorrowerOrServicer",
"type": "error",
"inputs": []
},
{
"name": "RateIncreaseNotAllowed",
"type": "error",
"inputs": [
{
"name": "currentBps",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "proposedBps",
"type": "uint32",
"internalType": "uint32"
}
]
},
{
"name": "ReentrancyGuardReentrantCall",
"type": "error",
"inputs": []
},
{
"name": "SafeERC20FailedOperation",
"type": "error",
"inputs": [
{
"name": "token",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "TermTooShort",
"type": "error",
"inputs": [
{
"name": "elapsedYears",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "proposedYears",
"type": "uint32",
"internalType": "uint32"
}
]
},
{
"name": "ZeroAddress",
"type": "error",
"inputs": []
},
{
"name": "ZeroAmount",
"type": "error",
"inputs": []
},
{
"name": "CollateralLockedForLoan",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "pledged",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "CollateralVaultChanged",
"type": "event",
"inputs": [
{
"name": "collateralVault",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "DeedRegistryChanged",
"type": "event",
"inputs": [
{
"name": "deed",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "InterestAccrued",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "totalAccrued",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "LoanClosedOut",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "OverpaymentRefunded",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "payer",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "Paused",
"type": "event",
"inputs": [
{
"name": "account",
"type": "address",
"indexed": false,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "PaymentMade",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "payer",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "interestPortion",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "principalPortion",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "remainingPrincipal",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"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": "VaultCreated",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "lender",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "principal",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "interestRateBps",
"type": "uint32",
"indexed": false,
"internalType": "uint32"
},
{
"name": "termYears",
"type": "uint32",
"indexed": false,
"internalType": "uint32"
}
],
"anonymous": false
},
{
"name": "VaultRefinanced",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "oldRateBps",
"type": "uint32",
"indexed": false,
"internalType": "uint32"
},
{
"name": "newRateBps",
"type": "uint32",
"indexed": false,
"internalType": "uint32"
},
{
"name": "newTermYears",
"type": "uint32",
"indexed": false,
"internalType": "uint32"
}
],
"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": "SERVICER_ROLE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "accrue",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "collateralVault",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract ICollateralVault"
}
],
"stateMutability": "view"
},
{
"name": "createVault",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "terms",
"type": "tuple",
"components": [
{
"name": "borrower",
"type": "address",
"internalType": "address"
},
{
"name": "lender",
"type": "address",
"internalType": "address"
},
{
"name": "principal",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "propertyValue",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "minCollateral",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "balloonAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "interestRateBps",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "termYears",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "balloonDueYears",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "balloonRequired",
"type": "bool",
"internalType": "bool"
},
{
"name": "propertyRef",
"type": "bytes32",
"internalType": "bytes32"
}
],
"internalType": "struct MortgageVault.LoanTerms"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "deed",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract IMortgageDeed"
}
],
"stateMutability": "view"
},
{
"name": "getLoan",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "tuple",
"components": [
{
"name": "borrower",
"type": "address",
"internalType": "address"
},
{
"name": "lender",
"type": "address",
"internalType": "address"
},
{
"name": "principal",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "outstandingPrincipal",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "interestAccrued",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "totalPrincipalPaid",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "totalInterestPaid",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "propertyValue",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "balloonAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "startedAt",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "lastAccrualAt",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "maturesAt",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "balloonDueAt",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "interestRateBps",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "termYears",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "balloonRequired",
"type": "bool",
"internalType": "bool"
},
{
"name": "exists",
"type": "bool",
"internalType": "bool"
},
{
"name": "closed",
"type": "bool",
"internalType": "bool"
},
{
"name": "propertyRef",
"type": "bytes32",
"internalType": "bytes32"
}
],
"internalType": "struct MortgageVault.Loan"
}
],
"stateMutability": "view"
},
{
"name": "getRoleAdmin",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "getVaultStatus",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "status",
"type": "tuple",
"components": [
{
"name": "outstandingPrincipal",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "accruedInterest",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "payoffAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "collateralUsdc",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "cltvBps",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "ltvBps",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "collateralLocked",
"type": "bool",
"internalType": "bool"
},
{
"name": "closed",
"type": "bool",
"internalType": "bool"
}
],
"internalType": "struct MortgageVault.Status"
}
],
"stateMutability": "view"
},
{
"name": "grantRole",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "account",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"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": "makePayment",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "matchesProperty",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "propertyAddress",
"type": "string",
"internalType": "string"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "pause",
"type": "function",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "paused",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "payoffAmount",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "refinanceVault",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "newRateBps",
"type": "uint32",
"internalType": "uint32"
},
{
"name": "newTermYears",
"type": "uint32",
"internalType": "uint32"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "releaseCollateral",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"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": "setCollateralVault",
"type": "function",
"inputs": [
{
"name": "vault",
"type": "address",
"internalType": "address"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setDeedRegistry",
"type": "function",
"inputs": [
{
"name": "registry",
"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": "totalLoans",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "totalOutstandingPrincipal",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "unaccountedBalance",
"type": "function",
"inputs": [],
"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"
}
]