Reproduced Exploit
Juicebox REVLoans Exploit — Trust-on-First-Use of a Caller-Supplied Loan Source Inflates Borrowable Surplus
1. REVLoans.borrowFrom() (src_REVLoans.sol:483-560) lets anyone open a loan against a revnet, passing a REVLoanSource{token, terminal} struct of their own choosing. The contract never checks that source.terminal is a real, directory-registered Juicebox terminal — it simply calls into it.
Loss
~21.76 ETH — 21.764969886576733610 ETH drained from Juicebox revnet #3's treasury via JBMultiTerminal (attack…
Chain
Ethereum
Category
Logic / State
Date
Apr 2026
Source & credit. Exploit reproduction, trace data, and analysis adapted from DeFiHackLabs by SunWeb3Sec — an open registry of reproduced on-chain exploits. Standalone Foundry PoC and full write-up: 2026-04-JuiceboxREVLoans_exp in the
evm-hack-registrymirror. Upstream DeFiHackLabs PoC:src/test/…/JuiceboxREVLoans_exp.sol.
Vulnerability classes: vuln/logic/missing-validation · vuln/access-control/missing-auth
Reproduction: the PoC compiles & runs in an isolated Foundry project at this project folder (the umbrella DeFiHackLabs repo contains several unrelated PoCs that do not all compile together, so this one was extracted). Full verbose trace: output.txt. Verified vulnerable source (mainnet bytecode at the fork block): src_REVLoans.sol.
Key info#
| Loss | ~21.76 ETH — 21.764969886576733610 ETH drained from Juicebox revnet #3's treasury via JBMultiTerminal (attack tx) |
| Vulnerable contract | REVLoans — 0x1880D832aa283d05b8eAB68877717E25FbD550Bb |
| Victim pool/vault | Juicebox revnet #3 treasury, held in JBMultiTerminal 0x2dB6d704058E552DeFE415753465df8dF0361846 |
| Attacker EOA | 0x23245F620d1e910ad76e6B6De4f8284A53C9Ad2d |
| Attacker contract | Direct EOA call + an attacker-deployed fake loan source (FakeLoanSourceTerminal, on-chain 0x5615…b72f in the PoC) |
| Attack tx | 0x9adbd62355eb72b4ff6c58716a503133672ed9317ab930a4c6aa31c7a1a8f938 |
| Chain / block / date | Ethereum mainnet / fork block 24,917,718 / April 2026 |
| Compiler / optimizer | Solidity v0.8.23+commit.f704f362, optimizer enabled, 150 runs (from sources/REVLoans_1880D8/_meta.json) |
| Bug class | Trust-on-first-use of an unverified, caller-supplied loan source (terminal + token). A fake source self-reports inflated accounting (36 decimals + an arbitrary useAllowanceOf return) so totalBorrowedFrom is booked without any real assets moving; this phantom borrow then inflates the borrowable surplus, letting tiny collateral pull real ETH from the genuine terminal. |
TL;DR#
-
REVLoans.borrowFrom()(src_REVLoans.sol:483-560) lets anyone open a loan against a revnet, passing aREVLoanSource{token, terminal}struct of their own choosing. The contract never checks thatsource.terminalis a real, directory-registered Juicebox terminal — it simply calls into it. -
On the first use of a given
(revnetId, terminal, token)triple,_addTo()(:779-851) registers that source (isLoanSourceOf = true, pushes it into_loanSourcesOf[revnetId]) and adds the loan tototalBorrowedFrom[revnetId][terminal][token]— before and independent of any real asset transfer. -
The attacker deploys a
FakeLoanSourceTerminalthat lies on three method calls:accountingContextForTokenOfreports 36 decimals (vs the native token's 18),useAllowanceOfsimply returns whatever amount it was asked to pay out (no real transfer), andtransfer/payare no-ops returningtrue/0. -
Step A (register the fake source): the attacker calls
borrowFromwithsource = {token: fake, terminal: fake}._borrowAmountFromasks the fake terminal for its accounting context (36 decimals) and computes a borrowable amount from the revnet's surplus;_addTothen bookstotalBorrowedFrom[3][fake][fake] = 444,895,155,932,119,291,075,904,440,330(output.txt:1753-1754) — a phantom ~4.45e29 (36-decimal) borrow — while the fakeuseAllowanceOf"pays out" nothing real (output.txt:1690-1691). -
Step B (drain real ETH): the attacker calls
borrowFromagain with the real native-ETH source{token: 0x…EEEe, terminal: JBMultiTerminal}. Because the revnet now appears to have a large outstanding loan,_borrowableAmountFrominflates the proportional cash-out: a microscopic collateral burn of 65,301,882,816,341 revnet-#3 tokens (~6.5e-5) borrows 23.154329666570993199 ETH (~23.15) from the genuine terminal (output.txt:1796). -
After the protocol's REV fee (~0.2315 ETH) and source fee (~0.5789 ETH), the genuine
JBMultiTerminaluseAllowanceOfsends 21.765069886576733610 ETH straight to the attacker's fallback (output.txt:1988). Net of the tiny seed and gas, the PoC asserts a profit of 21.764969886576733610 ETH (output.txt:1539, output.txt:2112).
The single defect is that REVLoans trusts the borrower-supplied terminal both as the registrar of totalBorrowedFrom and as the oracle of its own decimals/payout — so a fake terminal can fabricate the "I have lent X" bookkeeping that the proportional borrow math then treats as real surplus.
Background — what REVLoans does#
REVLoans (source) is the Juicebox "revnet" borrowing contract. A revnet user can lock revnet project tokens as collateral and borrow assets out of the revnet's terminal up to the cash-out value of that collateral. Loans are represented as ERC-721 NFTs; collateral tokens are burned on borrow and re-minted on repay so the revnet's token supply stays orderly. An upfront fee (min 2.5%, plus a 1% REV fee, plus a borrower-chosen prepaid fee) is taken when the loan opens.
The borrowable amount is computed proportionally in _borrowableAmountFrom (:292-333):
borrowable = cashOutFrom(surplus = totalSurplus + totalBorrowed, cashOutCount = collateral, totalSupply = supply + totalCollateral, cashOutTaxRate)
Crucially, the totalBorrowed term is added to the surplus. The idea is "money already lent out is still backing the revnet, so it counts toward what new borrowers can draw against." totalBorrowed is the sum, over every registered loan source, of totalBorrowedFrom[revnetId][terminal][token], each normalized via the source terminal's own accountingContextForTokenOf decimals/currency (_totalBorrowedFrom, :430-466).
On-chain parameters at the fork block (read from the trace):
| Parameter | Value | Source |
|---|---|---|
| Revnet under attack | #3 | PoC REVNET_ID = 3 |
Controller (JBController) | 0x27da30646502e2f642bE5281322Ae8C394F7668a | output.txt:1660 |
JBMultiTerminal (genuine) | 0x2dB6d704058E552DeFE415753465df8dF0361846 | output.txt:1551 |
JBPermissions | 0x04fD6913d6c32D8C216e153a43C04b1857a7793d | output.txt:1553 |
| Native token sentinel | 0x…EEEe, 18 decimals, currency 61166 | output.txt:1679 |
| Revnet #3 native surplus (18-dec) | 24,373,078,596,390,645,819 (~24.37 ETH) | output.txt:1778 |
| Same surplus re-scaled to 36 dec (fake source path) | 24,373,078,596,390,645,819,000,000,000,000,000,000 (~24.37) | output.txt:1674 |
MIN_PREPAID_FEE_PERCENT | 25 (2.5%) | :92 |
REV_PREPAID_FEE_PERCENT | 10 (1%) | :89 |
| Burn permission id | 10 (collateral burn) | PoC BURN_PERMISSION_ID = 10, output.txt:1644 |
The whole game: the revnet's genuine native surplus is only ~24.37 ETH. The attacker's real collateral entitles them to a trivial fraction of it — but by first booking a gigantic phantom totalBorrowed, they inflate the surplus the proportional formula sees, so a near-zero collateral burn maps onto ~23.15 real ETH.
The vulnerable code#
1. borrowFrom accepts a caller-supplied source and never validates the terminal#
function borrowFrom(
uint256 revnetId,
REVLoanSource calldata source, // ← attacker-chosen {token, terminal}
uint256 minBorrowAmount,
uint256 collateralCount,
address payable beneficiary,
uint256 prepaidFeePercent
)
public
override
returns (uint256 loanId, REVLoan memory)
{
address revnetOwner = PROJECTS.ownerOf(revnetId);
if (revnetOwner != address(REVNETS)) revert REVLoans_RevnetsMismatch(revnetOwner, address(REVNETS));
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
// ... prepaid-fee bounds check ...
REVLoan storage loan = _loanOf[loanId];
loan.source = source; // ← stored verbatim, terminal never checked against DIRECTORY
// ...
uint256 borrowAmount = _borrowAmountFrom({loan: loan, revnetId: revnetId, collateralCount: collateralCount});
// ...
_adjust({ loan: loan, revnetId: revnetId, newBorrowAmount: borrowAmount, ... });
}
The only validation is revnetOwner == REVNETS (the revnet itself is legitimate) and a non-zero collateral. The source.terminal is never checked against DIRECTORY.isTerminalOf(revnetId, terminal) — it can be any contract the attacker deploys.
2. _addTo registers the source on first use and books totalBorrowedFrom before any real transfer#
function _addTo(REVLoan memory loan, uint256 revnetId, uint256 addedBorrowAmount, uint256 sourceFeeAmount, address payable beneficiary) internal {
// Register the source if this is the first time its being used for this revnet.
if (!isLoanSourceOf[revnetId][loan.source.terminal][loan.source.token]) {
isLoanSourceOf[revnetId][loan.source.terminal][loan.source.token] = true;
_loanSourcesOf[revnetId].push(REVLoanSource({token: loan.source.token, terminal: loan.source.terminal}));
}
// Increment the amount of the token borrowed from the revnet from the terminal.
totalBorrowedFrom[revnetId][loan.source.terminal][loan.source.token] += addedBorrowAmount; // ⚠️ booked first
uint256 netAmountPaidOut;
{
JBAccountingContext memory accountingContext =
loan.source.terminal.accountingContextForTokenOf({projectId: revnetId, token: loan.source.token}); // ⚠️ fake answers
netAmountPaidOut = loan.source.terminal.useAllowanceOf({ // ⚠️ fake "pays" nothing
projectId: revnetId, token: loan.source.token, amount: addedBorrowAmount, currency: accountingContext.currency,
minTokensPaidOut: 0, beneficiary: payable(address(this)), feeBeneficiary: beneficiary, memo: "Lending out to a borrower"
});
}
// ...
_transferFrom({ from: address(this), to: beneficiary, token: loan.source.token, amount: netAmountPaidOut - revFeeAmount - sourceFeeAmount });
}
For the fake source, useAllowanceOf returns the full amount but transfers nothing real; the subsequent _transferFrom uses the fake token's no-op transfer. The contract never reconciles totalBorrowedFrom against assets it actually received — so the phantom borrow sticks.
3. The phantom totalBorrowedFrom re-enters the borrowable-surplus math#
function _borrowableAmountFrom(uint256 revnetId, uint256 collateralCount, uint256 decimals, uint256 currency, IJBTerminal[] memory terminals)
internal view returns (uint256)
{
// ...
uint256 totalBorrowed = _totalBorrowedFrom({revnetId: revnetId, decimals: decimals, currency: currency});
uint256 totalSupply = CONTROLLER.totalTokenSupplyWithReservedTokensOf(revnetId);
uint256 totalCollateral = totalCollateralOf[revnetId];
return JBCashOuts.cashOutFrom({
surplus: totalSurplus + totalBorrowed, // ⚠️ phantom borrow inflates the surplus
cashOutCount: collateralCount,
totalSupply: totalSupply + totalCollateral,
cashOutTaxRate: currentStage.cashOutTaxRate()
});
}
_totalBorrowedFrom (:430-466) normalizes each source's booked total using the source terminal's own decimals. The fake terminal claims 36 decimals, so the phantom total inflates the surplus term that the next, real loan draws against.
4. The attacker's fake source (PoC)#
function accountingContextForTokenOf(uint256, address token) external pure returns (JBAccountingContext memory context) {
// The historical fake source reported 36 decimals, inflating the recorded fake borrow by 1e18.
context = JBAccountingContext({token: token, decimals: 36, currency: ETH_CURRENCY});
}
function useAllowanceOf(uint256, address, uint256 amount, uint256, uint256, address payable, address, string calldata)
external pure returns (uint256) { return amount; } // "pays out" without moving assets
function transfer(address, uint256) external pure returns (bool) { return true; }
(test/JuiceboxREVLoans_exp.sol:172-203)
Root cause — why it was possible#
The defect is a trust-on-first-use of an unverified, caller-supplied loan source that is simultaneously:
-
The registrar of accounting it benefits from.
_addTowritestotalBorrowedFrom[revnetId][source.terminal][source.token] += addedBorrowAmount(:795) before any real asset moves, andborrowFromperforms zero check thatsource.terminalis a directory-registered Juicebox terminal (:483-520). Any address the attacker deploys becomes a "loan source" the first time it is used. -
The oracle of its own units.
_totalBorrowedFromand_borrowAmountFromcallsource.terminal.accountingContextForTokenOf(...)and trust the returneddecimals/currency(:448-449, :353-354). A fake terminal returning 36 decimals scales the phantom borrow up by 1e18. -
The payer the contract never reconciles.
_addTotreatsuseAllowanceOf's return value as the amount paid out (:805-814) without verifying thatREVLoansactually received those assets. The fake source returns a large number while transferring nothing.
The phantom totalBorrowed is then summed into the surplus used by JBCashOuts.cashOutFrom (:327-332). Because borrowable amount is proportional to surplus, inflating surplus with a fictional loan lets near-zero genuine collateral borrow the revnet's real ETH from the real JBMultiTerminal. The first (fake) loan moves no money; the second (real) loan is the cash-out.
The intended invariant — "totalBorrowedFrom reflects assets that genuinely left a genuine terminal" — is silently violated because the contract lets the borrower supply, register, and self-attest the source in one unguarded call.
Preconditions#
- The revnet must be a legitimately-deployed revnet (
PROJECTS.ownerOf(revnetId) == REVNETS). Revnet #3 satisfied this. (output.txt:1654-1655) - The attacker must hold a tiny amount of revnet-#3 project tokens to post as collateral and must grant
REVLoansthe burn permission (id 10) so collateral can be burned. The PoC mints tokens by paying 0.0001 ETH into the terminal (output.txt:1564) and grants the permission viaJBPermissions.setPermissionsFor(output.txt:1644-1652). - No special privilege, signature, or whitelisting is required to register a loan source —
borrowFromis permissionless and binds the source on first use. - Working capital is negligible: the PoC seeds the attacker with 1 ETH (
vm.deal, output.txt:1560) of which only ~0.0001 ETH is actually spent before the drain. The attack is self-funding within the transaction.
Attack walkthrough (with on-chain numbers from the trace)#
All raw values are 18-decimal wei unless noted; human approximations in parentheses. Revnet #3's genuine native surplus stays ~24.37 ETH throughout; the exploit manipulates the accounting, not the pool composition, until the final real draw.
| # | Step | Key on-chain value | Treasury / accounting state | Effect |
|---|---|---|---|---|
| 0 | Seed + buy collateral — vm.deal(attacker, 1e18); JBMultiTerminal.pay{value: 1e14} mints revnet-#3 tokens to attacker | pay value 100000000000000 (~0.0001 ETH) → mints 91613283200000000 (~0.0916) revnet tokens (output.txt:1564, output.txt:1606) | revnet #3 native surplus ~24.37 ETH | Attacker now holds tiny collateral. |
| 1 | Grant burn permission — JBPermissions.setPermissionsFor(operator = REVLoans, permissionIds = [10]) | permission id 10 (output.txt:1644-1652) | unchanged | Lets REVLoans burn collateral. |
| 2 | Register fake source — borrowFrom(3, {fake, fake}, collateral = 22903320800000000) | fake accountingContextForTokenOf → 36 decimals (output.txt:1657); fake useAllowanceOf returns 444895155932119291075904440330 (~4.45e29 in 36-dec) (output.txt:1690-1691) | totalBorrowedFrom[3][fake][fake] = 444,895,155,932,119,291,075,904,440,330 (output.txt:1753-1754) | Phantom borrow booked; no real assets moved. Source registered in _loanSourcesOf[3]. |
| 3 | Assert registration — totalBorrowedFrom(3, fake, fake) > 0 | 4.448e29 > 0 ✓ (output.txt:1753-1755) | phantom total persists | Confirms the fake source is now a counted loan source. |
| 4 | Borrow real ETH — borrowFrom(3, {0x…EEEe, JBMultiTerminal}, collateral = 65301882816341) | native surplus (18-dec) 24373078596390645819 (~24.37) (output.txt:1778); genuine useAllowanceOf pays out 23154329666570993199 (~23.15 ETH) (output.txt:1796) | revnet #3 surplus drained by ~23.15 ETH | Tiny collateral (~6.5e-5) borrows ~23.15 real ETH — only possible because the phantom borrow inflated the surplus. |
| 5 | Fees taken — REV fee + source fee paid back into the terminal | REV fee 231543296665709931 (~0.2315), source fee 578858241664274829 (~0.5789) (output.txt:1909, output.txt:2018) | partially refunds the terminal | Net ETH to attacker = 23.154 − 0.2315 − 0.5789. |
| 6 | Payout to attacker — genuine terminal forwards net ETH to attacker fallback | Attacker::fallback{value: 21765069886576733610} (~21.765) (output.txt:1988) | terminal down ~23.15 ETH gross | Attacker receives the drained ETH. |
| 7 | Settle — profit asserted | Attacker ETH profit: 21.764969886576733610 (output.txt:1539, output.txt:2112); assertGt(profit, 21 ether) ✓ (output.txt:2113) | — | Net profit after seed/gas. |
Why a 6.5e-5 collateral borrows 23.15 ETH: cashOutFrom is proportional to surplus / (supply + collateral). Revnet #3's honest native surplus is only ~24.37 ETH (output.txt:1778), but the borrowable amount is computed against surplus = totalSurplus + totalBorrowed. By first booking a ~4.45e29 (36-decimal-scaled) phantom totalBorrowed, the attacker makes the revnet appear to have an enormous amount already lent out and therefore an enormous backing surplus — so a microscopic collateral burn maps onto ~23.15 ETH of the real surplus the genuine terminal actually holds.
Profit / loss accounting (ETH)#
| Item | Amount (wei) | ~Human |
|---|---|---|
| Attacker balance before (after seed) | 1,000,000,000,000,000,000 | 1.000 |
Spent — collateral purchase pay{value:1e14} | 100,000,000,000,000 | 0.0001 |
Real ETH borrowed from genuine terminal (useAllowanceOf) | 23,154,329,666,570,993,199 | ~23.154 |
| − REV fee paid back | 231,543,296,665,709,931 | ~0.2315 |
| − Source fee paid back | 578,858,241,664,274,829 | ~0.5789 |
| Net ETH delivered to attacker fallback | 21,765,069,886,576,733,610 | ~21.765 |
| Net profit asserted by PoC | 21,764,969,886,576,733,610 | ~21.7650 |
The ~21.765 ETH net profit (output.txt:1539) equals the gross draw minus the two fees minus the ~0.0001 ETH collateral buy and gas — i.e. the attacker walked off with essentially the revnet's available native surplus.
Diagrams#
Sequence of the attack#
Treasury / accounting state evolution#
The flaw inside borrowFrom / _addTo#
Why it is theft: borrowable surplus before vs. after the phantom loan#
Why each magic number#
REVNET_ID = 3: the targeted revnet. Its native surplus (~24.37 ETH, output.txt:1778) is the prize; the attacker only needsPROJECTS.ownerOf(3) == REVNETSto pass the single legitimacy check.projectTokenPayment = 0.0001 ETH: the minimal ETH paid into the genuine terminal to mint a sliver of revnet-#3 tokens (~0.0916, output.txt:1606) to be burned as collateral. Just enough to makecollateralCount != 0.BURN_PERMISSION_ID = 10: the Juicebox permission that authorizesREVLoansto burn the attacker's revnet tokens as collateral; without it,_addCollateralTo'sburnTokensOfwould revert (output.txt:1705-1706).fakeSourceCollateral = 22903320800000000(~0.0229): the collateral passed to the fakeborrowFrom. Its only purpose is to make the call non-zero so_addToruns and books the phantomtotalBorrowedFrom; the fake source pays nothing real back.- Fake source
decimals = 36: the lie that scales the phantom borrow up by 1e18 relative to the native token's 18 decimals, maximizing the surplus inflation in_totalBorrowedFrom. nativeCollateral = 65301882816341(~6.5e-5): the real collateral for the genuine native-ETH loan. It is deliberately tiny — the inflated surplus means even this microscopic burn maps onto ~23.15 ETH borrowable (output.txt:1796).PREPAID_FEE_PERCENT = 25: the minimum allowed prepaid fee (MIN_PREPAID_FEE_PERCENT, :92); using the floor minimizes upfront cost while still passing the bounds check inborrowFrom.ETH_CURRENCY = 61166: Juicebox's currency code for ETH; the fake source reports it so its accounting context lines up with the native token's currency and avoids a price-feed lookup.
Remediation#
- Whitelist loan sources; never bind one on first caller use.
borrowFrommust require thatsource.terminalis a directory-registered terminal for the revnet (e.g.DIRECTORY.isTerminalOf(revnetId, source.terminal)) and thatsource.tokenis an accepted accounting token. Pre-register sources via governance instead of registering whatever a borrower supplies in_addTo(:788-792). - Reconcile booked borrow against real assets received. Before incrementing
totalBorrowedFrom(:795), measure_balanceOf(token)before/after theuseAllowanceOfcall and require that the contract actually receivedaddedBorrowAmount(or the net thereof). A source that "pays" without transferring must cause a revert. - Do not trust a caller-supplied terminal as its own decimals/currency oracle.
_totalBorrowedFromand_borrowAmountFromderive scale fromsource.terminal.accountingContextForTokenOf(:448-449). Pin the accounting context from the trustedDIRECTORY/CONTROLLER, not from the source contract itself, and bounddecimalsto a sane range. - Exclude unverified sources from the borrowable-surplus term. Only sources whose loans are backed by assets that genuinely left a registered terminal should contribute to
totalBorrowedin_borrowableAmountFrom(:318-332). A phantom loan must never inflate what new borrowers can draw. - Add an invariant/monitor: for every
(revnetId, terminal, token),totalBorrowedFromshould be reconstructable from real terminal payouts; alert and pause if the booked total diverges from terminal accounting.
How to reproduce#
The PoC runs offline against a local anvil fork served from anvil_state.json (the test's createSelectFork points at http://127.0.0.1:8545; the trace labels the fork "mainnet" at block 24,917,718). No public RPC is contacted at test time.
_shared/run_poc.sh 2026-04-JuiceboxREVLoans_exp --mt testExploit -vvvvv
- The harness boots a local anvil from the captured
anvil_state.jsonand points the fork RPC at127.0.0.1:8545, fork block 24,917,718 (vm.createSelectFork("http://127.0.0.1:8545", 24917718), test/JuiceboxREVLoans_exp.sol:96-97). foundry.tomlsetsevm_version = 'cancun'; the vulnerable contract was verified under Solidity 0.8.23 with optimizer enabled (150 runs). The local build compiles the harness with a newer solc (the trace showsSolc 0.8.34), which does not affect the forked mainnet bytecode under test.- Result:
[PASS] testExploit()loggingAttacker ETH profit: 21.764969886576733610.
Expected tail (from output.txt:1536-1539 and output.txt:2117-2119):
Ran 1 test for test/JuiceboxREVLoans_exp.sol:ContractTest
[PASS] testExploit() (gas: 1564737)
Logs:
Attacker ETH profit: 21.764969886576733610
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 31.47s (29.93s CPU time)
Ran 1 test suite in 32.02s (31.47s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Reference: DefimonAlerts — https://x.com/DefimonAlerts/status/2046862935650345139 (Juicebox REVLoans, Ethereum mainnet, ~21.77 ETH).
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2026-04-JuiceboxREVLoans_exp (evm-hack-registry mirror).
- Upstream DeFiHackLabs PoC:
JuiceboxREVLoans_exp.sol. - Attack transaction: view on explorer.
Alerts & third-party analyses
- Original alert / thread: post on X.
- DeFiHackLabs incident explorer: search "Juicebox REVLoans Exploit".
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.