executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
jb-revloansREVLoans contract mechanics for Juicebox V5 revnets. Use when: (1) implementing loan borrow/repay/refinance flows, (2) calculating loan fees and prepaid amounts, (3) understanding collateral burn/remint mechanics, (4) building loan-related UIs, (5) explaining loan solvency. Covers borrowFrom, repayLoan, reallocateCollateralFromLoan, and liquidation mechanic…
Independent GitHub snapshot · Automated classification · Original documentation governs
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.
// 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
When a loan is taken:
Total Fee = prepaidFee + revFee
Net to Borrower = borrowAmount - prepaidFee - revFee
Treasury Impact = -borrowAmount + internalFee
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:
function repayLoan(
uint256 loanId,
uint256 collateralToReturn, // Portion of collateral to unlock
address beneficiary // Who receives the unlocked tokens
) external payable
Repayment Mechanics:
repaymentRequired = (collateralToReturn / totalCollateral) * borrowAmount
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:
// Calculate headroom
const borrowableNow = revLoans.borrowableAmountFrom(projectId, collateral, ...)
const headroom = borrowableNow - originalBorrowAmount
// If headroom > 0, can refinance to extract the difference
function liquidateExpiredLoansFrom(
uint256 projectId,
uint256[] calldata loanIds,
address payable beneficiary
) external
Liquidation Mechanics:
LOAN_LIQUIDATION_DURATION (10 years)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
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;
}
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:
// 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>
)
}
loanOf(loanId) returns correct state after borrowborrowableAmountFrom matches expected cash-out valueComplete 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
}