Loading…
Contract
Native balance
0 tZROAR
Live from the node · no monetary value
Activity
8 transactions
First seen block 16,962 · 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";
/**
* @title EscrowVault
* @notice Mortgage escrow in USDC on Base. Replaces the Sui Move module
* `escrow_vault` (create_escrow_vault, deposit_monthly_escrow).
*
* @dev Escrow holds a borrower's property tax and insurance money. That gives
* this contract one overriding property: escrow funds are SEGREGATED and
* are never lent, staked or invested. There is deliberately no yield
* strategy, no deposit into a lending market, and no owner path that can
* move a borrower's balance anywhere except an approved payee or back to
* the borrower. Anything else would put a tax bill at market risk.
*
* Per-borrower accounting is explicit rather than relying on this
* contract's token balance, so one borrower's funds can never be paid out
* on behalf of another.
*/
contract EscrowVault is AccessControl, ReentrancyGuard, Pausable {
using SafeERC20 for IERC20;
/// @notice May disburse escrow to an approved payee (the servicer).
bytes32 public constant SERVICER_ROLE = keccak256("SERVICER_ROLE");
/// @notice May halt deposits and disbursements in an incident.
bytes32 public constant GUARDIAN_ROLE = keccak256("GUARDIAN_ROLE");
/// @notice The only token this vault accepts: native Circle USDC on Base.
IERC20 public immutable usdc;
struct Vault {
address borrower;
uint256 balance;
uint256 monthlyTaxAmount;
uint256 monthlyInsuranceAmount;
uint256 totalDeposited;
uint256 totalDisbursed;
bool exists;
bool closed;
}
/// @dev mortgageId => vault. The id is assigned off-chain by the servicer.
mapping(bytes32 => Vault) private _vaults;
/// @dev Payees escrow may be disbursed to (tax authority, insurer).
mapping(address => bool) public approvedPayees;
/**
* @dev Sum of every vault balance. Compared against the actual token
* balance so a shortfall or an accidental transfer in is detectable
* rather than silently absorbed into someone's escrow.
*/
uint256 public totalEscrowed;
event VaultCreated(bytes32 indexed mortgageId, address indexed borrower, uint256 monthlyTax, uint256 monthlyInsurance);
event MonthlyEscrowDeposited(bytes32 indexed mortgageId, address indexed from, uint256 amount, uint256 newBalance);
event EscrowDisbursed(bytes32 indexed mortgageId, address indexed payee, uint256 amount, string purpose);
event EscrowRefunded(bytes32 indexed mortgageId, address indexed borrower, uint256 amount);
event VaultClosed(bytes32 indexed mortgageId, uint256 refunded);
event PayeeApprovalChanged(address indexed payee, bool approved);
error VaultAlreadyExists();
error VaultNotFound();
error VaultIsClosed();
error ZeroAmount();
error ZeroAddress();
error InsufficientEscrow(uint256 requested, uint256 available);
error PayeeNotApproved(address payee);
error NotBorrower();
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);
}
// ---------------------------------------------------------------- vaults
/**
* @notice Opens an escrow account for a mortgage.
* @dev Replaces `escrow_vault::create_escrow_vault`.
*/
function createEscrowVault(
bytes32 mortgageId,
address borrower,
uint256 monthlyTaxAmount,
uint256 monthlyInsuranceAmount
) external onlyRole(SERVICER_ROLE) whenNotPaused {
if (borrower == address(0)) revert ZeroAddress();
if (_vaults[mortgageId].exists) revert VaultAlreadyExists();
_vaults[mortgageId] = Vault({
borrower: borrower,
balance: 0,
monthlyTaxAmount: monthlyTaxAmount,
monthlyInsuranceAmount: monthlyInsuranceAmount,
totalDeposited: 0,
totalDisbursed: 0,
exists: true,
closed: false
});
emit VaultCreated(mortgageId, borrower, monthlyTaxAmount, monthlyInsuranceAmount);
}
/**
* @notice Deposits the monthly escrow portion of a payment.
* @dev Replaces `escrow_vault::deposit_monthly_escrow`. Anyone may deposit
* on a borrower's behalf — a family member or employer paying in is a
* supported case — but funds are only ever credited to the named
* mortgage.
*/
function depositMonthlyEscrow(bytes32 mortgageId, uint256 amount)
external
nonReentrant
whenNotPaused
{
if (amount == 0) revert ZeroAmount();
Vault storage v = _vaults[mortgageId];
if (!v.exists) revert VaultNotFound();
if (v.closed) revert VaultIsClosed();
// Credited from the measured delta rather than the requested amount, so
// a fee-on-transfer or rebasing token cannot credit more than arrived.
uint256 before = usdc.balanceOf(address(this));
usdc.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = usdc.balanceOf(address(this)) - before;
v.balance += received;
v.totalDeposited += received;
totalEscrowed += received;
emit MonthlyEscrowDeposited(mortgageId, msg.sender, received, v.balance);
}
/**
* @notice Pays escrow to an approved payee — a tax authority or insurer.
* @dev The payee allowlist is what stops escrow leaving for an arbitrary
* address, including one controlled by an administrator.
*/
function disburse(
bytes32 mortgageId,
address payee,
uint256 amount,
string calldata purpose
) external onlyRole(SERVICER_ROLE) nonReentrant whenNotPaused {
if (amount == 0) revert ZeroAmount();
if (!approvedPayees[payee]) revert PayeeNotApproved(payee);
Vault storage v = _vaults[mortgageId];
if (!v.exists) revert VaultNotFound();
if (v.closed) revert VaultIsClosed();
if (amount > v.balance) revert InsufficientEscrow(amount, v.balance);
v.balance -= amount;
v.totalDisbursed += amount;
totalEscrowed -= amount;
usdc.safeTransfer(payee, amount);
emit EscrowDisbursed(mortgageId, payee, amount, purpose);
}
/**
* @notice Returns surplus escrow to the borrower.
* @dev An escrow analysis showing an overage must be able to pay the
* borrower back; the money is theirs.
*/
function refundToBorrower(bytes32 mortgageId, uint256 amount)
external
onlyRole(SERVICER_ROLE)
nonReentrant
{
if (amount == 0) revert ZeroAmount();
Vault storage v = _vaults[mortgageId];
if (!v.exists) revert VaultNotFound();
if (amount > v.balance) revert InsufficientEscrow(amount, v.balance);
v.balance -= amount;
totalEscrowed -= amount;
usdc.safeTransfer(v.borrower, amount);
emit EscrowRefunded(mortgageId, v.borrower, amount);
}
/**
* @notice Closes an escrow account at payoff, returning whatever remains.
* @dev Deliberately not pausable: a borrower who has repaid must be able to
* recover their own money even during an incident pause.
*/
function closeVault(bytes32 mortgageId) external onlyRole(SERVICER_ROLE) nonReentrant {
Vault storage v = _vaults[mortgageId];
if (!v.exists) revert VaultNotFound();
if (v.closed) revert VaultIsClosed();
uint256 remaining = v.balance;
v.balance = 0;
v.closed = true;
if (remaining > 0) {
totalEscrowed -= remaining;
usdc.safeTransfer(v.borrower, remaining);
}
emit VaultClosed(mortgageId, remaining);
}
// ----------------------------------------------------------- admin
function setPayeeApproval(address payee, bool approved) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (payee == address(0)) revert ZeroAddress();
approvedPayees[payee] = approved;
emit PayeeApprovalChanged(payee, approved);
}
function pause() external onlyRole(GUARDIAN_ROLE) {
_pause();
}
function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}
// ------------------------------------------------------------- views
function getVault(bytes32 mortgageId)
external
view
returns (
address borrower,
uint256 balance,
uint256 monthlyTaxAmount,
uint256 monthlyInsuranceAmount,
uint256 totalDeposited,
uint256 totalDisbursed,
bool closed
)
{
Vault storage v = _vaults[mortgageId];
if (!v.exists) revert VaultNotFound();
return (v.borrower, v.balance, v.monthlyTaxAmount, v.monthlyInsuranceAmount, v.totalDeposited, v.totalDisbursed, v.closed);
}
function monthlyEscrowDue(bytes32 mortgageId) external view returns (uint256) {
Vault storage v = _vaults[mortgageId];
if (!v.exists) revert VaultNotFound();
return v.monthlyTaxAmount + v.monthlyInsuranceAmount;
}
/**
* @notice USDC held here beyond what the ledger accounts for.
* @dev Should be zero. A non-zero value means tokens were sent in directly
* and belong to nobody — surfaced rather than quietly credited.
*/
function unaccountedBalance() external view returns (uint256) {
uint256 held = usdc.balanceOf(address(this));
return held > totalEscrowed ? held - totalEscrowed : 0;
}
}
Bytecode hash 0x1cf763fa93a487dfe5108c2a76489f14291c74d0114a35d48edf2916ff514644
[
{
"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": "EnforcedPause",
"type": "error",
"inputs": []
},
{
"name": "ExpectedPause",
"type": "error",
"inputs": []
},
{
"name": "InsufficientEscrow",
"type": "error",
"inputs": [
{
"name": "requested",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "available",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "NotBorrower",
"type": "error",
"inputs": []
},
{
"name": "PayeeNotApproved",
"type": "error",
"inputs": [
{
"name": "payee",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "ReentrancyGuardReentrantCall",
"type": "error",
"inputs": []
},
{
"name": "SafeERC20FailedOperation",
"type": "error",
"inputs": [
{
"name": "token",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "VaultAlreadyExists",
"type": "error",
"inputs": []
},
{
"name": "VaultIsClosed",
"type": "error",
"inputs": []
},
{
"name": "VaultNotFound",
"type": "error",
"inputs": []
},
{
"name": "ZeroAddress",
"type": "error",
"inputs": []
},
{
"name": "ZeroAmount",
"type": "error",
"inputs": []
},
{
"name": "EscrowDisbursed",
"type": "event",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "payee",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "purpose",
"type": "string",
"indexed": false,
"internalType": "string"
}
],
"anonymous": false
},
{
"name": "EscrowRefunded",
"type": "event",
"inputs": [
{
"name": "mortgageId",
"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": "MonthlyEscrowDeposited",
"type": "event",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "from",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "newBalance",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "Paused",
"type": "event",
"inputs": [
{
"name": "account",
"type": "address",
"indexed": false,
"internalType": "address"
}
],
"anonymous": false
},
{
"name": "PayeeApprovalChanged",
"type": "event",
"inputs": [
{
"name": "payee",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"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": "VaultClosed",
"type": "event",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "refunded",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "VaultCreated",
"type": "event",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "monthlyTax",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "monthlyInsurance",
"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": "SERVICER_ROLE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "approvedPayees",
"type": "function",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "closeVault",
"type": "function",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "createEscrowVault",
"type": "function",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"internalType": "address"
},
{
"name": "monthlyTaxAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "monthlyInsuranceAmount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "depositMonthlyEscrow",
"type": "function",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "disburse",
"type": "function",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "payee",
"type": "address",
"internalType": "address"
},
{
"name": "amount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "purpose",
"type": "string",
"internalType": "string"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "getRoleAdmin",
"type": "function",
"inputs": [
{
"name": "role",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "getVault",
"type": "function",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "borrower",
"type": "address",
"internalType": "address"
},
{
"name": "balance",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "monthlyTaxAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "monthlyInsuranceAmount",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "totalDeposited",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "totalDisbursed",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "closed",
"type": "bool",
"internalType": "bool"
}
],
"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": "monthlyEscrowDue",
"type": "function",
"inputs": [
{
"name": "mortgageId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "pause",
"type": "function",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "paused",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "refundToBorrower",
"type": "function",
"inputs": [
{
"name": "mortgageId",
"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": "setPayeeApproval",
"type": "function",
"inputs": [
{
"name": "payee",
"type": "address",
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "supportsInterface",
"type": "function",
"inputs": [
{
"name": "interfaceId",
"type": "bytes4",
"internalType": "bytes4"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "totalEscrowed",
"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"
}
]