Loading…
Native balance
0 tZROAR
Live from the node · no monetary value
Activity
4 transactions
First seen block 16,964 · last · nonce 1
Contract
Verified — exact match
The compiled bytecode matches the deployed bytecode exactly, including metadata.0x000000000000000000000000de8fc3b73e75987a7ff93300dc0b8abb6c26d4b1
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
/**
* @title MortgageDeed
* @notice A non-transferable on-chain record that a mortgage lien exists
* against a property, bound to the borrower of record.
*
* @dev Replaces the `VaultNFT` object the Sui `loan_vault::create_vault`
* minted. Two things about that design needed rethinking rather than
* transliterating:
*
* 1. THIS IS NOT A DEED OF TITLE. Title to real property is created and
* transferred by the county recorder, not by a token. What this token
* represents is an attestation: "at time T, loan L was originated
* against property P for borrower B, and the lien is live." Calling it
* a deed and letting it trade would invite someone to buy a token that
* conveys no property right whatsoever.
*
* 2. BECAUSE OF (1) IT IS SOULBOUND. A freely transferable token is
* actively harmful here. Transfer would detach the record from the
* borrower address the loan ledger pays out to and unlocks collateral
* for, so a transfer would either (a) mean nothing, leaving a token
* whose holder has no rights but a strong impression of having some, or
* (b) be read as assigning the mortgage, which requires recorded
* assignment and borrower notice off-chain. Both are worse than simply
* refusing. `_update` permits mint and burn only, and the approval
* entry points revert rather than letting a marketplace list something
* it can never deliver.
*
* The lien is a fact about the loan, so it is minted at origination and
* burned at payoff. Burning is the on-chain counterpart of a recorded
* release of lien; a borrower can point at the burn as evidence the
* servicer acknowledged payoff.
*/
contract MortgageDeed is ERC721, AccessControl {
using Strings for uint256;
/// @notice Held by the MortgageVault. May mint at origination, burn at payoff.
bytes32 public constant REGISTRAR_ROLE = keccak256("REGISTRAR_ROLE");
struct Lien {
bytes32 loanId;
address borrower;
uint64 originatedAt;
uint64 releasedAt;
bool released;
}
/// @dev tokenId => lien record. tokenId is uint256(loanId), so the loan id
/// and the token id are the same value in two types — there is exactly
/// one deed per loan and no counter to get out of step.
mapping(uint256 => Lien) private _liens;
string private _baseUri;
/// @notice Number of liens currently live (minted and not yet released).
uint256 public activeLiens;
event DeedMinted(bytes32 indexed loanId, address indexed borrower, uint256 indexed tokenId);
event DeedBurned(bytes32 indexed loanId, address indexed borrower, uint256 indexed tokenId);
event BaseUriChanged(string baseUri);
error DeedNotTransferable();
error DeedApprovalDisabled();
error DeedAlreadyExists(bytes32 loanId);
error DeedNotFound(bytes32 loanId);
error ZeroAddress();
constructor(address admin) ERC721("VaultToHome Mortgage Lien", "VTHLIEN") {
if (admin == address(0)) revert ZeroAddress();
_grantRole(DEFAULT_ADMIN_ROLE, admin);
}
// -------------------------------------------------------------- minting
/**
* @notice Records a live lien for a loan.
* @dev Only the registrar (the MortgageVault) may call this, so a lien
* record can never exist without a loan behind it.
*/
function mintDeed(bytes32 loanId, address borrower)
external
onlyRole(REGISTRAR_ROLE)
returns (uint256 tokenId)
{
if (borrower == address(0)) revert ZeroAddress();
tokenId = uint256(loanId);
if (_ownerOf(tokenId) != address(0)) revert DeedAlreadyExists(loanId);
_liens[tokenId] = Lien({
loanId: loanId,
borrower: borrower,
originatedAt: uint64(block.timestamp),
releasedAt: 0,
released: false
});
activeLiens += 1;
_safeMint(borrower, tokenId);
emit DeedMinted(loanId, borrower, tokenId);
}
/**
* @notice Releases the lien at payoff.
* @dev The Lien struct is kept after the burn rather than deleted. A
* released lien is history someone may need to prove later, and
* `releasedAt` is the proof; deleting it would refund a little gas and
* erase the record.
*/
function burnDeed(bytes32 loanId) external onlyRole(REGISTRAR_ROLE) {
uint256 tokenId = uint256(loanId);
address owner = _ownerOf(tokenId);
if (owner == address(0)) revert DeedNotFound(loanId);
Lien storage lien = _liens[tokenId];
lien.released = true;
lien.releasedAt = uint64(block.timestamp);
activeLiens -= 1;
_burn(tokenId);
emit DeedBurned(loanId, owner, tokenId);
}
// --------------------------------------------------------- soulbinding
/**
* @dev Mint (from == 0) and burn (to == 0) are allowed; every other move is
* refused. This is the single chokepoint in OZ 5.x — `transferFrom`,
* `safeTransferFrom` and any internal transfer all route through it,
* so there is no path around it.
*/
function _update(address to, uint256 tokenId, address auth)
internal
override
returns (address)
{
address from = _ownerOf(tokenId);
if (from != address(0) && to != address(0)) revert DeedNotTransferable();
return super._update(to, tokenId, auth);
}
/// @dev Refused so a marketplace cannot list a token it can never deliver.
function approve(address, uint256) public pure override {
revert DeedApprovalDisabled();
}
/// @dev Same reasoning as {approve}.
function setApprovalForAll(address, bool) public pure override {
revert DeedApprovalDisabled();
}
// --------------------------------------------------------------- views
function getLien(bytes32 loanId)
external
view
returns (address borrower, uint64 originatedAt, uint64 releasedAt, bool released)
{
Lien storage lien = _liens[uint256(loanId)];
if (lien.borrower == address(0)) revert DeedNotFound(loanId);
return (lien.borrower, lien.originatedAt, lien.releasedAt, lien.released);
}
/// @notice True while the lien is live (minted and not released).
function hasActiveLien(bytes32 loanId) external view returns (bool) {
return _ownerOf(uint256(loanId)) != address(0);
}
function tokenURI(uint256 tokenId) public view override returns (string memory) {
_requireOwned(tokenId);
return bytes(_baseUri).length == 0 ? "" : string.concat(_baseUri, tokenId.toString());
}
// --------------------------------------------------------------- admin
function setBaseUri(string calldata newBaseUri) external onlyRole(DEFAULT_ADMIN_ROLE) {
_baseUri = newBaseUri;
emit BaseUriChanged(newBaseUri);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, AccessControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
Bytecode hash 0x30232d21327adcc50defbec4dca4d5fbc6ef5403d47550aecef86399dbe1a24b
[
{
"type": "constructor",
"inputs": [
{
"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": "DeedAlreadyExists",
"type": "error",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
]
},
{
"name": "DeedApprovalDisabled",
"type": "error",
"inputs": []
},
{
"name": "DeedNotFound",
"type": "error",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
]
},
{
"name": "DeedNotTransferable",
"type": "error",
"inputs": []
},
{
"name": "ERC721IncorrectOwner",
"type": "error",
"inputs": [
{
"name": "sender",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "owner",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "ERC721InsufficientApproval",
"type": "error",
"inputs": [
{
"name": "operator",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "ERC721InvalidApprover",
"type": "error",
"inputs": [
{
"name": "approver",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "ERC721InvalidOperator",
"type": "error",
"inputs": [
{
"name": "operator",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "ERC721InvalidOwner",
"type": "error",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "ERC721InvalidReceiver",
"type": "error",
"inputs": [
{
"name": "receiver",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "ERC721InvalidSender",
"type": "error",
"inputs": [
{
"name": "sender",
"type": "address",
"internalType": "address"
}
]
},
{
"name": "ERC721NonexistentToken",
"type": "error",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"name": "ZeroAddress",
"type": "error",
"inputs": []
},
{
"name": "Approval",
"type": "event",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "ApprovalForAll",
"type": "event",
"inputs": [
{
"name": "owner",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "operator",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "approved",
"type": "bool",
"indexed": false,
"internalType": "bool"
}
],
"anonymous": false
},
{
"name": "BaseUriChanged",
"type": "event",
"inputs": [
{
"name": "baseUri",
"type": "string",
"indexed": false,
"internalType": "string"
}
],
"anonymous": false
},
{
"name": "DeedBurned",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "DeedMinted",
"type": "event",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"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": "Transfer",
"type": "event",
"inputs": [
{
"name": "from",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "to",
"type": "address",
"indexed": true,
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"name": "DEFAULT_ADMIN_ROLE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "REGISTRAR_ROLE",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"name": "activeLiens",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "approve",
"type": "function",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
},
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "pure"
},
{
"name": "balanceOf",
"type": "function",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"name": "burnDeed",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "getApproved",
"type": "function",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"name": "getLien",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "borrower",
"type": "address",
"internalType": "address"
},
{
"name": "originatedAt",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "releasedAt",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "released",
"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": "hasActiveLien",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"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": "isApprovedForAll",
"type": "function",
"inputs": [
{
"name": "owner",
"type": "address",
"internalType": "address"
},
{
"name": "operator",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "mintDeed",
"type": "function",
"inputs": [
{
"name": "loanId",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "borrower",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"name": "name",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "ownerOf",
"type": "function",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"stateMutability": "view"
},
{
"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": "safeTransferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "safeTransferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "data",
"type": "bytes",
"internalType": "bytes"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "setApprovalForAll",
"type": "function",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
},
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"outputs": [],
"stateMutability": "pure"
},
{
"name": "setBaseUri",
"type": "function",
"inputs": [
{
"name": "newBaseUri",
"type": "string",
"internalType": "string"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"name": "supportsInterface",
"type": "function",
"inputs": [
{
"name": "interfaceId",
"type": "bytes4",
"internalType": "bytes4"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"name": "symbol",
"type": "function",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "tokenURI",
"type": "function",
"inputs": [
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"name": "transferFrom",
"type": "function",
"inputs": [
{
"name": "from",
"type": "address",
"internalType": "address"
},
{
"name": "to",
"type": "address",
"internalType": "address"
},
{
"name": "tokenId",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
}
]