Purpose: Final contract functions, params, events, revert conditions. Prevents: Frontend–contract mismatch. Update rule: Once, after contracts are finalized on Day 2. Never again. Authority: If this file conflicts with ARCHITECTURE.md, this file wins for contract specifics.
Fill in after Day 2 deploy. Never leave blank when going to demo.
Network: Base Sepolia (chainId: 84532)
CertificateRegistry: 0xBcF8f15E2c981663A08Db3878B994d65ddd84944
LeaseManager: 0x1dEcC3fBa8fbc2eb04394Ac5cC6A9497BF9E7a00
AnonAadhaar Verifier: 0xA205f7DED9430ac03b7F0CD3eA1b22C54C1A1453 (AnonAadhaarZKVerifier)
MockAnonAadhaar: 0x68AACB01AaeD9cAC1D46aD248F35cBd2F554F7D0 (Base Sepolia stand-in)
Deployer / Issuer Wallet: 0xCaE780beffd68d4F3a9d0DAbC2Dcb66858aCFdf2
Basescan (Registry): https://sepolia.basescan.org/address/0xBcF8f15E2c981663A08Db3878B994d65ddd84944
Basescan (LeaseManager): https://sepolia.basescan.org/address/0x1dEcC3fBa8fbc2eb04394Ac5cC6A9497BF9E7a00
{
"solidity": "0.8.28",
"optimizer": { "enabled": true, "runs": 200 },
"viaIR": false
}Inherits: ERC721, ReentrancyGuard, Ownable
Standard: ERC-5192 (Minimal Soulbound Token)
Rule: locked() always returns true. transferFrom / safeTransferFrom always revert.
struct Certificate {
address owner; // wallet that owns this cert
bytes32 attributeKey; // keccak256 of attribute name string
uint8 confidenceLevel; // 0–100 integer (e.g. 91 = 91%)
uint40 issuedAt; // unix timestamp
uint40 expiresAt; // unix timestamp; 0 = no expiry
address issuer; // issuer wallet address
bool revoked;
}mapping(uint256 tokenId => Certificate) public certificates;
mapping(address owner => mapping(bytes32 attrKey => uint256 tokenId)) public ownerAttrToken;
mapping(address => bool) public authorizedIssuers;
uint256 private _tokenIdCounter; // starts at 1function mintCertificate(
address owner, // wallet receiving the SBT
bytes32 attributeKey, // keccak256("age_range") etc.
uint8 confidenceLevel, // 0–100
uint40 expiresAt // 0 = no expiry
)
external
onlyIssuer
returns (uint256 tokenId)Behaviour:
- Increments
_tokenIdCounterand assigns astokenId - Writes
Certificatestruct tocertificates[tokenId] - Writes
tokenIdtoownerAttrToken[owner][attributeKey] - Mints ERC-721 token to
owner - Emits
CertificateMinted
Reverts:
| Condition | Revert message |
|---|---|
msg.sender not in authorizedIssuers |
"Not authorized" |
owner == address(0) |
"Invalid owner" |
confidenceLevel > 100 |
"Invalid confidence" |
ownerAttrToken[owner][attributeKey] != 0 and existing cert not revoked |
"Cert already exists" |
function revokeCertificate(uint256 tokenId)
external
onlyIssuerBehaviour:
- Sets
certificates[tokenId].revoked = true - Emits
CertificateRevoked - Does NOT burn the token — token remains minted, just flagged revoked
Reverts:
| Condition | Revert message |
|---|---|
msg.sender not in authorizedIssuers |
"Not authorized" |
tokenId does not exist |
"Token does not exist" |
| Already revoked | "Already revoked" |
function addIssuer(address issuer) external onlyOwner
function removeIssuer(address issuer) external onlyOwnerReverts: msg.sender != owner → OZ Ownable default revert.
function locked(uint256 tokenId) external pure returns (bool)
// Always returns true. No exceptions.// Both overrides:
revert("Soulbound: non-transferable");function getCertificate(uint256 tokenId)
external view
returns (Certificate memory)Reverts: Token does not exist → "Token does not exist"
function getTokenId(address owner, bytes32 attributeKey)
external view
returns (uint256 tokenId)
// Returns 0 if no cert exists for this owner+attributeKey combination.function isValid(uint256 tokenId)
external view
returns (bool)
// Returns true if: token exists AND certificate.revoked == false
// AND (certificate.expiresAt == 0 OR block.timestamp < certificate.expiresAt)event CertificateMinted(
uint256 indexed tokenId,
address indexed owner,
bytes32 indexed attributeKey,
uint8 confidence,
uint40 issuedAt
);
event CertificateRevoked(
uint256 indexed tokenId,
address indexed owner,
bytes32 indexed attributeKey
);
event IssuerAdded(address indexed issuer);
event IssuerRemoved(address indexed issuer);modifier onlyIssuer() {
require(authorizedIssuers[msg.sender], "Not authorized");
_;
}Inherits: ReentrancyGuard, Ownable
Payment token: Native ETH (demo). Production: ERC-20 USDC.
Pattern: Pull-over-push escrow. State machine per lease.
enum LeaseStatus { Funded, Active, Settled, Revoked, Cancelled }| Value | Meaning |
|---|---|
Funded |
Buyer deposited escrow. No user has approved yet for this slot. |
Active |
User approved. Payment earmarked. Lease running. |
Settled |
Lease expired. Payment released to user. |
Revoked |
User revoked early. Payment forfeited. |
Cancelled |
Buyer withdrew unfilled escrow after requestExpiry. |
struct LeaseRequest {
address buyer;
bytes32 attributeKey;
uint8 minConfidence; // 0–100
bool aiAllowed; // buyer opts into AI fallback tier
uint256 pricePerUser; // wei per lease slot
uint40 leaseDurationSec; // lease length in seconds
uint40 requestExpiry; // unix ts; request auto-cancels if unfilled
uint256 escrowBalance; // remaining unfilled ETH held
uint256 maxUsers; // max lease slots
uint256 filledCount; // approved lease slots so far
bool active; // false after requestExpiry or fully filled
}
struct Lease {
uint256 requestId;
address user;
uint256 certificateTokenId;
LeaseStatus status;
uint40 startedAt;
uint40 expiresAt;
uint256 paidAmount; // wei held for this lease slot
}mapping(uint256 requestId => LeaseRequest) public leaseRequests;
mapping(uint256 leaseId => Lease) public leases;
mapping(uint256 requestId => mapping(address user => bool)) public requestFilledByUser;
uint256 private _requestIdCounter; // starts at 1
uint256 private _leaseIdCounter; // starts at 1
address public certificateRegistry; // set in constructor, immutable afterfunction postRequest(
bytes32 attrKey,
uint8 minConf, // 0–100
bool aiAllowed,
uint256 pricePerUser, // wei
uint40 duration, // seconds
uint40 reqExpiry, // unix timestamp
uint256 maxUsers
)
external
payable
returns (uint256 requestId)Behaviour:
msg.valuemust equalpricePerUser * maxUsers- Increments
_requestIdCounter, storesLeaseRequest - Sets
escrowBalance = msg.value - Emits
RequestPosted
Reverts:
| Condition | Revert message |
|---|---|
msg.value != pricePerUser * maxUsers |
"Incorrect escrow amount" |
maxUsers == 0 |
"maxUsers must be > 0" |
pricePerUser == 0 |
"pricePerUser must be > 0" |
reqExpiry <= block.timestamp |
"Request already expired" |
duration == 0 |
"Duration must be > 0" |
function approveLease(
uint256 requestId,
uint256 certificateTokenId
)
external
nonReentrant
returns (uint256 leaseId)Validation sequence (enforced in this order):
leaseRequests[requestId].buyer != address(0)→"Request does not exist"leaseRequests[requestId].active == true→"Request not active"block.timestamp <= leaseRequests[requestId].requestExpiry→"Request expired"ICertificateRegistry(certificateRegistry).isValid(certificateTokenId)→"Certificate invalid or revoked"certificates[certificateTokenId].owner == msg.sender→"Not certificate owner"certificates[certificateTokenId].attributeKey == leaseRequests[requestId].attributeKey→"Attribute mismatch"certificates[certificateTokenId].confidenceLevel >= leaseRequests[requestId].minConfidence→"Confidence too low"- If cert method requires
aiAllowedcheck (verified via cert'sissueror a flag on cert):leaseRequests[requestId].aiAllowed == true→"AI certs not accepted" !requestFilledByUser[requestId][msg.sender]→"Already approved"leaseRequests[requestId].filledCount < leaseRequests[requestId].maxUsers→"Request fully filled"
Behaviour after validation:
- Increments
_leaseIdCounter, createsLease{status: Active} - Decrements
leaseRequests[requestId].escrowBalancebypricePerUser - Sets
requestFilledByUser[requestId][msg.sender] = true - Increments
leaseRequests[requestId].filledCount - If
filledCount == maxUsers: setsactive = false - Emits
LeaseApproved - Returns
leaseId
Note on AI cert check (step 8):
CertificateRegistrystoresissuerper cert. The backend issues AI-tier certs from a dedicated sub-address flagged asAI_ISSUER.LeaseManagerstoresaiIssuerAddressand checkscert.issuer == aiIssuerAddressto identify AI-tier certs. Alternatively, add abool aiDerivedfield toCertificatestruct set by the issuer at mint time.
function settleLease(uint256 leaseId)
external
nonReentrantBehaviour:
- Validates
leases[leaseId].status == Active→"Lease not active" - Validates
block.timestamp >= leases[leaseId].expiresAt→"Lease not yet expired" - Sets
leases[leaseId].status = Settled - Transfers
leases[leaseId].paidAmounttoleases[leaseId].userviacall{value} - Emits
LeaseSettled - Caller: Anyone can call this after expiry (permissionless settlement). Demo: Vercel cron. Production: Chainlink Automation.
ETH transfer pattern (mandatory):
(bool sent, ) = payable(leases[leaseId].user).call{value: leases[leaseId].paidAmount}("");
require(sent, "ETH transfer failed");Reverts:
| Condition | Revert message |
|---|---|
status != Active |
"Lease not active" |
block.timestamp < expiresAt |
"Lease not yet expired" |
| ETH transfer fails | "ETH transfer failed" |
function revokeLease(uint256 leaseId)
external
nonReentrantBehaviour:
- Validates
msg.sender == leases[leaseId].user→"Not lease owner" - Validates
leases[leaseId].status == Active→"Lease not active" - Sets
leases[leaseId].status = Revoked paidAmountstays in contract (claimable by protocol treasury or returned to buyer — TBD for production; for demo, stays in contract)- Emits
LeaseRevoked
Reverts:
| Condition | Revert message |
|---|---|
msg.sender != lease.user |
"Not lease owner" |
status != Active |
"Lease not active" |
function withdrawUnfilledEscrow(uint256 requestId)
external
nonReentrantBehaviour:
- Validates
msg.sender == leaseRequests[requestId].buyer→"Not request owner" - Validates
block.timestamp > leaseRequests[requestId].requestExpiry→"Request not yet expired" - Validates
leaseRequests[requestId].escrowBalance > 0→"No escrow to withdraw" - Transfers
escrowBalanceto buyer viacall{value} - Sets
escrowBalance = 0,active = false - Emits
RequestExpired
Reverts:
| Condition | Revert message |
|---|---|
msg.sender != request.buyer |
"Not request owner" |
block.timestamp <= requestExpiry |
"Request not yet expired" |
escrowBalance == 0 |
"No escrow to withdraw" |
| ETH transfer fails | "ETH transfer failed" |
function setCertificateRegistry(address registry)
external
onlyOwnerCalled once at deploy. Sets the certificateRegistry address used in approveLease validation.
function getRequest(uint256 requestId)
external view
returns (LeaseRequest memory)
function getLease(uint256 leaseId)
external view
returns (Lease memory)
function hasUserFilledRequest(uint256 requestId, address user)
external view
returns (bool)event RequestPosted(
uint256 indexed requestId,
address indexed buyer,
bytes32 indexed attrKey,
uint256 pricePerUser,
uint256 maxUsers,
uint40 requestExpiry
);
event LeaseApproved(
uint256 indexed leaseId,
uint256 indexed requestId,
address indexed user,
bytes32 attrKey,
uint8 confidence,
uint40 expiresAt
);
event LeaseSettled(
uint256 indexed leaseId,
address indexed user,
uint256 amount
);
event LeaseRevoked(
uint256 indexed leaseId,
uint256 indexed requestId,
address indexed user
);
event RequestExpired(
uint256 indexed requestId,
address indexed buyer,
uint256 escrowReturned
);Implemented by each ZK provider's on-chain verifier. Not deployed by DataDaddy.
// interfaces/IZKVerifier.sol
interface IZKVerifier {
/// @param proof Encoded proof bytes (provider-specific format)
/// @param context Provider-specific context (nullifier seed, appId, etc.)
/// @return valid Whether the proof cryptographically verifies
/// @return attributeKey keccak256 attribute this proof attests to
/// @return confidence 0–100 (always 100 for valid ZK proofs)
function verifyProof(
bytes calldata proof,
bytes calldata context
)
external view
returns (bool valid, bytes32 attributeKey, uint8 confidence);
}All attribute keys are keccak256 hashes of their canonical string name. Use these constants everywhere — contracts, backend, frontend. Never hardcode raw bytes32 values inline.
// In Solidity:
bytes32 constant DEFI_USER = keccak256("defi_user");
bytes32 constant ASSET_HOLDER = keccak256("asset_holder");
bytes32 constant ACTIVE_WALLET = keccak256("active_wallet");
bytes32 constant LONG_TERM_HOLDER = keccak256("long_term_holder");
bytes32 constant NFT_HOLDER = keccak256("nft_holder");
bytes32 constant AGE_RANGE = keccak256("age_range");
bytes32 constant STATE_OF_RESIDENCE = keccak256("state_of_residence");// In TypeScript (viem):
import { keccak256, toHex } from "viem";
export const ATTRIBUTE_KEYS = {
defi_user: keccak256(toHex("defi_user")),
asset_holder: keccak256(toHex("asset_holder")),
active_wallet: keccak256(toHex("active_wallet")),
long_term_holder: keccak256(toHex("long_term_holder")),
nft_holder: keccak256(toHex("nft_holder")),
age_range: keccak256(toHex("age_range")),
state_of_residence: keccak256(toHex("state_of_residence")),
} as const;Critical:
keccak256("age_range")in Solidity andkeccak256(toHex("age_range"))in viem must produce identicalbytes32values. Verify this in a unit test before any integration work.
import { useReadContract } from "wagmi";
import { CERTIFICATE_REGISTRY } from "@/shared/contracts";
import { ATTRIBUTE_KEYS } from "@/shared/attributeKeys";
// Get tokenId for a wallet + attribute
const { data: tokenId } = useReadContract({
...CERTIFICATE_REGISTRY,
functionName: "getTokenId",
args: [address, ATTRIBUTE_KEYS.defi_user],
});
// Check if valid
const { data: isValid } = useReadContract({
...CERTIFICATE_REGISTRY,
functionName: "isValid",
args: [tokenId],
enabled: !!tokenId && tokenId !== 0n,
});
// Get full cert struct
const { data: cert } = useReadContract({
...CERTIFICATE_REGISTRY,
functionName: "getCertificate",
args: [tokenId],
enabled: !!tokenId && tokenId !== 0n,
});import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { parseEther } from "viem";
const { writeContract, data: hash } = useWriteContract();
const { isSuccess, isLoading: isConfirming } = useWaitForTransactionReceipt({ hash });
writeContract({
...LEASE_MANAGER,
functionName: "postRequest",
args: [
ATTRIBUTE_KEYS.defi_user, // attrKey bytes32
80n, // minConf uint8
false, // aiAllowed bool
parseEther("0.001"), // pricePerUser uint256 wei
86400n, // duration uint40 seconds
BigInt(Math.floor(Date.now() / 1000) + 86400 * 7), // reqExpiry
10n, // maxUsers
],
value: parseEther("0.001") * 10n, // pricePerUser * maxUsers
});const { writeContract, data: hash } = useWriteContract();
const { isSuccess, isLoading: isConfirming } = useWaitForTransactionReceipt({ hash });
writeContract({
...LEASE_MANAGER,
functionName: "approveLease",
args: [
BigInt(requestId), // requestId
BigInt(certificateTokenId), // certificateTokenId
],
});writeContract({
...LEASE_MANAGER,
functionName: "revokeLease",
args: [BigInt(leaseId)],
});writeContract({
...LEASE_MANAGER,
functionName: "settleLease",
args: [BigInt(leaseId)],
});import { useWatchContractEvent } from "wagmi";
// Watch for new lease approvals
useWatchContractEvent({
...LEASE_MANAGER,
eventName: "LeaseApproved",
args: { user: address },
onLogs(logs) {
// refetch active leases
},
});Minimal ABI for frontend if TypeChain isn't available. Use generated types where possible.
export const CERTIFICATE_REGISTRY_ABI = [
{
name: "mintCertificate",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "owner", type: "address" },
{ name: "attributeKey", type: "bytes32" },
{ name: "confidenceLevel", type: "uint8" },
{ name: "expiresAt", type: "uint40" },
],
outputs: [{ name: "tokenId", type: "uint256" }],
},
{
name: "revokeCertificate",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "tokenId", type: "uint256" }],
outputs: [],
},
{
name: "getTokenId",
type: "function",
stateMutability: "view",
inputs: [
{ name: "owner", type: "address" },
{ name: "attributeKey", type: "bytes32" },
],
outputs: [{ name: "tokenId", type: "uint256" }],
},
{
name: "isValid",
type: "function",
stateMutability: "view",
inputs: [{ name: "tokenId", type: "uint256" }],
outputs: [{ name: "", type: "bool" }],
},
{
name: "getCertificate",
type: "function",
stateMutability: "view",
inputs: [{ name: "tokenId", type: "uint256" }],
outputs: [
{
name: "",
type: "tuple",
components: [
{ name: "owner", type: "address" },
{ name: "attributeKey", type: "bytes32" },
{ name: "confidenceLevel", type: "uint8" },
{ name: "issuedAt", type: "uint40" },
{ name: "expiresAt", type: "uint40" },
{ name: "issuer", type: "address" },
{ name: "revoked", type: "bool" },
],
},
],
},
{
name: "locked",
type: "function",
stateMutability: "pure",
inputs: [{ name: "tokenId", type: "uint256" }],
outputs: [{ name: "", type: "bool" }],
},
{
name: "CertificateMinted",
type: "event",
inputs: [
{ name: "tokenId", type: "uint256", indexed: true },
{ name: "owner", type: "address", indexed: true },
{ name: "attributeKey", type: "bytes32", indexed: true },
{ name: "confidence", type: "uint8", indexed: false },
{ name: "issuedAt", type: "uint40", indexed: false },
],
},
{
name: "CertificateRevoked",
type: "event",
inputs: [
{ name: "tokenId", type: "uint256", indexed: true },
{ name: "owner", type: "address", indexed: true },
{ name: "attributeKey", type: "bytes32", indexed: true },
],
},
] as const;
export const LEASE_MANAGER_ABI = [
{
name: "postRequest",
type: "function",
stateMutability: "payable",
inputs: [
{ name: "attrKey", type: "bytes32" },
{ name: "minConf", type: "uint8" },
{ name: "aiAllowed", type: "bool" },
{ name: "pricePerUser", type: "uint256" },
{ name: "duration", type: "uint40" },
{ name: "reqExpiry", type: "uint40" },
{ name: "maxUsers", type: "uint256" },
],
outputs: [{ name: "requestId", type: "uint256" }],
},
{
name: "approveLease",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "requestId", type: "uint256" },
{ name: "certificateTokenId", type: "uint256" },
],
outputs: [{ name: "leaseId", type: "uint256" }],
},
{
name: "settleLease",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "leaseId", type: "uint256" }],
outputs: [],
},
{
name: "revokeLease",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "leaseId", type: "uint256" }],
outputs: [],
},
{
name: "withdrawUnfilledEscrow",
type: "function",
stateMutability: "nonpayable",
inputs: [{ name: "requestId", type: "uint256" }],
outputs: [],
},
{
name: "getRequest",
type: "function",
stateMutability: "view",
inputs: [{ name: "requestId", type: "uint256" }],
outputs: [
{
name: "",
type: "tuple",
components: [
{ name: "buyer", type: "address" },
{ name: "attributeKey", type: "bytes32" },
{ name: "minConfidence", type: "uint8" },
{ name: "aiAllowed", type: "bool" },
{ name: "pricePerUser", type: "uint256" },
{ name: "leaseDurationSec", type: "uint40" },
{ name: "requestExpiry", type: "uint40" },
{ name: "escrowBalance", type: "uint256" },
{ name: "maxUsers", type: "uint256" },
{ name: "filledCount", type: "uint256" },
{ name: "active", type: "bool" },
],
},
],
},
{
name: "getLease",
type: "function",
stateMutability: "view",
inputs: [{ name: "leaseId", type: "uint256" }],
outputs: [
{
name: "",
type: "tuple",
components: [
{ name: "requestId", type: "uint256" },
{ name: "user", type: "address" },
{ name: "certificateTokenId", type: "uint256" },
{ name: "status", type: "uint8" },
{ name: "startedAt", type: "uint40" },
{ name: "expiresAt", type: "uint40" },
{ name: "paidAmount", type: "uint256" },
],
},
],
},
{
name: "LeaseApproved",
type: "event",
inputs: [
{ name: "leaseId", type: "uint256", indexed: true },
{ name: "requestId", type: "uint256", indexed: true },
{ name: "user", type: "address", indexed: true },
{ name: "attrKey", type: "bytes32", indexed: false },
{ name: "confidence", type: "uint8", indexed: false },
{ name: "expiresAt", type: "uint40", indexed: false },
],
},
{
name: "LeaseSettled",
type: "event",
inputs: [
{ name: "leaseId", type: "uint256", indexed: true },
{ name: "user", type: "address", indexed: true },
{ name: "amount", type: "uint256", indexed: false },
],
},
{
name: "LeaseRevoked",
type: "event",
inputs: [
{ name: "leaseId", type: "uint256", indexed: true },
{ name: "requestId", type: "uint256", indexed: true },
{ name: "user", type: "address", indexed: true },
],
},
{
name: "RequestPosted",
type: "event",
inputs: [
{ name: "requestId", type: "uint256", indexed: true },
{ name: "buyer", type: "address", indexed: true },
{ name: "attrKey", type: "bytes32", indexed: true },
{ name: "pricePerUser", type: "uint256", indexed: false },
{ name: "maxUsers", type: "uint256", indexed: false },
{ name: "requestExpiry", type: "uint40", indexed: false },
],
},
{
name: "RequestExpired",
type: "event",
inputs: [
{ name: "requestId", type: "uint256", indexed: true },
{ name: "buyer", type: "address", indexed: true },
{ name: "escrowReturned", type: "uint256", indexed: false },
],
},
] as const;Every test must pass before contracts are locked on Day 2.
-
mintCertificatesucceeds with authorized issuer, returns non-zerotokenId -
mintCertificatereverts with unauthorized caller ("Not authorized") -
mintCertificatereverts if cert already exists for same owner + attrKey and is not revoked -
locked(tokenId)returnstrue -
transferFromreverts ("Soulbound: non-transferable") -
safeTransferFromreverts ("Soulbound: non-transferable") -
revokeCertificatesetsrevoked = true, emitsCertificateRevoked -
revokeCertificatereverts if already revoked -
isValidreturnstruefor live cert,falsefor revoked cert -
getTokenIdreturns0for unknown owner + attrKey -
ownerAttrTokenmapping updates correctly on mint -
keccak256("defi_user")used asattrKeystores and retrieves correctly
-
postRequeststores request, holds ETH, emitsRequestPosted -
postRequestreverts ifmsg.value != pricePerUser * maxUsers -
postRequestreverts ifmaxUsers == 0 -
approveLeasecreates lease, earmarks payment (does NOT transfer yet), emitsLeaseApproved -
approveLeasereverts if certificate is revoked -
approveLeasereverts ifconfidenceLevel < minConfidence -
approveLeasereverts if cert is AI-derived andaiAllowed == false -
approveLeasereverts if same user tries to fill same request twice -
approveLeasereverts iffilledCount >= maxUsers -
approveLeasereverts if request has expired -
settleLeasetransfers ETH to user afterexpiresAt, emitsLeaseSettled -
settleLeasereverts if called beforeexpiresAt -
settleLeasereverts ifstatus != Active -
revokeLeasesetsstatus = Revoked, does NOT transfer ETH, emitsLeaseRevoked -
revokeLeasereverts ifmsg.sender != lease.user -
revokeLeasereverts ifstatus != Active -
withdrawUnfilledEscrowreturns ETH to buyer afterrequestExpiry -
withdrawUnfilledEscrowreverts beforerequestExpiry -
withdrawUnfilledEscrowreverts if caller is not buyer -
nonReentrantguard prevents re-entrancy onapproveLease,settleLease,revokeLease
-
approveLeasecallsCertificateRegistry.isValid()and correctly rejects revoked cert -
attributeKeyhash from TypeScriptATTRIBUTE_KEYSmatcheskeccak256("defi_user")in Solidity