REVLoans Contract Mechanics
Problem
Building loan functionality for revnets requires understanding the REVLoans contract's specific mechanics: how collateral works (burn vs lock), fee structures, and the functions for borrowing, repaying, and refinancing.
Context / Trigger Conditions
- Implementing borrow flow in a revnet UI
- Building repay or refinance functionality
- Explaining how loan fees work to users
- Calculating prepaid fee amounts
- Understanding collateral mechanics (why tokens are burned, not locked)
- Implementing liquidation detection
Solution
Contract Constants
// Liquidation timeline
uint256 constant LOAN_LIQUIDATION_DURATION = 3650 days // 10 years
// Prepaid fee bounds (basis points)
uint256 constant MIN_PREPAID_FEE_PERCENT = 25 // 0.25% minimum (6 months)
uint256 constant MAX_PREPAID_FEE_PERCENT = 500 // 5% maximum (10 years)
// REV protocol fee on prepaid amount
uint256 constant REV_PREPAID_FEE_PERCENT = 10 // 0.1% to REV
Fee Structure
When a loan is taken:
- Prepaid Fee (2.5% - 50%): Paid upfront, covers interest for prepaid duration
- REV Fee (0.1%): Goes to REV protocol from prepaid amount
- Internal Fee (2.5%): Added back to treasury as revenue
Total Fee = prepaidFee + revFee
Net to Borrower = borrowAmount - prepaidFee - revFee
Treasury Impact = -borrowAmount + internalFee
Key Functions
borrowFrom - Take a Loan
function borrowFrom(
uint256 projectId,
address terminal,
address token,
uint256 amount, // Amount to borrow (in base token)
uint256 collateral, // Tokens to lock as collateral
address beneficiary, // Who receives borrowed funds
uint256 prepaidFeePercent // Fee percent (25-500 basis points)
) external returns (uint256 loanId)
Collateral Mechanics:
- Collateral tokens are BURNED at origination, not locked
- This is key: the loan is secured by the right to remint tokens on repayment
- Burning increases floor price for all remaining holders
repayLoan - Repay and Unlock Collateral
function repayLoan(
uint256 loanId,
uint256 collateralToReturn, // Portion of collateral to unlock
address beneficiary // Who receives the unlocked tokens
) external payable
Repayment Mechanics:
- Can repay partially (return some collateral)
- Tokens are REMINTED to beneficiary pro rata
- Repayment amount = proportional share of original borrow
repaymentRequired = (collateralToReturn / totalCollateral) * borrowAmount
reallocateCollateralFromLoan - Refinance
function reallocateCollateralFromLoan(
uint256 loanId,
uint256 collateralToReallocate, // Collateral to move
uint256 minBorrowAmount, // Min additional borrow (slippage)
address beneficiary, // Who receives new funds
uint256 newPrepaidFeePercent // New prepaid duration
) external returns (uint256 newLoanId)
Refinancing Mechanics:
- Extracts value from appreciated collateral
- Original loan collateral partially/fully moved to new loan
- "Headroom" = borrowable amount - current debt
// Calculate headroom
const borrowableNow = revLoans.borrowableAmountFrom(projectId, collateral, ...)
const headroom = borrowableNow - originalBorrowAmount
// If headroom > 0, can refinance to extract the difference
liquidateExpiredLoansFrom - Liquidate Old Loans
function liquidateExpiredLoansFrom(
uint256 projectId,
uint256[] calldata loanIds,
address payable beneficiary
) external
Liquidation Mechanics:
- Only callable after
LOAN_LIQUIDATION_DURATION(10 years) - Collateral is permanently burned (already was, just can't be reclaimed)
- Original borrowed funds were already withdrawn
Calculating Borrowable Amount
The amount borrowable for a given collateral depends on the current cash-out value:
// On-chain: REVLoans.borrowableAmountFrom()
function borrowableAmountFrom(
uint256 projectId,
uint256 collateral, // Token amount as collateral
uint256 decimals, // Token decimals (18 for ETH)
uint256 currency // 1 = ETH, 2 = USDC
) external view returns (uint256)
// The borrowable amount ≈ cash-out value of collateral
// Uses bonding curve formula internally
Loan State Tracking
Each loan is an ERC-721 NFT with this state:
struct REVLoan {
uint256 projectId;
uint256 collateral; // Remaining collateral
uint256 borrowAmount; // Original borrow amount
uint256 prepaidDuration; // Seconds of prepaid time remaining
uint256 prepaidFeePercent;
uint256 createdAt; // Timestamp
address terminal;
address token;
string tokenUri;
}
Solvency Guarantee
From the academic whitepaper:
"The revnet remains solvent for any sequence of loans, regardless of their number, sizes, or whether they default."
This is because:
- Collateral is burned at origination (reduces supply)
- Borrow amount ≤ cash-out value of burned collateral
- Treasury backing never drops below zero
UI Implementation Pattern
// BorrowDialog flow
function BorrowDialog({ projectId, userBalance }) {
const [collateral, setCollateral] = useState(0n)
// Get borrowable amount for selected collateral
const { data: borrowable } = useReadContract({
address: REVLOANS_ADDRESS,
abi: revLoansAbi,
functionName: 'borrowableAmountFrom',
args: [projectId, collateral, 18, 1],
})
// Calculate fees
const prepaidFeePercent = 250 // 2.5% = 6 months
const prepaidFee = (borrowable * BigInt(prepaidFeePercent)) / 10000n
const revFee = (prepaidFee * 10n) / 10000n // 0.1%
const netReceived = borrowable - prepaidFee - revFee
// Execute borrow
const { write: borrow } = useContractWrite({
address: REVLOANS_ADDRESS,
abi: revLoansAbi,
functionName: 'borrowFrom',
args: [projectId, terminal, token, borrowable, collateral, beneficiary, prepaidFeePercent],
})
return (
<Dialog>
<CollateralInput value={collateral} max={userBalance} onChange={setCollateral} />
<BorrowableDisplay amount={borrowable} />
<FeeBreakdown prepaid={prepaidFee} rev={revFee} net={netReceived} />
<Button onClick={() => borrow()}>Borrow</Button>
</Dialog>
)
}
Verification
- Check
loanOf(loanId)returns correct state after borrow - Verify
borrowableAmountFrommatches expected cash-out value - Test partial repayment correctly returns proportional collateral
- Confirm refinance extracts correct headroom amount
Example
Complete borrow transaction:
import { revLoansAbi } from '@/abi/revLoans'
async function borrowFromRevnet(
wallet: WalletClient,
params: {
projectId: bigint
terminal: Address
token: Address
collateral: bigint
prepaidMonths: number // 6-120 months
}
) {
// Calculate prepaid fee percent (25 BPS = 6 months, 500 BPS = 10 years)
const prepaidFeePercent = Math.min(
500,
Math.max(25, Math.floor(params.prepaidMonths / 6 * 25))
)
// Get borrowable amount
const borrowable = await revLoans.read.borrowableAmountFrom([
params.projectId,
params.collateral,
18n,
1n
])
// Execute borrow
const txHash = await wallet.writeContract({
address: REVLOANS_ADDRESS,
abi: revLoansAbi,
functionName: 'borrowFrom',
args: [
params.projectId,
params.terminal,
params.token,
borrowable,
params.collateral,
wallet.account.address, // beneficiary
BigInt(prepaidFeePercent)
]
})
return txHash
}
Notes
- Loans are non-custodial: no one can take your collateral except you (until liquidation)
- Collateral burn is what makes loans "self-liquidating" - they secure themselves
- The 10-year liquidation duration is extremely long - most DeFi loans liquidate quickly
- Prepaid fee scales linearly: 2.5% for 6 months, 5% for 10 years
- Interest after prepaid period: 5% annual, added to repayment amount
- Multi-chain: loans exist on specific chains, can't be moved cross-chain
References
- REVLoans.sol
- useBorrowDialog.tsx
- Whitepaper: "Cryptoeconomics of Revnets" - Section on Loan Solvency
