Skip to content

VDB-53: VIP-657: wire Cash+ and Ceffu FRV vaults into the Liquidity Hub, raise FRV caps to 50% - #753

Merged
fred-venus merged 7 commits into
mainfrom
bot/VDB-53-vip-add-ceffu-frv-and-cash-frv-into-liquidity-hub
Aug 19, 2026
Merged

VDB-53: VIP-657: wire Cash+ and Ceffu FRV vaults into the Liquidity Hub, raise FRV caps to 50%#753
fred-venus merged 7 commits into
mainfrom
bot/VDB-53-vip-add-ceffu-frv-and-cash-frv-into-liquidity-hub

Conversation

@trumpgpt-bot

@trumpgpt-bot trumpgpt-bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

VIP-657 [BNB Chain] Wire Cash+ and Ceffu FRV vaults into the Liquidity Hub and raise FRV caps to 50%

Renumbered from the placeholder vip-665 to vip-657 — on-chain proposalCount() is 656, so this is the next proposal id. safe-vote.json votes on 657.

What it does

BNB Chain mainnet, REGULAR (Normal Timelock), 4 commands, one atomic transaction, no ACM grants (VIP-650/651 already gave the Normal Timelock the full Governance role set):

# Target Call
1 U FRV source 0x30908edd…9143 addResource(CASH_PLUS_VAULT 0x41179fc6…bcea, AdapterFRV 0x1FA0365b…5902)
2 U Hub 0x0e5AA174…88B0 raiseYieldGroupCap(U FRV source, 5,000,000e18, 5000 bps)
3 USDT FRV source 0x621eF38c…4bE7 addResource(CEFFU_VAULT 0x086fd797…0103, AdapterFRV)
4 USDT Hub 0x18AfDACF…7226 raiseYieldGroupCap(USDT FRV source, 5,000,000e18, 5000 bps)

Absolute FRV cap is unchanged at 5,000,000; only the percentage-of-TVL dimension moves 3000 → 5000 bps. Registering a vault does not auto-route deposits: the FRV source stays out of each Hub's outer deposit queue, so FRV is filled only by the Operator's reallocate. Inner deposit/withdraw queues are deliberately left empty.

⚠️ The Ceffu leg is proposed ahead of its vault deployment — deliberately

Corrects the previous version of this description, which claimed the Ceffu addResource had been deferred to a follow-up VIP. It has not — command 3 above is in the proposal.

0x086fd7972510dF9d9cFdc4efB8677fc72d290103 holds no code today. The deploy-first gate confirms it:

$ npx hardhat run scripts/checkCeffuVaultReady.ts --network bscmainnet
VIP-657 deploy-first gate FAILED:
  - Ceffu vault 0x086fd7972510dF9d9cFdc4efB8677fc72d290103 holds no code — not deployed yet.

The address is fixed in advance regardless: it is the deterministic CREATE2 clone the InstitutionalVaultController (0x6D9e91cB…6E85) mints for the Ceffu institution (0x8972E6F8…6262) at that institution's nonce 0 — salt = keccak256(institutionOperator, nonce) — and predictVaultAddress(CEFFU_INSTITUTION) returns exactly that address today, with institutionNonce == 0.

The safety property is atomic fail-closed, not trust in the address: addResource reverts ResourceNotContract against a codeless address, so if the Ceffu vault is not live at execution time the whole proposal reverts — the CASH+ registration and both cap raises included. Nothing lands by halves.

Operational requirement: the fixed-rate-vaults workstream must deploy the vault at that address inside the propose → execute window. Run scripts/checkCeffuVaultReady.ts before queueing and again immediately before executing. Once the vault is live, verify it by its on-chain code, asset() == USDT, and InstitutionalVaultController.isRegistered(…)not by re-running predictVaultAddress, which returns the institution's next clone after createVault bumps the nonce.

The Asseto CASH+ vault (0x41179fc6…bcea) is live, with asset() == U.

Description rewrite

meta.description was rewritten in this commit so the on-chain text matches reality:

  • Removed the false claim "This proposal is submitted only AFTER that vault has been deployed at … and verified there", replaced with an explicit "not deployed at proposal time" section and the fail-closed argument.
  • Deploy-gate framing changed from "must not be proposed until the vault is live""run before queueing and again before executing".
  • all six commandsall four commands.
  • All inline backticks stripped — the venus.io governance UI silently swallows their contents.

Verification

  • Pre-VIP on-chain state re-checked at HEAD: both FRV sources have resources() == [], both FRV yield groups sit at (5,000,000e18, 3000 bps) registered and unpaused, and the Normal Timelock holds every role called.
  • Fork simulation at block 116,780,000: 25/25 passing, including the behavioural proof that an Operator reallocate push reverts ResourceNotRegistered pre-VIP and ResourceCapacityExceeded post-VIP (both vaults are pre-Fundraising, maxDeposit == 0). The sim asserts getCode(CEFFU_VAULT) == "0x" and then hardhat_setCodes a minimal USDT-asset stand-in, so it proves the Hub-side wiring only — it is not evidence the real vault exists.
  • propose() gas 1,116,868 — 6.65% of the 16,777,216 per-tx cap (EIP-7825 headroom fine).
  • propose calldata decoded with cast: 4 targets in order, values [0,0,0,0], proposalType 0 (REGULAR), params byte-checked (0x422ca8b0a00a425000000 = 5e24, 0x1388 = 5000).
  • yarn prettier clean, yarn lint 0 errors. scripts/checkCeffuVaultReady.ts also renumbered — it still imported vips/vip-665/bscmainnet and crashed on module resolution.

Standing security note (unchanged from review)

Do not reallocate funds into either vault yet. On the InstitutionalVaultController, sweep(address,address), setTreasury(address), openVault, closeVault and cancelVault are all held by the CriticalGuardian 3-of-6 Safe (0x7B1AE5Ea599bC56734624b95589e7E8E64C351c9) with no timelock delay, and BaseVault.sweep does not exclude the supply asset — that Safe could move vault capital while AdapterFRV still marks full principal. Not exploitable while the vaults are pre-Fundraising, so this VIP ships safely, but funds must wait until the controller's authority is audited, those roles are narrowed to the Normal Timelock, and the sweep supply-asset guard is added. The secondary exit-fee brake is also still off (redeemFeeBps == 0); this VIP sets no redeem fee, as none was specified in the approved scope.

… to 50%

Registers the Asseto CASH+ fixed-rate vault as a resource on the U Hub's
FRV yield source (behind the shared AdapterFRV) with its inner deposit/
withdraw queues, and raises the FRV percentage-of-TVL cap from 30% to 50%
(absolute cap unchanged at 5,000,000) on both the U and USDT Hubs. All
commands are direct Normal Timelock calls (roles granted by VIP-650/651);
no ACM grants. The Ceffu/USDT vault is not yet deployed, so its resource
registration is left to a follow-up VIP.

Adds a 4-part fork simulation (19 assertions, green at block 116780000)
including a behavioral proof that an Operator reallocate push into the
vault reverts ResourceNotRegistered pre-VIP and ResourceCapacityExceeded
post-VIP, proving registration went live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@trumpgpt-bot
trumpgpt-bot marked this pull request as ready for review August 19, 2026 05:22
@vancegpt-bot

Copy link
Copy Markdown
Contributor

Review — VIP-665 (Cash+ FRV vault → U Liquidity Hub, FRV cap 30%→50%)

Reviewed by a three-dimension squad (code quality / functional correctness / security). All on-chain claims below were re-verified independently against BNB Chain mainnet at head ~116,792,100–116,792,252, not taken from the PR description.

The five-command payload itself verifies clean — every address matches the VIP-650 address book / venus-liquidity-hub deployments, all five roles are genuinely held by the Normal Timelock (FastTrack and Critical hold none), all five commands eth_call-simulate green from the timelock, cap encoding and hub/source pairing are correct, and ordering is required and correct. Scope matches what VDB-53/VPD-1867 approved.

The material finding is not about the payload — it is about what registration imports: a 3-of-6 Safe with no delay can extract Hub funds from the CASH+ vault once capital sits in it (Security #1, HIGH). Not exploitable while the vault is pre-Fundraising with maxDeposit == 0, so this VIP can ship, but the Operator must not push funds until it is closed.

Counts: Code Quality 8 · Functional Correctness 1 · Security 5.


Code Quality

Comments

  • vips/vip-665/bscmainnet.ts:5-40 — the 36-line banner restates meta.description below it: banner 15-20 = description "Actions" (85-98), banner 22-23 = description 82-83, banner 25-28 = description "Access control" (103-107), banner 35-36 = the ADAPTER_FRV comment at 50-52 and description 90 — cut the banner to the parts that exist nowhere else (the on-chain verification block, 30-39) and delete the rest.
  • vips/vip-665/bscmainnet.ts:111-113 — this Notes bullet repeats lines 78-80 of the same description almost verbatim ("does NOT auto-route deposits … FRV is filled only by the Operator's reallocate") — delete the bullet, 78-80 already says it.
  • vips/vip-665/bscmainnet.ts:58-60 — "absolute cap unchanged / _effectiveCap takes the lower of the two" is stated a fourth time here after banner 19-20, description 100-101 and description 115 — drop this block; the trailing comments on 61-63 carry the same information at the point of use.
  • simulations/vip-665/bscmainnet.ts:29-31 — comment cites block 116,788,760 as where "every fact this sim asserts was verified" but FORK_BLOCK on the next line is 116,780,000, so "that height" names neither number unambiguously — state the two heights separately, or fork at the verified block.
  • simulations/vip-665/bscmainnet.ts:41-42 — comment sits on the U_FLUX_SOURCE constant but describes the assertion at 165-169, not the constant — move it onto that it, leave a one-line label on the constant.

Complexity

  • simulations/vip-665/bscmainnet.ts:84 and :146 — the only two as [Contract, string][] inline casts in the repo; the convention is a typed declaration (const legs: [Contract, string][] at simulations/vip-654/bscmainnet.ts:65, and line 95 of this very file) — hoist const frvGroups: [Contract, string][] = [[uHub, U_FRV_SOURCE], [usdtHub, USDT_FRV_SOURCE]] to describe scope, which drops both casts and the duplicated literal.
  • simulations/vip-665/bscmainnet.ts:81-90 / :143-152 — the two cap-assertion loops are the same four expects (registered / paused / absoluteCap / percentageCapBps) differing only in the expected bps — extract expectFrvCaps(bps) and call it with _OLD then _NEW.

Organization

  • simulations/vip-665/bscmainnet.ts:35,38,43OPERATOR, U_CORE_SOURCE, U_FLUX_SOURCE (and U_VTOKEN, 39) re-literal addresses that already exist in vips/vip-650/addresses/bscmainnet.ts (OPERATOR:49, STACKS_PART_2) — import them; cross-VIP address imports are established here (simulations/vip-265/bscmainnet.ts imports vips/vip-248/.../Addresses), and the sim is not an on-chain artifact so it has no reason to keep its own copy.
  • vips/vip-665/bscmainnet.ts:56CASH_PLUS_VAULT is the only non-checksummed address literal in the PR (every other one here and in vip-650's address book is checksummed) — checksum it.

Checked, no finding: the vendored ABIs are plain ABI arrays with no bytecode and Hub.json / YieldGroupFRV.json are byte-identical to simulations/vip-650/abi/, which is this repo's convention (114 sim dirs carry their own wholesale ACM copy); every exported constant is used; the 5-command payload is built inline with no aggregator or wrapper layer, which is the right shape at this size.


Functional Correctness

  • simulations/vip-665/bscmainnet.ts:162low. The parseUnits("1", 18) tolerance and its stated rationale ("within adapter rounding dust") are both wrong: Hub.yieldGroupEffectiveCap returns (totalAssets() * pctBps) / 10_000 and the test recomputes totalAssets().mul(5000).div(10_000) from the same fork block with no state-changing tx in between (the U Hub's only funded leg is Core, valued off exchangeRateStored, and Flux holds 0), so the two sides are bit-identical and the delta is exactly 0 — no adapter is on the _effectiveCap path at all. Trigger: at the U Hub's live TVL of 10.005380406076675554 U, a ±1e18 window admits any percentageCapBps in [4000, 6000], so a 4000- or 6000-bps typo in the payload passes this assertion. It is caught by the exact cfg.percentageCapBps check at line 151, so nothing escapes the suite as shipped — fix is to assert effAfter.eq(half) and drop the tolerance comment.

Nothing else. The five-command payload verifies clean; details below.

Verified against BNB Chain mainnet (head ~116,792,100, not the fork block — no archive RPC on this host, so the 19/19 sim run itself was not reproduced)
  • Addresses. U_HUB/U_FRV_SOURCE/USDT_HUB/USDT_FRV_SOURCE/ADAPTER_FRV/U_CORE_SOURCE/U_VTOKEN/U_FLUX_SOURCE/OPERATOR all match VIP-650's address book (vips/vip-650/addresses/bscmainnet.ts). hub.asset()/source.asset() match U and USDT respectively.
  • CASH+ vault. 0x41179f…bcea = FRV Asseto CASH+ 13AUG2026-30, asset() == U, AdapterFRV.asset(vault) == U, EIP-1167 clone of InstitutionalLoanVault, state() == 1 (MarginDeposited) as claimed. It is the only U vault in the controller's registry (allVaultsLength() == 2; the other is a matured USDT Matrixdock vault), so the target is unambiguous and the "Ceffu USDT vault not yet deployed" scope note is accurate. AdapterFRV.validateRegistration is external pure no-op, so registration cannot be vetoed.
  • Payload executes. All five commands eth_call-simulate green from the Normal Timelock (0x939bD8…6396); ACM hasRole is true for all five (contract, signature) pairs; ProposalType.REGULAR → NormalTimelock in the framework, so the role set matches the executor.
  • Cap semantics. Third arg is bps; on-chain yieldGroupConfig is (5e24, 3000, paused=false, registered=true) on both FRV groups, so parseUnits("5000000",18) matches the stored absolute cap exactly (U and USDT are both 18-decimal). HubAdminLib.setYieldGroupCap(raising=true) rejects only a strict decrease in either dimension or a full no-op, so equal absolute + 3000→5000 passes. Correct hub/source pairing (a swap would revert YieldGroupNotRegistered). Effective cap goes 3.0016 → 5.0027 U on the U Hub and 810,172 → 1,350,287 USDT on the USDT Hub; both stay under the 5M absolute.
  • Ordering. setInnerDepositQueue/setInnerWithdrawQueue run _validateQueue, which reverts ResourceNotRegistered for an unregistered entry — confirmed live: calling setInnerDepositQueue([vault]) today reverts 0xb03667fd + the vault address. addResource first is required, and the five commands are one atomic proposal.
  • Queue wiring. [vault] on both inner queues mirrors stackCommands() in vips/vip-650/commands.ts exactly (Core/Flux got addResource + both inner queues set to the single resource). Both Hubs' outerDepositQueue() is [core, flux] — FRV is genuinely absent, so lender deposits are not auto-routed; Hub._aggregateDepositCapacity also iterates only the outer deposit queue, so the Hub's advertised maxDeposit() is not inflated by the new inner-queue entry. outerWithdrawQueue() is [flux, core, frv] unchanged. The FRV group's _prepareResource (vault.updateVaultState()) sits outside the inner try/catch, but Hub._tryWithdrawOneYieldGroup wraps the whole group call in try/catch and sizes off maxWithdraw() first, so a reverting vault poke cannot brick lender withdrawals.
  • Behavioural proof holds. ResourceCapacityExceeded is thrown at YieldGroupBase.depositResource only after the registered and paused gates, so reaching it does prove registration went live; no other contract on the reallocate path can emit that selector (the Core pull leg fails with ResourceLiquidityInsufficient). The (1e18, 0) args match today's state: AdapterFRV.maxDeposit(vault) == 0 because the vault is pre-Fundraising, and _resourceDepositRoom is the raw adapter headroom (YieldGroupFRV has no per-resource cap), so there is no unset-cap confound. minSupplierDeposit == 0, so the min-deposit branch is dead. Push of 1 U clears the Hub cap on both sides (3.0016 / 5.0027 U) and the Core source has 10.005 U liquid, so the pull leg cannot be what reverts. updateVaultState() is permissionless and succeeds today.
  • expectEvents [1,1,1,2]. addResource→1 ResourceAdded, each setter→1 event, raiseYieldGroupCap×2→2 YieldGroupCapRaised. expectEvents parses every log once per ABI, so a name present in both vendored ABIs would double-count — the only overlaps between Hub.json and YieldGroupFRV.json are DepositRouted/Initialized/Swept/WithdrawRouted, none of which are asserted. Both vendored ABIs carry named outputs, so cfg.registered / cfg.absoluteCap / resourceConfig destructuring resolves.
  • uEffectiveCapBefore. Assigned in the second pre-VIP it and read in a later describe; mocha's ordering guarantees the write precedes the read, and testVip's loadFixture snapshot is taken after the pre-VIP block, so nothing rewinds it. The effAfter.gt(before) comparison is sound here specifically because Core is valued off exchangeRateStored and nothing pokes the vToken during the ~5 simulated days of voting + timelock, so TVL growth cannot masquerade as the raise.
  • Deferred Ceffu leg. No placeholder address, zero address, TODO or predicted-address reference anywhere in either new file — the only mentions are two prose lines explaining the deferral. Raising the USDT Hub cap with USDT_FRV_SOURCE.resources() == [] is inert: an empty inner deposit queue makes YieldGroupBase.deposit refund everything and return 0, and maxDeposit() returns 0, so the cascade skips the group and no funds can strand.

Intent note (not a code defect): VDB-53 asks for addResource(ceffuFRV, …) with the vault address predicted; the PR drops that leg to a follow-up VIP instead. That is disclosed in both the PR body and the on-chain description, and the Ceffu vault genuinely is not deployed, but it does mean this VIP delivers 2 of the ticket's 3 actions.


Security

Reviewed the diff adversarially and traced every suspicion into venus-liquidity-hub and fixed-rate-vaults, with all state claims re-verified on BNB Chain mainnet (block ~116,792,252). The payload's own access-control claims hold up; the risk is in what registering this vault imports, not in who calls the five commands.

1. vips/vip-665/bscmainnet.ts:133-135 (addResource) — registering CASH+ puts Hub user funds inside a trust domain where a 3-of-6 Safe can extract them with no timelock. Severity: HIGH. Confidence: medium-high.

The VIP verifies the caller's roles but not the role set that registration imports. Traced end-to-end:

  • ACM role sweep(address,address) on InstitutionalVaultController (0x6D9e91cB766259af42619c14c994E694E57e6E85) is held by NormalTimelock (48h), FastTrackTimelock (6h) and CriticalGuardian 0x7B1AE5Ea599bC56734624b95589e7E8E64C351c9 — a 3-of-6 Gnosis Safe with no delay at all.
  • setTreasury(address) on the same controller is held by NormalTimelock and CriticalGuardian; it rejects only the zero address.
  • InstitutionalVaultController.sweep(vault, token)BaseVault.sweep(token), which transfers the entire balance of any token to treasury(). Unlike YieldGroupBase.sweep, which explicitly reverts SweepProtectedAsset, the FRV vault's sweep has no supply-asset exclusion.
  • isRegistered[0x41179fc6…] is already true, so the path is live for this exact vault.

So setTreasury(attacker) + sweep(CASH_PLUS_VAULT, U) in one bundle drains whatever Hub capital sits in the vault during Fundraising. Worse, AdapterFRV._breakdown marks non-terminal positions off rt.totalRaised, not the vault's actual token balance — so after the drain the Hub keeps reporting full principal with no on-chain signal that the backing is gone.

Precondition is a privileged multisig, not a permissionless exploit — but this is a unilateral, zero-delay extraction primitive over user funds, which is stronger than the pause-only powers guardians normally hold. Before this VIP, zero Hub funds were reachable this way (reallocate reverts ResourceNotRegistered — the sim proves it); after it, up to min(5M, 50% × TVL) is.

Fix: exclude _config.supplyAsset from BaseVault.sweep (mirror Hub.sweep's guard), and narrow sweep/setTreasury on the controller to the Normal Timelock. Not immediately exploitable — the vault is pre-Fundraising, maxDeposit == 0 — so this VIP can ship, but the Operator must not push funds until this is closed.

2. vips/vip-665/bscmainnet.ts:152 and :157 (raiseYieldGroupCap 30%→50%) — widens the only active containment for two acknowledged design risks. Severity: MEDIUM. Confidence: high on mechanism.

The per-source cap is doing all the work here:

  • FRV positions are non-withdrawable (maxWithdraw == 0) through Fundraising/Lock/PendingSettlement — up to 40 days on this vault's on-chain config (openDuration 7d + lockDuration 30d + settlementWindow 3d). Up to 50% of a Hub's TVL can be illiquid; the outer withdraw queue [Flux, Core, FRV] can then serve only the liquid half and user withdrawals revert.
  • AdapterFRV marks a locked position at principal + straight-line accrued coupon while that value is non-deliverable. Its own NatSpec concedes a defaulting position is "marked above its realizable value," bounded by "governance's per-source cap" and "an exit fee as anti-sandwich friction." On-chain, both Hubs have redeemFeeBps == 0 and feeBps == (0,0)the exit-fee mitigation is off. This raises the primary containment 67% while the secondary one is inactive, so early redeemers can exit at the marked-up price out of liquid Flux/Core assets and leave the remaining users to absorb a settlement shortfall.

Scale: the U Hub holds only ~10.005 U, so its 50% is immaterial today — but the USDT Hub holds 2,700,575 USDT, whose FRV headroom rises 810k → 1.35M USDT. Fix: set a non-zero redeemFeeBps alongside the raise, or hold FRV at 30% until an exit fee is live.

3. vips/vip-665/bscmainnet.ts:137-141 (setInnerDepositQueue) — unnecessary, and pre-opens the gate the VIP's own safety argument depends on. Severity: LOW (defense-in-depth). Confidence: high on mechanism, medium on intent.

The Operator's push goes Hub.reallocateYieldGroupBase.depositResource, which per its own NatSpec "bypasses the inner deposit queue to place the full amount into one resource." The inner deposit queue is read only by the cascade path (YieldGroupBase.deposit), which is unreachable today because the FRV source is deliberately out of the outer deposit queue (verified: outerDepositQueue == [Core, Flux]). So this command is a no-op for the stated goal — but it pre-arms automatic routing: if any later VIP or mistake adds the FRV source to the outer deposit queue, ordinary lender deposits immediately start cascading into CASH+ and locking up, with no further explicit authorization. It is also outside the approved scope (VDB-53/VPD-1867 list only addResource ×2 and the cap raise). Fix: drop it, or set it in the same VIP that adds FRV to the outer deposit queue.

4. vips/vip-665/bscmainnet.ts:56 (CASH_PLUS_VAULT lowercase) — forfeits checksum protection on the one constant that decides where funds go. Severity: LOW. Confidence: high.

The address is correct — verified independently of the PR's comments: EIP-1167 minimal proxy → impl 0xe87a1efced88bbddf8ccf78efb3bcf62cfdd5bdc (= InstitutionalLoanVault in fixed-rate-vaults/deployments), asset() == U, AdapterFRV.asset(vault) == U, and it is allVaults[1] with isRegistered == true on the governance-owned controller. But it is the only all-lowercase address in a file where the other five are EIP-55 checksummed, and all-lowercase bypasses ethers' checksum validation — a single-character corruption would encode silently instead of throwing. Fix: use 0x41179fc6ff878b7795B900888E0B61fd8029bceA.

5. simulations/vip-665/bscmainnet.ts:172-183 — the "behavioural proof" never moves funds. Severity: LOW. Confidence: high.

The vault is pre-Fundraising (state() == 1 MarginDeposited, runtime() timestamps all zero, maxDeposit == 0), so the post-VIP assertion only shows the revert reason changing from ResourceNotRegistered to ResourceCapacityExceeded(1e18, 0). That genuinely proves the registry flag flipped, but no deposit into or withdrawal out of CASH+ is ever executed — the diff wires a live mainnet Hub to an external vault with zero simulation coverage of its funds-moving paths. Fix: impersonate the controller to openVault into Fundraising and assert a full reallocate-in → settle → withdraw-out round trip.

Checked and clean
  • Access control on the five commands is exactly as claimed. All five roles verified held by the Normal Timelock via ACM.hasRole; FastTrack and Critical hold none of them, and there are no mainnet wildcard (address(0)) grants for the Hub/YieldGroup signature set. ProposalType.REGULAR is therefore correct and in fact the only track that works.
  • Addresses: all five Hub-side constants match venus-liquidity-hub/deployments/bscmainnet_addresses.json byte-for-byte. No wrong-chain or substitutable address.
  • Both cap commands will land: HubAdminLib.setYieldGroupCap accepts absolute-unchanged + percentage-strictly-increasing. Command ordering is correct (addResource precedes the queue setters, which reject unregistered resources).
  • Pre-state matches every sim assertion: both FRV groups at (5e24, 3000, unpaused, registered), resources() == [] on both, outer queues as asserted. Fork block 116,780,000 does not predate any relevant state.
  • initMainnetUser(OPERATOR, …) is not a fabricated privilege0x83f4262… genuinely holds reallocate on the U Hub on-chain.
  • No withdraw-DoS via the state poke. _prepareResourceupdateVaultState() sits outside the YieldGroup's try/catch (YieldGroupBase.sol:530,578), but Hub._tryWithdrawOneYieldGroup wraps the whole group withdraw in try/catch, so a reverting vault poke is isolated to a skipped group, not a user-withdrawal halt.
  • No residual-approval bug. AdapterFRV.deposit leaves a stale allowance only if BaseVault.deposit clamps, and both call sites bound amount ≤ maxDeposit, so the clamp is unreachable through the Hub. BaseVault uses plain Context, not ERC-2771, so _msgSender() can't be spoofed.
  • Vendored ABIs are faithful, not doctored: yieldGroupConfig's tuple order matches IHub.YieldGroupConfig (absoluteCap, percentageCapBps, paused, registered), resourceConfig matches (registered, paused, adapter), reallocate tuples match. No bytecode, embedded addresses, URLs, keys or secrets anywhere in the diff.
  • Scope matches approval: VDB-53/VPD-1867 explicitly approved "adjust the frv percentage cap on both hub to 50%, value cap unchange," so the USDT-Hub raise is in scope, not broader than approved. Deferring Ceffu instead of hardcoding a predicted vault address is the right call.

Escalation: Security finding 1 crosses three repos (vipsvenus-liquidity-hubfixed-rate-vaults) and concerns a live Hub holding user funds. Recommend a full multi-researcher audit of the FRV trust boundary — specifically the vault sweep/treasury authority set — before the Operator's first material reallocation, independently of whether this VIP ships.

trumpgpt-bot and others added 2 commits August 19, 2026 05:55
…edupe addresses

- drop setInnerDepositQueue command (out of VDB-53 scope + pre-arms FRV cascade)
- checksum CASH_PLUS_VAULT (EIP-55)
- import Hub/FRV/adapter/cap constants from vip-650 address book
- tighten effective-cap assertion to exact equality; drop bogus tolerance
- hoist frvGroups typed decl + expectFrvCaps helper; trim redundant docs
- record sweep/setTreasury + redeemFeeBps guardrails in the VIP description

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the Ceffu fixed-rate vault leg to VIP-665 alongside the existing
Cash+ leg, per review feedback. The Ceffu vault is not yet deployed; its
address is the deterministic CREATE2 clone address read live from the
InstitutionalVaultController (predictVaultAddress(CEFFU_INSTITUTION) ==
0x086fd7972510dF9d9cFdc4efB8677fc72d290103), verified on-chain at HEAD
and at the sim fork block.

VIP now, per USDT Hub FRV source: addResource(Ceffu, AdapterFRV) +
setInnerWithdrawQueue([Ceffu]); the USDT Hub FRV cap raise to 50% was
already present. No new ACM grants (Normal Timelock already holds the
roles on the USDT FRV source, verified).

Sim: assert the controller still predicts CEFFU_VAULT, etch a minimal
FRV-vault stub at that address (asset()==USDT), and mirror the Cash+
pre/post registration + reallocate-push behavioural checks. 27/27 green
on the bscmainnet fork; eslint + prettier clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vancegpt-bot

Copy link
Copy Markdown
Contributor

Review — VIP-665 round 2 (39eae990..eda44819)

Second pass by the same three-dimension squad (code quality / functional correctness / security),
reviewing the delta since the round-1 review: fce6d317 (round-1 fixes) and
eda44819 (the new Ceffu leg). All on-chain claims below were re-verified independently against BNB
Chain mainnet at head ~116,811,300–116,812,350, not taken from the PR text.

Round-1 fixes: all landed. Every one of the eight code-quality findings is fixed, the functional
correctness finding is fixed correctly (effAfter.eq(half) — exact equality is right, and the loose
tolerance was not reintroduced on the USDT leg), and security #3 and #4 are fixed
(setInnerDepositQueue dropped from both legs; CASH_PLUS_VAULT checksummed). Thanks — that was
a clean turnaround. Security #1, #2 and #5 are not fixed, and the Ceffu leg widens all three.

The headline is the Ceffu leg, and both correctness and security independently reached the same
recommendation: put it back where the PR originally had it — a follow-up VIP, proposed after the
vault is deployed.
Not because registering an empty address is unsafe in itself — it is not, and
the framing in our dispatch was wrong, see below — but because:

  1. The address commits to code, not to terms. The CREATE2 salt is
    keccak256(abi.encode(institutionOperator, nonce)) — it pins the institution, the nonce and (via
    EIP-1167 initcode) the implementation. It commits to none of VaultConfig / InstConfig /
    RiskConfig. So supplyAsset, fixedAPY, openDuration, lockDuration, settlementWindow,
    maxBorrowCap and idealCollateralAmount are all chosen at createVault time — after the
    vote
    — by a createVault holder, and that set includes the CriticalGuardian 3-of-6 Safe with
    zero delay. _validateVaultConfig bounds only sanity (non-zero durations, fixedAPY ≤ MAX_APY_BPS); there is no upper bound on lock length or collateralisation. Voters approve an
    address; the counterparty terms behind it are set afterwards with no further governance step.
  2. The one safeguard the proposal offers voters for this does not work. The
    predictVaultAddress assertion runs against a fork pinned at block 116,780,000, so it returns the
    same value forever regardless of mainnet — and the on-chain meta.description tells voters "the
    fork simulation asserts the prediction still holds."
  3. It couples the whole proposal to an external deployment. All six commands are one atomic
    transaction, so if the vault has not landed by execution time the three already-verified CASH+
    commands and both cap raises revert with it.

The CASH+ leg and the cap raises are separable and could proceed on the round-1 terms.

Correction to our own round-2 brief: we asked both reviewers to check whether addResource on an
undeployed address silently registers a dead resource, and whether someone could front-run the
deployment to inherit a pre-approved Hub resource. Both independently established that it cannot
YieldGroupBase.addResource:115 has an explicit resource.code.length == 0 → ResourceNotContract
guard, confirmed live (the call reverts today with 0x54918cb4 + the vault address), and hitting the
predicted address requires the clone implementation to be exactly 0xe87A1eFC…5bdC, which is a
non-upgradeable plain contract. There is no standing grant on an empty address and no
deployment front-run. The registration path fails closed. The exposure is narrower than we asked
about, and it is the config, not the code.

Counts: Code Quality 12 · Functional Correctness 6 · Security 7.

Also — the PR description is stale. It still describes the 5-command, Ceffu-deferred version
("the Ceffu USDT FRV vault is not yet deployed on-chain, so its addResource is deferred to a
follow-up VIP; only the USDT Hub's FRV cap is raised here", "passes 19/19"), while the head commit
registers Ceffu at a predicted address across 6 commands with 27 assertions. The on-chain
meta.description is current; please bring the PR body in line so reviewers and voters are reading
the same proposal.


Code Quality

All eight round-1 findings landed (status list at the bottom). Twelve new findings, all introduced by
the Ceffu leg or by the way the round-1 fixes were written up.

Comments

  • vips/vip-665/bscmainnet.ts:57-62 — the Ceffu CREATE2/prediction explanation is now written three times in one file: banner 25-29, this constant block, and meta.description 91-96 (plus a condensed fourth copy at simulations/vip-665/bscmainnet.ts:133-135) — cut this block to one line ("predicted CREATE2 clone address for CEFFU_INSTITUTION — see header"), the banner already carries the verification and the description the voter-facing version.
  • vips/vip-665/bscmainnet.ts:136 — the meta.description heading "Security / operational guardrails (reviewer findings — record before merge)" writes PR-review provenance and a repo merge instruction into permanent voter-facing on-chain text; a voter cannot act on "record before merge" — retitle to #### Operational guardrails and drop the parenthetical (the two bullets themselves are fine there — they condition the approval).
  • simulations/vip-665/bscmainnet.ts:253-258 — four of this comment's seven lines argue why a funds-movement round trip is not simulated ("out of scope here … Once open, the same call funds"), which answers a reviewer rather than describing the test — keep the first two sentences (what the revert-reason change proves), delete the defence.
  • vips/vip-665/bscmainnet.ts:54 and :61-62 — "EIP-55 checksummed so ethers validates the literal" / "All EIP-55 checksummed so ethers validates the literals" state the repo default twice and only exist to record a round-1 fix — delete both sentences.
  • vips/vip-665/bscmainnet.ts:35 and simulations/vip-665/bscmainnet.ts:56-57 — "rather than being re-literalled" / "instead of being re-literalled here" describe what the previous revision did wrong, not what the code does — drop the clause, keep "from VIP-650's address book".
  • simulations/vip-665/bscmainnet.ts:190-191 — "No inner deposit-queue event (that command was dropped from both legs)" narrates the PR's revision history; a reader of the file never saw that command — "the inner deposit queue is left unset, so no such event is expected".
  • simulations/vip-665/bscmainnet.ts:34-37 — the prose enumeration of the stub's five functions is immediately followed by the Solidity source showing the same five — keep the one non-obvious line (asset() must be USDT or addResource's asset-match check fails) and let the source listing carry the rest.

Complexity / duplication

  • simulations/vip-665/bscmainnet.ts:116-130, :168-184, :202-226, :259-275 — the Ceffu leg was added by copy-pasting five it bodies verbatim and swapping (hub, frv source, vault, core, vToken); expectFrvCaps at 102-110 already proves the cure works in this file — hoist const LEGS = [{name:"U/CASH+", hub:uHub, frv:uFrv, source:U_FRV_SOURCE, vault:CASH_PLUS_VAULT, core:U_CORE_SOURCE, vToken:U_VTOKEN}, {…Ceffu…}] and generate each pair with for (const leg of LEGS).
  • simulations/vip-665/bscmainnet.ts:169,178,260,270initMainnetUser(OPERATOR, parseEther("1")) is re-run inside four separate tests — resolve it once into an operator signer in a before().
  • vips/vip-665/bscmainnet.ts:36-42 and simulations/vip-665/bscmainnet.ts:58-67 — the same "find the stack in STACKS or throw" lookup is written three times (once as the stack() helper, twice inline in the sim) — export uStack/usdtStack from the VIP module and import them in the sim, deleting both inline copies.
  • vips/vip-665/bscmainnet.ts:165-199 — the two payload legs are identical modulo (frv source, vault, hub), and vips/vip-650/bscmainnet-part-2.ts:74 already ships the shape for this (STACKS_PART_2.flatMap(stackCommands)) — [[uStack, CASH_PLUS_VAULT], [usdtStack, CEFFU_VAULT]].flatMap(([s, vault]) => [...]). Judgement call at two legs: explicit literals are defensible in a governance payload, so collapse it or leave it deliberately, but the "too small to loop" argument from round 1 no longer holds.

Organization

  • vips/vip-665/bscmainnet.ts:51,70export { ADAPTER_FRV } and export { FRV_ABSOLUTE_CAP, … } are pass-throughs of vip-650 constants that exist only so the sim can import them from vip-665, yet the sim already imports OPERATOR, STACKS straight from vip-650 (simulations/vip-665/bscmainnet.ts:10) — drop the two pass-throughs and import both from vip-650 as well (FRV_PERCENTAGE_CAP_BPS_OLD is the one that earns its keep — it renames).
Checked, no finding · and round-1 finding status

The etched MockFRVVault stub is the established repo pattern, not a new fixture layer — simulations/vip-656/bscmainnet.ts:73 and simulations/vip-627/bscmainnet.ts:258 both carry a hex blob plus its Solidity source in a comment, and vip-656 also does the etch inside a [Test-Only] it in the pre-VIP block, so the ordering dependency it creates is the convention here, not a defect. CONTROLLER_ABI as a one-line human-readable ABI is right-sized. Both files (206 / 277 lines) sit mid-range for this repo (vip-664 is 419). Every export is consumed; no dead parameters or one-implementation abstractions in the new code.

Round-1 findings — current state

  • 36-line banner duplicating meta.description — fixed (banner is 23 lines and verification-only), but see the new Ceffu triple above.
  • Notes bullet repeating "does NOT auto-route deposits" — fixed; the bullet now covers only the inner deposit queue.
  • _effectiveCap stated four times — resolved: the banner and trailing-comment copies are gone, and the surviving block at vips/vip-665/bscmainnet.ts:67-69 is now the only place the min() semantics appear.
  • Fork-block comment citing two heights ambiguously — fixed; simulations/vip-665/bscmainnet.ts:52-54 states both separately.
  • Misplaced U_FLUX_SOURCE comment — fixed; :56-57 now labels the constant block it sits on.
  • Two as [Contract, string][] casts — fixed; const frvGroups: [Contract, string][] at :95.
  • Duplicated cap-assertion loops — fixed; expectFrvCaps(bps) at :102-110.
  • Re-literalled addresses (OPERATOR, U_CORE_SOURCE, U_FLUX_SOURCE, U_VTOKEN) — fixed; all come from vip-650's address book now.
  • Non-checksummed CASH_PLUS_VAULT — fixed at :55 (it picked up a redundant comment, above).

Functional Correctness

Six findings, all on the new Ceffu leg. Every on-chain claim re-verified against BNB Chain mainnet at block 116,811,302 (not taken from the PR text).

Round-1 finding is fixed and correct. simulations/vip-665/bscmainnet.ts:240 now asserts effAfter.eq(half); Hub._effectiveCap (Hub.sol:1219-1223) is (ta * pctBps) / 10_000 with ta = totalAssets(), and the test recomputes from the same totalAssets() at the same block, so exact equality is right. The loose pattern was not reintroduced on the USDT leg — that leg has no effective-cap assertion at all, and a bps typo is still caught by cfg.percentageCapBps in expectFrvCaps.

  • vips/vip-665/bscmainnet.ts:188addResource(CEFFU_VAULT, ADAPTER_FRV) targets an address with no code today (verified eth_getCode(0x086fd797…0103) == 0x), and its deployment is a fixed-rate-vaults action gated on the controller's createVault role, not on this VIP. YieldGroupBase.addResource:115 is if (resource.code.length == 0) revert ResourceNotContract(resource) — so if the vault has not landed by execution time the entire six-command Timelock execution reverts, taking down the three already-verified CASH+/U-Hub commands and both cap raises with it. Verified Normal Timelock delay() == 172800 (48h) and GRACE_PERIOD() == 1209600 (14d), so execute is retryable only inside that 14-day window; past it the whole proposal expires and must be re-proposed and re-voted. — Fix: split the Ceffu leg into its own VIP, proposed after the vault exists.

  • vips/vip-665/bscmainnet.ts:65 (and header :25-29, description :95) — the predicted address is stable only while both the institution nonce and vaultImplementation are unchanged; the VIP names only the nonce. predictVaultAddress is Clones.predictDeterministicAddress(vaultImplementation, keccak256(abi.encode(institution, institutionNonce[institution]))) (InstitutionalVaultController.sol:544-548), and vaultImplementation is a mutable slot with a live setter (:452-458). I reproduced 0x086fd797…0103 independently via cast create2 from deployer 0x6D9e91cB…, impl 0xe87A1eFC… and salt keccak256(abi.encode(0x8972E6F8…, 0)) — the impl is inside the EIP-1167 initcode, so changing it moves the address even at nonce 0. Trigger: fixed-rate-vaults ships a new InstitutionalLoanVault (repo is at 1.0.0-dev.6, actively changing) and setVaultImplementation is called before the Ceffu vault is created — verified held by NormalTimelock and FastTrackTimelock (6h delay, delay() == 21600), i.e. executable well inside this REGULAR VIP's ~5-day window. Same outcome from the nonce side: createVault is verified held by the CriticalGuardian 0x7B1AE5Ea…, a 3-of-6 Safe with zero delay (getThreshold() == 3, 6 owners), so any other Ceffu vault created during voting bumps the nonce. Either way CEFFU_VAULT is never occupied and the finding above fires. — Fix: state both stability conditions and re-derive predictVaultAddress immediately before queueing.

  • simulations/vip-665/bscmainnet.ts:132-137 — the drift guard cannot fail. Its comment claims "If the institution deploys a different vault first, this fails and the VIP's address must be re-derived before shipping." But forking(FORK_BLOCK) pins the fork at block 116,780,000 (:54), so controller.predictVaultAddress reads frozen historical state where institutionNonce == 0; no transaction mined after that block can change the result. Trigger: the nonce or implementation advances tomorrow — the sim still passes, CI still reports green, and the VIP ships with a dead address. The happy path makes it worse: deploying the Ceffu vault (required before execution, per description :95) bumps the nonce to 1, so against a live block this assertion would fail precisely when the VIP is correct — it is anti-correlated with what it claims to check and cannot be repaired by un-pinning the block. — Fix: drop it and check predictVaultAddress against a live provider in a pre-queue script; a pinned fork cannot express this.

  • vips/vip-665/bscmainnet.ts:96 — the on-chain, voter-facing meta.description states "the fork simulation asserts the prediction still holds." Verified false by the finding above: the simulation asserts it against a pinned historical block and keeps passing after any drift. This is the only safeguard the description offers voters for a registration whose target does not yet exist, so voters are told a guarantee that does not exist. — Fix: delete the clause, or replace it with what is actually guaranteed — addResource reverts if nothing is deployed at that address, so the proposal fails closed rather than registering a dead resource.

  • vips/vip-665/bscmainnet.ts:113 — "Reverts unless the vault's asset matches the source asset (USDT), which it will." The address commits only to (controller, implementation, institution operator, nonce) — verified from createVault:192-196. It commits to none of _vaultConfig / _instConfig / _riskConfig, so supplyAsset, fixedAPY, openDuration, lockDuration, settlementWindow and reserveFactor are all chosen at createVault time, after the vote, by a createVault holder (including the zero-delay CriticalGuardian Safe). Trigger: the vault is created at nonce 0 with a wrong config, then cancelVault + redeploy at nonce 1 — the abandoned vault still sits at 0x086fd797…0103 with asset() == USDT, and AdapterFRV.validateRegistration is an external pure no-op (AdapterFRV.sol:180-184), so addResource succeeds and the USDT Hub (2,700,589 USDT live) is permanently wired to a cancelled vault; removing it needs another VIP. The same mechanism admits a live vault whose terms voters never saw. — Fix: pin the approved vault config in the description and verify the deployed vault's config() / institutionalConfig() before execution, or defer the leg until the vault exists.

  • simulations/vip-665/bscmainnet.ts:143-144expect(getCode(CEFFU_VAULT)).to.equal("0x") followed by the etch. With FORK_BLOCK pinned, this asserts the vault's absence forever — the exact state in which finding 1 fires — and the only execution path ever simulated is the stub. After the real vault is deployed the sim will still fork at 116,780,000, still assert 0x, and still etch, so no CI run ever exercises the state the VIP actually requires. Trigger: a Ceffu vault deployed with asset() != USDT reverts ResourceAssetMismatch on mainnet while the sim keeps passing against a stub hard-coded to return USDT. — Fix: bump FORK_BLOCK past the deployment and skip the etch when getCode(CEFFU_VAULT) != "0x".

Checked and clean
  • addResource fails closed, contra the "silently succeed with a dead resource" outcome the brief asked about — the resource.code.length == 0 guard fires before the asset probe, so a missing vault reverts rather than registering. setInnerWithdrawQueue never gets the chance (_validateQueue:710-716 would reject an unregistered entry anyway).
  • Dropping setInnerDepositQueue does not break the Operator push. depositResource (YieldGroupBase.sol:320-350) never reads the inner deposit queue — it goes straight to registered/paused_prepareResource_resourceDepositRoom. The Notes bullet at vips/vip-665/bscmainnet.ts:129-132 is accurate.
  • The stub covers exactly what the six commands touch. addResource reads asset(); setInnerWithdrawQueue reads balanceOf() via _requireWithdrawQueueCoversFundedResources:724-734; AdapterFRV._breakdown:200-207 short-circuits at shares == 0 before touching runtime() / config() / previewRedeem(). A real zero-balance vault takes the identical path, so the stub is not more cooperative than the real thing for this VIP's state. (It hides all of the valuation and lifecycle surface, but none of that is on the registration path.)
  • The etch survives loadFixture. testVip's first loadFixture(governanceFixture) runs in the commands describe's before(), after the Pre-VIP block, so the snapshot contains the etched code and the execution describe's revert restores it.
  • expectEvents [2, 2, 2] is correct. No name collisions between the two vendored ABIs on the three asserted events (ResourceAdded and InnerWithdrawQueueSet only in YieldGroupFRV.json, YieldGroupCapRaised only in Hub.json), so no double-counting.
  • Cap math on both Hubs. Verified live: U Hub TVL 10.005410916358435826 → effective FRV cap 3.0016 → 5.0027 U; USDT Hub TVL 2,700,589.31 → 810,176.79 → 1,350,294.65 USDT. Both stay far under the 5,000,000 absolute, so the percentage dimension binds on both and _effectiveCap returns pctCap. Both FRV groups still (5e24, 3000, unpaused, registered) with resources() == [], matching every pre-VIP assertion.
  • Description matches the payload. All six numbered actions correspond 1:1 and in order to the six makeProposal commands, with the correct target named in each item. The guardrail section's on-chain claims check out: sweep(address,address) and setTreasury(address) on the controller are both verified held by the CriticalGuardian Safe.

Security

All state re-verified against BNB Chain mainnet at block ~116,812,346, not taken from the PR text.

Headline: the brief's central hypothesis does not hold. addResource cannot register an unoccupied address — YieldGroupBase.addResource has an explicit resource.code.length == 0 → ResourceNotContract guard, confirmed live (addResource(CEFFU_VAULT, ADAPTER_FRV) from the Normal Timelock reverts today with 0x54918cb4 + the vault address). So there is no standing grant on an empty address, and no deployment front-run that inherits a pre-approved Hub resource. The real exposure is narrower but still serious: the address is pinned, the bytecode is pinned, and the economic terms are not — and they get chosen after the vote by a zero-delay multisig.

  • vips/vip-665/bscmainnet.ts:65,188governance votes on a CREATE2 address whose contract terms are set after the vote by a 3-of-6 Safe with no delay. HIGH, confidence high on mechanism. The clone salt is keccak256(abi.encode(institutionOperator, institutionNonce)) (InstitutionalVaultController.sol:193) — it commits to the institution and nonce only, not to VaultConfig/RiskConfig. On-chain: institutionNonce[CEFFU_INSTITUTION] == 0 and vaultImplementation == 0xe87A1eFC…5bdC, so the first createVault for that institution lands at exactly CEFFU_VAULT regardless of every economic parameter. createVault is held by NormalTimelock, FastTrackTimelock (6h) and CriticalGuardian 0x7B1AE5Ea…51c9 (3-of-6 Safe, threshold 3, no delay) — verified via ACM.hasRole. _validateVaultConfig (:600-633) bounds only sanity: non-zero durations, fixedAPY ≤ MAX_APY_BPS, mantissa ranges, non-zero oracle price — there is no upper bound on lockDuration, openDuration, settlementWindow, maxBorrowCap or idealCollateralAmount. The only property this VIP enforces at execution is AdapterFRV.asset(vault) == USDT plus code-present. So voters approve an address, and the counterparty terms — lock length, coupon, collateralisation, liquidation params — are fixed afterwards by a party including a no-delay Safe, with no further governance step, on a Hub holding 2,700,590 USDT whose FRV headroom this same VIP raises to 1,350,295 USDT. Fix: deploy the vault first and register a deployed, inspectable vault — i.e. keep the Ceffu leg as the follow-up VIP the PR originally scoped it to.

  • vips/vip-665/bscmainnet.ts:188round-1 HIGH [VEN-933] Vip Framework #1 now spans the 2.7M USDT Hub, not just the ~10 U Hub. HIGH, confidence high on mechanism, medium on exploitability. controller.sweep(vault, token) gates on ACM + isRegistered[vault], and createVault sets isRegistered = true; BaseVault.sweep (src/BaseVault.sol:162-170) still transfers the entire balance of any token to controller.treasury() with no supply-asset exclusion. sweep and setTreasury are both held by the CriticalGuardian Safe with no delay (setTreasury rejects only address(0)). Round 1 scoped this to the U Hub (~10 U); the Ceffu leg extends the identical primitive to the Hub that actually holds user funds. Honest preconditions, all verified: reaching Fundraising needs CEFFU_INSTITUTION to post margin (depositCollateral is onlyPositionHolder) plus an openVault call, and capital only arrives via reallocate, which on the USDT Hub is held by the Operator only — CriticalGuardian does not hold it. Once funded, extraction is unilateral and instant. Fix: narrow sweep/setTreasury/createVault/openVault to the Normal Timelock and add the supply-asset exclusion before this VIP, not after.

  • vips/vip-665/bscmainnet.ts:138-143the meta.description guardrail note understates the exposure. MEDIUM, confidence high. It presents "not exploitable while a vault is pre-Fundraising (maxDeposit == 0), so this VIP ships safely" as the safety property. That state is one ACM-gated openVault call away, and openVault is held by the same CriticalGuardian Safe (verified). It also omits that the Safe chooses the vault's entire configuration (finding 1). The note is an off-chain commitment recorded in prose, not an on-chain control. Fix: state that the pre-Fundraising window is Safe-controlled and not a durable guarantee, or land the controller role changes first so the note is unnecessary.

  • simulations/vip-665/bscmainnet.ts:132-137the predictVaultAddress guard is inert and gives false assurance. MEDIUM, confidence high. forking(FORK_BLOCK) pins all state to block 116,780,000 (src/vip-framework/index.ts:71setForkBlock), so this assertion reads the controller at a fixed historical block and returns the same value forever regardless of mainnet. The inline claim — "If the institution deploys a different vault first, this fails and the VIP's address must be re-derived before shipping" — cannot happen: a nonce advance after the fork block is invisible to the sim. Note the failure mode it is meant to catch is worse than "the address moves": since nonce 0 maps to CEFFU_VAULT, an unintended first createVault for that institution occupies the exact address this VIP registers, and the VIP would then register that vault. meta.description:96 repeats the assurance. Fix: verify predictVaultAddress against live mainnet in a pre-flight check at proposal time; do not rely on a pinned-fork assertion for it.

  • vips/vip-665/bscmainnet.ts:198the 30%→50% raise on the USDT Hub is no longer inert. MEDIUM, confidence high. Round 1 rated this MEDIUM but noted the USDT leg was harmless because USDT_FRV_SOURCE.resources() == []. The Ceffu leg changes that: the USDT Hub gains its first FRV destination in the same proposal that widens its headroom 810,177 → 1,350,295 USDT. redeemFeeBps == 0 on the USDT Hub (verified live), so the secondary anti-sandwich brake round 1 flagged is still off while the primary containment is widened 67%. meta.description:144-148 acknowledges this and argues the Operator will hold off — again an off-chain commitment. Fix: set a non-zero redeemFeeBps in this VIP, or hold USDT FRV at 3000 bps until a vault is actually funded.

  • vips/vip-665/bscmainnet.ts:185-194the undeployed Ceffu leg couples the whole proposal to an external deployment. LOW, confidence high. Verified live: this addResource reverts today with ResourceNotContract (YieldGroupBase.sol:115). All six commands are one atomic proposal, so if the vault is not deployed by execution time the CASH+ registration and both cap raises fail with it, costing a full governance cycle. It fails safe — no silent registration of an empty address — hence LOW. Fix: gate proposal submission on the vault being live.

  • simulations/vip-665/bscmainnet.ts:268-275the Ceffu "behavioural proof" runs against etched stub code. LOW, confidence high. Round-1 [VEN-1303] add simulations for provide VAI/USDT liquidity VIP in PancakeSwap #5 restated, weaker for the new leg: the post-VIP push asserts against MOCK_FRV_VAULT_BYTECODE etched at the address, so it proves only that the Hub rejects a zero-capacity resource. None of the real InstitutionalLoanVault deposit/withdraw/settlement logic is exercised. Fix: as round 1 — impersonate the controller, create and open a real vault in the fork, assert a reallocate-in → settle → withdraw-out round trip.

Round-1 items confirmed fixed: #3setInnerDepositQueue dropped from both legs; only
setInnerWithdrawQueue remains, and the sim asserts innerDepositQueue stays empty on both sources.
#4CASH_PLUS_VAULT is now EIP-55 checksummed; CEFFU_VAULT, CONTROLLER and
CEFFU_INSTITUTION all round-trip through to-check-sum-address cleanly.

Checked and clean
  • No arbitrary code can land at the predicted address. To hit CEFFU_VAULT the clone implementation must be exactly 0xe87A1eFCED88bBddf8CCF78EfB3bCF62cFdd5bdC. That implementation is a plain contract — EIP-1967 implementation slot empty, 40,061 bytes — so it is not upgradeable, and post-Cancun SELFDESTRUCT cannot remove it. setVaultImplementation (Normal + FastTrack) would move the prediction, making addResource revert ResourceNotContract — fails safe. Only the config is free, which is finding 1.
  • CEFFU_INSTITUTION 0x8972E6F8…6262 is an external counterparty EOA (no code), institutionNonce == 0, no vault ever created for it. It cannot create or open the vault itself; its only role in the chain is posting margin as position holder.
  • ACM / executor re-derived for all six commands. Every (contract, signature) pair is held by the Normal Timelock only; FastTrack, Critical, Guardian and CriticalGuardian hold none, and there are no address(0) wildcard grants. ProposalType.REGULAR / Normal Timelock remains correct for the grown payload. (The Operator additionally holds setInnerWithdrawQueue and raiseYieldGroupCap — pre-existing from VIP-650, not introduced here.)
  • The MockFRVVault stub is test-only and inert. 195 bytes of executable code (remainder is CBOR metadata, solc 0.8.26); the dispatcher covers exactly the five documented selectors; no CALL/DELEGATECALL/STATICCALL/CREATE/SSTORE/LOG/SELFDESTRUCT anywhere in the executable region; one embedded address, BSC USDT 0x55d3…7955. hardhat_setCode and the constant appear only in simulations/vip-665/bscmainnet.ts — zero occurrences in vips/vip-665/bscmainnet.ts and in all three ABIs.
  • Vendored ABIs are faithful. All three byte-identical to simulations/vip-650/abi/*, plain ABI arrays whose entry keys are only [anonymous, inputs, name, outputs, stateMutability, type] — no bytecode, embedded addresses or extra keys. Each of the three asserted event names resolves in exactly one ABI, so expectEvents [2,2,2] cannot double-count.
  • No secrets, keys, mnemonics, URLs or injection surfaces anywhere in the diff.

Escalation (reiterating round 1, now with a wider surface): the crux is the
InstitutionalVaultController authority set — createVault, openVault, sweep and setTreasury
all reachable by a no-delay 3-of-6 Safe, with createVault/setVaultImplementation also on the 6h
FastTrack. That set is what makes the two HIGH findings real, and it spans three repos (vips
venus-liquidity-hubfixed-rate-vaults). We recommend the full multi-researcher audit of the FRV
trust boundary before the Ceffu leg ships, not merely before the first reallocation.

Option A: keep the single 6-command VIP with the Ceffu leg; the VIP is
only proposed after the Ceffu vault is deployed. Reword meta.description
and the CEFFU_VAULT comment so voters are not told the fork-pinned
predictVaultAddress assertion proves the address at execution time — a
fork at a fixed block returns the same value forever. The real guarantee
is the deploy-first sequencing plus the atomic revert (addResource fails
ResourceNotContract on a codeless address).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vancegpt-bot

Copy link
Copy Markdown
Contributor

Review — VIP-665 round 3 (eda44819..63356238)

Third pass by the same three-dimension squad, reviewing the one-commit delta since the round-2
review
. Assessed under
Option A — single atomic 6-command VIP, proposed only after the Ceffu vault is deployed at
0x086fD797…0103. That scope decision is settled and we did not re-argue it; everything below asks
what still needs to change given that sequencing commitment. All on-chain claims re-verified
independently against BNB Chain mainnet at block ~116,819,000–116,819,900.

The delta is comments and meta.description only — +13/-7 in one file. simulations/vip-665/bscmainnet.ts is byte-identical to round 2.

Three things to take away

1. "No fixes were needed" does not hold. Of the 25 round-2 findings, exactly one was addressed.
FC #4 — the voter-facing "the fork simulation asserts the prediction still holds" clause — is gone
from meta.description, and the ResourceNotContract fail-closed claim that replaced it verifies
out
(YieldGroupBase.addResource:115 fires the resource.code.length == 0 guard before the adapter
and asset probes; the call reverts live today with 0x54918cb4 + the vault address). That is a real
fix and the mechanism is stated correctly. All 12 code-quality findings, all 7 security findings and
the remaining 5 correctness findings are untouched — and the identical false assurance FC #4 removed
from the description still sits in-repo at simulations/vip-665/bscmainnet.ts:133-135.

2. The vault is still not deployed, and nothing enforces the gate. Verified independently by two
reviewers and by the lead against two RPC endpoints: eth_getCode(0x086fd797…0103) == 0x,
institutionNonce[CEFFU_INSTITUTION] == 0, controller.isRegistered(CEFFU_VAULT) == false. Option A
is a process commitment with no artifact in this repo that gates queueing on it — no pre-proposal
script, no CI check, no simulation assertion that can observe it. The whole safety argument for the
Ceffu leg now rests on a manual step that the codebase cannot see.

3. Option A closes one finding, and it does so by consuming the only on-chain guard the Ceffu leg
had.
ResourceNotContract fires on exactly one condition — no code at the address. Deploy-first
satisfies that condition deliberately, before the vote, so after deployment the guard can never
fire again
. What remains on the registration path is AdapterFRV.asset(vault) == USDT plus a pure
no-op validateRegistration (AdapterFRV.sol:183-187) — and no lifecycle-state check anywhere.
Security found three new primitives reachable in the ≥3-day (up to 17-day) proposal→execution window
that this creates: cancelVault, closeVault, and the timing reachability of openVault, all held
by the same zero-delay 3-of-6 CriticalGuardian Safe. Sequencing changed when the vault exists, not
who can act on it.

The one thing to fix before anything else: the description's only voter-verifiable check is
guaranteed to fail. createVault does institutionNonce[institution]++
(InstitutionalVaultController.sol:205) and predictVaultAddress reads the current nonce
(:544-548) — so once the vault is deployed, predictVaultAddress(CEFFU_INSTITUTION) returns the
nonce-1 clone, not CEFFU_VAULT. A voter who runs the check the description hands them gets a
mismatch, and the description's own caveat then tells them the address is stale. Under Option A a
correct VIP reads as broken. The paragraph also opens "The Ceffu vault is not yet deployed"
(:93) and closes "submitted only after the Ceffu vault has been deployed" (:100-101) — flatly
self-contradictory, in permanent on-chain text.

Counts: Code Quality 17 (12 round-2 still open, 5 new) · Functional Correctness 7 ·
Security 10. Both reviewers independently converged on the same simulation rebuild and the same
pre-proposal gate; those are consolidated at the bottom.


Code Quality

Triage: 0 of 12 round-2 findings fixed, 12 still open, 0 moot under Option A — and finding #1
regressed. Line numbers at head 63356238.

Comments

  • [VEN-933] Vip Framework #1 vips/vip-665/bscmainnet.ts:57-64still open, worse. Still four copies (banner :25-29, this block, description :93-102, sim :132-136); this commit grew the block 6→8 comment lines and the description paragraph 7→10, and added a fifth duplicated fact — the ResourceNotContract fail-closed mechanism now appears at both :61-62 and :97-99. Option A cuts the need further, not less: with the vault deployed before proposal, the CREATE2 derivation is authoring-time provenance — collapse this block to "predicted CREATE2 clone address for CEFFU_INSTITUTION — see header" and keep one voter-facing copy.
  • [VEN-1217]: VIP simulation Liquidator #2 vips/vip-665/bscmainnet.ts:142still open, unchanged. "Security / operational guardrails (reviewer findings — record before merge)" — retitle to #### Operational guardrails, drop the parenthetical.
  • VIP-105 simulations #3 simulations/vip-665/bscmainnet.ts:253-258still open, unchanged. Keep the first two sentences, delete the "out of scope here … Once open, the same call funds" defence.
  • VIP-123: Resilient Oracle #4 vips/vip-665/bscmainnet.ts:54 and :64still open. Both EIP-55 sentences survive; the second rode along to :64 as the tail of the grown block — delete both.
  • [VEN-1303] add simulations for provide VAI/USDT liquidity VIP in PancakeSwap #5 vips/vip-665/bscmainnet.ts:35 and simulations/vip-665/bscmainnet.ts:56-57still open, unchanged. Drop the "re-literalled" clauses.
  • [VEN-1311] add simulations for April, 10th week #6 simulations/vip-665/bscmainnet.ts:190-191still open, unchanged. Replace with "the inner deposit queue is left unset, so no such event is expected".
  • [VEN-1259] [VEN-1262]: Vault upgrades simulations - testnet #7 simulations/vip-665/bscmainnet.ts:34-37still open, unchanged. Keep the asset()-must-be-USDT line, let the source listing carry the rest.

Complexity / duplication

  • chore: update datatype of the function params #8 simulations/vip-665/bscmainnet.ts:116-130, :168-184, :202-226, :259-275still open, unchanged. Hoist a LEGS table and generate each pair with for (const leg of LEGS).
  • [VEN-1341] add simulations for VIP-109 #9 simulations/vip-665/bscmainnet.ts:169, :178, :260, :269still open, unchanged. Resolve initMainnetUser(OPERATOR, …) once into an operator signer in a before().
  • [VEN-1315] send proposal from script #10 vips/vip-665/bscmainnet.ts:36-42 and simulations/vip-665/bscmainnet.ts:58-67still open, unchanged. Export uStack/usdtStack from the VIP module, delete both inline copies.
  • [VEN-1146]: Added vip for comptroller diamond proxy #11 vips/vip-665/bscmainnet.ts:171-205still open, judgement call (unchanged from round 2). Explicit literals in a governance payload remain defensible at two legs; collapsing to [[uStack, CASH_PLUS_VAULT], [usdtStack, CEFFU_VAULT]].flatMap(…) is the alternative. Not a blocker.

Organization

New — the delta's own prose

  • vips/vip-665/bscmainnet.ts:93 — "The Ceffu vault is not yet deployed" contradicts :100-101 ("submitted only after the Ceffu vault has been deployed at …") inside the same paragraph, and under Option A the second is the one true at read time — open the paragraph with the deployed fact.
  • vips/vip-665/bscmainnet.ts:161 (and banner :25, :29) — "Ceffu vault (USDT, predicted, pending deployment)" / "NOT yet deployed" / "supplyAsset will be USDT" are all false once the proposal is submitted post-deployment — drop the parenthetical at :161, and mark the banner bullet as a dated authoring-time verification record.
  • vips/vip-665/bscmainnet.ts:60-64 — five of this block's eight comment lines now argue round-2 review points about a test in a different file ("that deployment, not the sim, is the real guarantee", "The sim's predictVaultAddress assertion is only a sanity check") — delete them; the block exists to say what CONTROLLER / CEFFU_INSTITUTION / CEFFU_VAULT are, and the cross-file claim goes stale the moment the sim assertion changes.
  • vips/vip-665/bscmainnet.ts:101 — "that deployment — not any simulation assertion — is what guarantees…" negates a guarantee voters were never offered and cannot inspect, in permanent on-chain text — cut to "This proposal is submitted only after the Ceffu vault has been deployed at ${CEFFU_VAULT}." (The fail-closed sentence at :97-99 is genuinely voter-facing — keep that one.)

New — missed by rounds 1 and 2

  • vips/vip-665/bscmainnet.ts:22-24 and :49-50 — the CASH+ asset-match justification is stated twice 27 lines apart in the same dev-facing header region (banner: "asset() == U and AdapterFRV.asset(vault) == U, so addResource's asset-match check passes"; constant comment: "AdapterFRV.asset(CASH_PLUS_VAULT) == U, so addResource does not revert ResourceAssetMismatch") — this is the CASH+ twin of [VEN-933] Vip Framework #1; keep the banner copy, cut :49-50 to "Shared FRV adapter (VIP-650); validateRegistration is a no-op."
Checked, no finding

The addr() helper is an established repo convention (simulations/vip-650/shared.ts), "all three Hub assets are 18-dec" at simulations/vip-665/bscmainnet.ts:74 is accurate per vip-650's address book, and both files (212 / 277 lines) remain mid-range for this repo.


Functional Correctness

Live state, for the record: eth_getCode(0x086fd797…0103) == 0x, institutionNonce[CEFFU_INSTITUTION] == 0,
predictVaultAddress(CEFFU_INSTITUTION) == 0x086fd797…0103, controller.isRegistered(CEFFU_VAULT) == false.
The Ceffu vault is still not deployed today, so the deploy-first commitment is as yet unfulfilled and
nothing in the repo prevents the VIP from being queued before it is. addResource(CEFFU_VAULT, ADAPTER_FRV)
from the Normal Timelock (0x939bD8d6…6396, role verified held) reverts 0x54918cb4 +
0x086fd797…0103 = ResourceNotContractthe description's fail-closed claim is accurate.

FORK_BLOCK is still 116780000 (simulations/vip-665/bscmainnet.ts:54), the guard is still at
:132-137, and :143 still asserts getCode == 0x before the etch at :144.

Triage of the six round-2 findings

# Round-2 finding Status under Option A
1 :188 atomic revert if the vault is undeployed at execution Relocated, not resolved. Deploy-first removes the cause, but the enforcement is a manual pre-proposal step: verified live that the vault is still undeployed and isRegistered == false, and no repo artifact gates queueing on it. Residual coupling (48h delay, 14d grace) is unchanged.
2 :65 prediction stable only while nonce and vaultImplementation are unchanged Moot in substance — once the vault is deployed the prediction stops mattering, the address is occupied by real code. But the incomplete caveat survives verbatim in the description (:97) and now decorates a claim that is false anyway — folded into finding 1 below.
3 sim :132-137 drift guard cannot fail Still open, untouched — and now actively harmful. See finding 2.
4 description "the fork simulation asserts the prediction still holds" Fixed in the voter-facing text, and the replacement ResourceNotContract claim verifies out. But the identical false assurance still sits in-repo at simulations/vip-665/bscmainnet.ts:133-135 — it was deleted from the description only.
5 :113 "which it will" — the address commits to code, not terms Relocated. Deploy-first genuinely improves this: the config becomes inspectable before the vote. Nothing in the PR inspects it — see finding 6.
6 sim :143-144 asserts absence, etches a stub Still open, untouched, and now directly contradicts the sequencing the description commits to. See finding 3.

Net: Option A resolves none of the six in the repo. It resolves #1/#2/#5 in principle, conditional on
a manual step, and leaves #3/#4/#6 — all three in the simulation — exactly as they were.

Findings

  • vips/vip-665/bscmainnet.ts:93-102 — the description's one voter-verifiable claim is guaranteed false at the moment a voter can check it: createVault does institutionNonce[institution]++ (InstitutionalVaultController.sol:205) and predictVaultAddress reads the current nonce (:544-548), so once the vault is deployed (nonce 0→1) predictVaultAddress(0x8972E6F8…6262) returns the nonce-1 clone, not 0x086fd797…0103; a voter running the check the description hands them gets a mismatch, and the description's own caveat "(this holds only while the institution's nonce is unchanged)" then tells them the address is stale — so a correct VIP reads as broken. Same paragraph opens "The Ceffu vault is not yet deployed" and closes "submitted only after the Ceffu vault has been deployed" — flatly self-contradictory. — Fix: delete the CREATE2/prediction paragraph entirely and state the deploy-first facts a voter can verify at proposal time: eth_getCode(CEFFU_VAULT) != 0x, controller.isRegistered(CEFFU_VAULT) == true, IInstitutionalLoanVault(CEFFU_VAULT).asset() == 0x55d3…7955, institutionalConfig().institutionOperator == CEFFU_INSTITUTION.

  • simulations/vip-665/bscmainnet.ts:136 — the drift guard is not merely inert, it inverts: as shipped, forking(116780000) freezes institutionNonce == 0 so no mainnet drift is visible and CI stays green; and after the obvious repair (bump the fork past the deployment, which Option A requires) nonce is 1 and this assertion fails precisely when the VIP is correct. It cannot be fixed by moving the block. Its comment at :133-135 still carries the round-2 VIP-123: Resilient Oracle #4 falsehood ("If the institution deploys a different vault first, this fails…"). — Fix: delete the test; assert expect(await controller.isRegistered(CEFFU_VAULT)).to.equal(true) instead — monotonic (set at InstitutionalVaultController.sol:207, never cleared; cancelVault:231-237 leaves it true).

  • simulations/vip-665/bscmainnet.ts:143-144 — the sim asserts the exact state Option A commits the proposal not to be in (getCode == 0x) and then etches a 195-byte stub whose asset() is a hard-coded return 0x55d3…7955, so no CI run ever executes against the real vault. Trigger: the deployed Ceffu vault is created with supplyAsset != USDT — nothing in the address commits it, the salt is keccak256(abi.encode(institution, nonce)) (InstitutionalVaultController.sol:193) and AdapterFRV.validateRegistration is an external pure no-op (AdapterFRV.sol:183-186) — mainnet reverts ResourceAssetMismatch and takes all six commands down while the sim passes green. — Fix: delete :143-144, the etch and MOCK_FRV_VAULT_BYTECODE; assert getCode != "0x" and read AdapterFRV.asset(CEFFU_VAULT) off the real vault.

  • simulations/vip-665/bscmainnet.ts:268-275 — the hard-coded ResourceCapacityExceeded(PUSH, 0) breaks as soon as the fork block moves into the deploy-first state. Verified the current assertion is right only because the stub returns maxDeposit == 0: depositResource checks amount > room → ResourceCapacityExceeded before _minDeposit (YieldGroupBase.sol:336-347). Trigger: fork past a Ceffu deployment whose vault has been opened into Fundraising (VaultState.Fundraising == 2, IVaultTypes.sol:15-28) — room > 0, so the push either succeeds outright or reverts ResourceBelowMinimumDeposit via _minDeposit → IFRVVault.config().minSupplierDeposit (YieldGroupFRV.sol:104-106), and the test fails on the wrong error. The stub cannot reach that branch at all — it has no config() selector. — Fix: assert the real vault.state() explicitly and branch the expected behaviour on it instead of hard-coding the pre-Fundraising revert.

  • vips/vip-665/bscmainnet.ts:25-29 and :57-64 — the header's on-chain verification record and the constant-block comment are falsified by the very step Option A adds. :25 "Ceffu (USDT) vault: NOT yet deployed", :28-29 "predictVaultAddress(CEFFU_INSTITUTION) == CEFFU_VAULT (checked at HEAD and at 116,780,000, so the institution's nonce has not advanced)", :57 "NOT yet deployed", :63 "The sim's predictVaultAddress assertion is only a sanity check". Trigger: the vault is deployed (the precondition for proposing) — the banner then records a verification that no longer reproduces, and the banner is what a future reader uses to re-derive the address. — Fix: re-run the verification after deployment and record getCode != 0x, isRegistered, asset() and the vault's lifecycle state instead of the prediction.

  • vips/vip-665/bscmainnet.ts:119 and :161 — the description is still written in the pre-deployment tense for facts that become checkable at proposal time under Option A: :119 "which it will" (action 4's asset match) and :161 "Ceffu vault (USDT, predicted, pending deployment)". Materially: deploy-first makes config() / institutionalConfig() / riskConfig() inspectable, but the description discloses no vault terms — so voters approve wiring the USDT Hub (verified live TVL 2,700,594.93 USDT, FRV headroom rising 810,178.53 → 1,350,297.46) to a counterparty whose coupon, lock length, settlement window and collateralisation appear nowhere in the proposal. — Fix: after deployment, replace "which it will" with the read value and add the approved config() terms to the description.

  • simulations/vip-665/bscmainnet.ts:243-249 — the outer-queue regression guard is asserted for the U Hub only, while the description at :139 claims it for "each Hub". Verified live that both Hubs are currently outerDepositQueue == [Core, Flux] and outerWithdrawQueue == [Flux, Core, FRV], so the claim is true today. Trigger: the Operator (0x83f42623…6a7A) holds setOuterDepositQueue(address[]) on the USDT Hub with no timelock — verified via ACM.hasRole — so an Operator queue change before execution puts USDT FRV into the outer deposit queue, lender deposits then cascade into the Ceffu vault automatically, contradicting the description's "FRV is filled only by the Operator's reallocate" (:90-91), and this sim passes. The unguarded Hub is the one holding 2.7M USDT and gaining its first FRV destination. — Fix: iterate the assertion over both stacks.

Does the reframed meta.description describe what a voter is approving?

Not yet. The ResourceNotContract fail-closed claim (:98-99) is accurate — verified live, and
YieldGroupBase.addResource:115 fires the resource.code.length == 0 guard before the adapter and asset
probes. The deploy-first claim (:100-102) is a correct statement of intent. But the paragraph they sit in
is self-contradictory (finding 1), its one actionable check is designed to fail (finding 1), and the
surrounding text — :93, :97, :119, :161 — is uniformly written for the pre-deployment world the
proposal is committing to leave. Two further notes:

  • :124-125 "the Hub's raise guard accepts (absolute unchanged, percentage strictly increases)" — verified
    correct
    : raiseYieldGroupCap reverts NotIncreasing only if a value decreases or both are unchanged
    (Hub/Hub.sol:225-228HubAdminLib.setYieldGroupCap). One dimension strictly rising is sufficient.
  • :147-148 "Not exploitable while a vault is pre-Fundraising (maxDeposit == 0), so this VIP ships safely"
    — under Option A the Ceffu vault exists for the whole voting window, so this premise is now an unverified
    runtime property rather than a structural one. Flagged here as a description-accuracy issue; the
    underlying exposure is Security's, below.
Checked and clean this round

addResource's guard ordering (YieldGroupBase.sol:111-124: access → zero-address → already-registered →
code.length → adapter code → asset match → validateRegistration) confirms the fail-closed claim.
setInnerWithdrawQueue validation (:203-209, _validateQueue:710-719,
_requireWithdrawQueueCoversFundedResources:725-735) is satisfied by a freshly registered zero-balance
resource. The six description actions still map 1:1 and in order onto the six makeProposal commands.
Both FRV groups verified live at (5e24, 3000 bps, registered, unpaused) with resources() == []; CASH+
verified state() == 1 (MarginDeposited), asset() == U, maxDeposit == 0, and AdapterFRV.asset(CASH_PLUS_VAULT) == U.
Round-1's effAfter.eq(half) fix remains correct and was not weakened.


Security

All state re-verified live at block ~116,819,900.

  • eth_getCode(0x086fd797…0103) == 0xthe Ceffu vault is still not deployed. Option A's
    pre-proposal gate is not yet met.
  • institutionNonce[0x8972E6F8…6262] == 0, predictVaultAddress(CEFFU_INSTITUTION) == CEFFU_VAULT,
    vaultImplementation == 0xe87A1eFC…5bdC (matches fixed-rate-vaults/deployments/bsc_mainnet_addresses.json),
    EIP-1967 impl/admin slots both empty, 40,061 bytes — all unchanged since round 2.
  • ACM role holders on the controller, unchanged since round 2, and wider than round 2 recorded:
    createVault / openVault / cancelVault / closeVault / partialPauseVault / unpauseVault /
    sweepNormalTimelock + FastTrackTimelock (6h) + CriticalGuardian (3-of-6 Safe, zero delay);
    setTreasury → NormalTimelock + CriticalGuardian; setVaultImplementation → NormalTimelock +
    FastTrackTimelock. No address(0) wildcard grants on any of them.
  • USDT Hub totalAssets 2,700,595 USDT, FRV effective cap 810,178 → 1,350,297 after this VIP,
    redeemFeeBps == 0, resources() == []. U Hub 10.0054 U, redeemFeeBps == 0.
  • Governance window: REGULAR voting period 192,384 blocks ≈ 1.0 day at the measured 0.45 s/block,
    NormalTimelock delay() == 172800 (48 h), GRACE_PERIOD() == 1209600 (14 d). So under Option A the
    deployed vault sits live and Safe-reachable for ≥3 days, up to 17 days, between proposal and
    execution — plus however long deployment precedes proposal.

Triage of the seven round-2 security findings

# Round-2 finding Round-3 verdict
1 :65,188 config chosen after the vote by a zero-delay 3-of-6 Safe Still open. Option A makes the config inspectable but the PR adds no inspection step, and three risk params stay mutable after inspection.
2 :188 sweep/setTreasury now spans the 2.7M USDT Hub Still open, unchanged. Roles re-verified identical; and the separation-of-duties argument that bounded it is weaker than round 2 stated (see the LOW below).
3 :138-143 guardrail note understates exposure Still open and now factually wrong, not merely incomplete — Option A moves the vault's lifecycle start before execution.
4 sim :132-137 predictVaultAddress guard is inert Still open. The delta relabels it "only a sanity check" in comments; the assertion is unchanged and is still the only automated check touching the Ceffu address.
5 :198 30%→50% raise on the USDT Hub no longer inert Still open, and widened by Option A — the registered vault may now hold real capacity at execution.
6 :185-194 atomic coupling to an external deployment Closed as a revert risk, replaced by a worse one. Deploy-first removes it by consuming the only on-chain guard the Ceffu leg had.
7 sim :268-275 behavioural proof runs against an etched stub Still open, and worse. The stub now stands in for a contract that will actually exist at proposal time.

Round-1 items #3 (setInnerDepositQueue dropped) and #4 (checksums) remain fixed.

What Option A closes, and what survives it

Closes: the round-2 functional exposure that the whole six-command proposal reverts because the
vault never landed (round-2 security #6, LOW). It also closes nonce/vaultImplementation drift in
principle
— once deployed, CEFFU_VAULT is verifiable by eth_getCode + isRegistered rather than
by a prediction. Note the consequence: after deployment institutionNonce becomes 1, so
predictVaultAddress will no longer return CEFFU_VAULT, and any live re-derivation must switch
to checking the deployed vault directly.

Survives, one by one, with the check that would close each:

  • Config chosen after the vote. Not closed. Deployment fixes VaultConfig/InstConfig/RiskConfig
    at createVault time, but nothing in this PR requires anyone to read them. Re-verified
    _validateVaultConfig (InstitutionalVaultController.sol:600-633) bounds only sanity: non-zero
    durations, 0 < fixedAPY ≤ 10000, minBorrowCap ≤ maxBorrowCap, idealCollateralAmount != 0,
    marginRate ≤ 1e18, LT·LI < 1. There is no bound tying idealCollateralAmount to
    maxBorrowCap
    — a vault with maxBorrowCap = 5,000,000 USDT and idealCollateralAmount = 1 wei
    passes validation. Closing check: pin the approved (supplyAsset, fixedAPY, minBorrowCap, maxBorrowCap, openDuration, lockDuration, settlementWindow, idealCollateralAmount, marginRate, liquidationThreshold, liquidationIncentive, latePenaltyRate) in meta.description, and add a
    pre-queue script that reads config() / institutionalConfig() / riskConfig() off the deployed
    vault and diffs them against those literals.
  • Address drift. Closed if and only if the pre-proposal gate is enforced and verified. Closing
    check:
    before queueing, assert eth_getCode(CEFFU_VAULT) != 0x, controller.isRegistered(CEFFU_VAULT) == true,
    AdapterFRV.asset(CEFFU_VAULT) == USDT, and runtime().state ∈ {WaitingForMargin, MarginDeposited}.
    Do not re-assert predictVaultAddress — it is guaranteed to be wrong post-deployment.
  • sweep / setTreasury reaching the 2.7M USDT Hub. Not closed at all — orthogonal to sequencing.
    Re-verified BaseVault.sweep:162-170 transfers the full balance of any token to
    controller.treasury() with no supply-asset exclusion, and AdapterFRV._breakdown:227 marks a
    Lock-state position from rt.totalRaised, i.e. bookkeeping, not the vault's actual balance — so
    a sweep does not reduce the Hub's marked NAV by one wei. Closing change: narrow sweep,
    setTreasury, createVault, openVault, cancelVault and closeVault to the Normal Timelock and
    add the supply-asset exclusion, before any reallocate.
  • 30%→50% cap raise. Not closed; widened. Closing change: set a non-zero redeemFeeBps, or split
    the USDT cap raise out and leave it at 3000 bps until a funded vault exists.

New exposure introduced by deploy-first

ResourceNotContract fires on exactly one condition — no code at the address. Deploy-first satisfies
that condition deliberately, before the vote. After deployment the guard can never fire again, and
the only remaining on-chain property is AdapterFRV.asset(vault) == USDT. Verified: addResource
(YieldGroupBase.sol:111-135) checks non-zero args, not-already-registered, code.length != 0,
adapter code, asset match, then AdapterFRV.validateRegistration — which is an external pure no-op
(AdapterFRV.sol:183-187). There is no lifecycle-state check anywhere on the registration path.

That makes a deployed-but-wrong vault at the pinned address strictly more likely than under the
undeployed framing, and gives three concrete new paths, all live for ≥3 days:

  1. cancelVault(CEFFU_VAULT) — CriticalGuardian, zero delay. Transitions to Failed
    (InstitutionalLoanVault.sol:185-199); the clone keeps its code and asset() == USDT. The VIP then
    registers a dead vault, and the replacement Ceffu vault lands at nonce 1 — a different address
    needing another VIP.
  2. openVault(CEFFU_VAULT) — same Safe. Verified maxDeposit (BaseVault.sol:378-383) returns 0
    unless Fundraising, so the description's "not exploitable while a vault is pre-Fundraising" is a
    claim about vault state at execution time that Option A no longer guarantees.
  3. Fundraising expiry — a self-inflicted deadlock. If openVault is called during the window,
    _checkAndAdvanceState (InstitutionalLoanVault.sol:551-563) sends the vault to Failed once
    block.timestamp >= openEndTime with totalRaised < minBorrowCap, and updateVaultState() is
    permissionless. The Hub is the intended supplier but cannot deposit until this VIP executes. So
    any openDuration shorter than the ~3-day proposal→execution window guarantees the vault fails
    before the wiring that was supposed to fund it goes live.

Findings

  • vips/vip-665/bscmainnet.ts:67,194 — the VIP registers a vault whose economic terms are set outside governance and never checked. Under Option A the config is fixed at deployment but nothing in the PR reads it: addResource enforces only code-present + asset() == USDT + a pure no-op validateRegistration, and _validateVaultConfig places no bound between idealCollateralAmount and maxBorrowCap, so a 5,000,000-USDT-cap vault collateralised at 1 wei passes. createVault is held by NormalTimelock, FastTrackTimelock (6h) and the zero-delay CriticalGuardian 3-of-6 Safe. HIGH, confidence high. — Pin the full approved config in meta.description and add a pre-queue script that diffs the deployed vault's config()/institutionalConfig()/riskConfig() against those literals; abort the proposal on any mismatch.

  • vips/vip-665/bscmainnet.ts:194sweep/setTreasury extend the round-1 extraction primitive to the Hub holding 2,700,595 USDT, with FRV headroom raised to 1,350,297 by this same VIP. BaseVault.sweep:162-170 moves the entire balance of any token to controller.treasury() with no supply-asset exclusion, and AdapterFRV._breakdown:227 marks a Lock position from rt.totalRaised rather than the vault's balance, so the Hub's NAV does not move when the vault is drained. Both roles verified still held by the zero-delay CriticalGuardian Safe (sweep also by FastTrack). HIGH, confidence high on mechanism, medium on exploitability (needs the Operator to have reallocated in first). — Narrow both roles to the Normal Timelock and add the supply-asset exclusion before any reallocate.

  • vips/vip-665/bscmainnet.ts:194closeVault(address) is a second, unreported extraction-equivalent primitive on the same authority set, missed by rounds 1 and 2. Verified: BaseVault.closeVault:118-123 accepts any terminal state (Matured/Failed/Liquidated) with no check that suppliers have withdrawn (the docstring says governance "should only call this once all suppliers have withdrawn" — there is no such guard); maxWithdraw returns 0 for Closed (BaseVault.sol:401-409); and AdapterFRV._breakdown:213 deliberately values a Closed position at 0. So between a vault maturing and the Operator pulling funds back out, one zero-delay 3-of-6 Safe call permanently strands the Hub's entire FRV position and writes it to zero. HIGH, confidence high on mechanism, medium on exploitability. — Add closeVault (and cancelVault, partialPauseVault, completePauseVault) to the set narrowed to the Normal Timelock; round 2's escalation list was incomplete.

  • vips/vip-665/bscmainnet.ts:146-149 — the on-chain, voter-facing guardrail bullet says "Not exploitable while a vault is pre-Fundraising (maxDeposit == 0), so this VIP ships safely." Option A falsifies this and the delta did not update it: the vault now exists for the ≥3-day (up to 17-day) proposal→execution window, and one openVault call by the same zero-delay CriticalGuardian Safe puts it in Fundraising with maxDeposit == maxBorrowCap, so the VIP can execute onto a vault with live capacity. Voters are given a safety property the sequencing change removed. MEDIUM, confidence high. — Rewrite the bullet to state that the vault is live throughout voting and the timelock delay and that its lifecycle state is Safe-controlled, and add a pre-queue assertion that runtime().state ∈ {WaitingForMargin, MarginDeposited}.

  • vips/vip-665/bscmainnet.ts:194 — deploy-first disarms the only on-chain control the Ceffu leg has. ResourceNotContract fires solely on an empty address; satisfying it before the vote means it can never fire, and addResource performs no lifecycle check, so a cancelVault'd (Failed) vault — or one that fell to Failed because its openDuration expired during the ~3-day window while the Hub, its intended supplier, was still blocked on this very VIP — registers successfully and is permanently wired into the USDT Hub plus its inner withdraw queue. Removal needs another VIP. cancelVault is zero-delay CriticalGuardian; the expiry path needs no privilege at all, since updateVaultState() is permissionless. MEDIUM, confidence high. — Add a pre-queue check on runtime().state, and sequence openVault to happen after this VIP executes, not before; state that ordering constraint in the description.

  • simulations/vip-665/bscmainnet.ts:132-137,143-144 — the only automated check on the Ceffu address is structurally incapable of observing the deployment the whole safety argument now rests on. forking(FORK_BLOCK) pins state at 116,780,000, so predictVaultAddress and getCode(CEFFU_VAULT) == "0x" return the same values forever. The delta demotes the assertion to "only a sanity check" in a comment but leaves it in place and adds no replacement, so after the vault is deployed CI stays green while asserting the vault's absence — and institutionNonce will then be 1, making the predictVaultAddress assertion false against any live block. MEDIUM, confidence high. — Replace both with a pre-queue live-provider script (getCode != 0x, isRegistered, AdapterFRV.asset == USDT, runtime().state, config diff); a pinned fork cannot express this check.

  • vips/vip-665/bscmainnet.ts:204 — the 30%→50% USDT raise is widened further by Option A: it now lifts headroom 810,178 → 1,350,297 USDT on a Hub that gains its first FRV destination in the same transaction, and that destination may hold live capacity at execution rather than being certainly pre-Fundraising. redeemFeeBps == 0 re-verified on both Hubs, so the secondary brake is still off while the primary containment is widened 67%. The mitigation offered is an off-chain commitment that the Operator will hold off. MEDIUM, confidence high. — Set a non-zero redeemFeeBps in this VIP, or hold the USDT FRV percentage at 3000 bps and raise it in the VIP that authorises the first reallocation.

  • simulations/vip-665/bscmainnet.ts:268-275 — the Ceffu behavioural proof still runs against MOCK_FRV_VAULT_BYTECODE etched at the address, and Option A makes this worse rather than better: the stub now stands in for a contract that will genuinely exist at proposal time, so the sim substitutes a fiction for a state it could test for real. It proves only that the Hub rejects a zero-capacity resource; none of the real InstitutionalLoanVault lifecycle, valuation or settlement surface is exercised. MEDIUM, confidence high (raised from round-2 LOW). — Bump FORK_BLOCK past the deployment and skip the etch when getCode(CEFFU_VAULT) != "0x", so CI exercises the vault the VIP will actually register.

  • vips/vip-665/bscmainnet.ts:146-149 — the separation-of-duties argument that bounds the sweep finding is weaker than round 2 recorded. Round 2 noted that reallocate is Operator-only and CriticalGuardian does not hold it. Verified live, the Operator 0x83f426…6a7A is itself a 3-of-6 Safe sharing two of its six owners (0x358f758D…DFF2, 0x209D6B1E…319f) with the CriticalGuardian Safe — so funding the vault and draining it requires four distinct signers, not six, and the two Safes are not independent principals. LOW, confidence high (defence-in-depth; it does not create the exposure, it shrinks the margin). — Record the actual overlap wherever the "Operator-only" separation is relied on, and prefer disjoint signer sets for the fund-in and fund-out roles.

  • vips/vip-665/bscmainnet.ts:93,161 — under Option A the permanent on-chain description will be factually false at vote time: "The Ceffu vault is not yet deployed" and "Ceffu vault (USDT, predicted, pending deployment)". The vault will be deployed before the proposal is submitted — that is the whole commitment. Telling voters the target is unbuilt actively suppresses the one check they should perform, which is to inspect the deployed vault's config. LOW, confidence high. — Rewrite both to state the vault is deployed at that address, and cite the deployment tx plus the pinned config so voters approve terms rather than an address.

Checked and clean this round
  • No arbitrary code can still land at CEFFU_VAULT. Re-verified: hitting the address requires the
    clone implementation to be exactly 0xe87A1eFC…5bdC; that contract's EIP-1967 implementation and
    admin slots are both empty (40,061 bytes), so it is not upgradeable, and setVaultImplementation
    (NormalTimelock + FastTrack only) would move the prediction rather than change the code at the
    pinned address. Round-2's conclusion holds under Option A.
  • CEFFU_INSTITUTION 0x8972E6F8…6262 still has no code, institutionNonce == 0, no vault ever
    created. It cannot create or open the vault itself.
  • No wildcard grants. keccak256(abi.encodePacked(address(0), sig)) holds no member among
    NormalTimelock / FastTrack / Critical / Guardian / CriticalGuardian for any of the eight controller
    functions checked.
  • The payload is byte-identical to round 2 — same six commands, same targets, same params. The
    Normal Timelock still holds all six (contract, signature) pairs.
  • All four address literals round-trip EIP-55 cleanly (CEFFU_VAULT, CASH_PLUS_VAULT,
    CONTROLLER, CEFFU_INSTITUTION) — re-confirmed by the lead.
  • AdapterFRV.assetIFRVVault(resource).asset() confirmed, so a deployed vault whose
    supplyAsset != USDT reverts ResourceAssetMismatch and the VIP fails closed on that axis.
  • No secrets, keys, URLs or injection surfaces introduced by the delta.

Consolidated asks

Correctness and security converged independently on the same two work items. Neither is optional under
Option A — the sequencing commitment is what makes the current simulation wrong.

A. Rebuild the simulation for the post-deployment state. As shipped it asserts the vault's
absence and etches a stub, which is the exact state Option A commits the proposal not to be in.

  1. Bump FORK_BLOCK (:54) past the Ceffu createVault tx, in the same "prepare VIP-665 for
    proposal" chore commit the repo already uses for this (cf. cfb0113 for VIP-656). forking()
    takes a literal block number (src/vip-framework/index.ts:71).
  2. Delete the prediction plumbing — the guard (:132-137), CONTROLLER_ABI (:30), the controller
    handle (:91). Replace with isRegistered(CEFFU_VAULT) == true (monotonic) and
    institutionalConfig().institutionOperator == CEFFU_INSTITUTION — that is what the prediction was
    actually trying to establish.
  3. Delete the etch (:139-147, MOCK_FRV_VAULT_BYTECODE :32-46). Assert getCode != "0x" and
    AdapterFRV.asset(CEFFU_VAULT) == usdtStack.asset — the exact call addResource makes
    (YieldGroupBase.sol:121), now against real vault code.
  4. Pin the approved terms: vault.config(), vault.riskConfig(), vault.state(). Structurally
    impossible against the stub, and this is what converts "voters approved an address" into "voters
    approved these terms".
  5. Rewrite the two behavioural tests (:259-275) to branch on the real state() instead of
    hard-coding ResourceCapacityExceeded(PUSH, 0).
  6. Extend the outer-queue guard (:243-249) and the effective-cap equality assertion (:232-241) to
    the USDT leg — both are currently U-only, and USDT is the Hub holding the funds.

B. Add a pre-proposal gate, and make it an artifact rather than a promise. A pinned fork cannot
express any of these; they need a live-provider script run immediately before queueing, with the
proposal aborted on any mismatch:

  • eth_getCode(CEFFU_VAULT) != 0x and controller.isRegistered(CEFFU_VAULT) == true
  • AdapterFRV.asset(CEFFU_VAULT) == USDT and institutionalConfig().institutionOperator == CEFFU_INSTITUTION
  • runtime().state ∈ {WaitingForMargin, MarginDeposited}
  • full config() / riskConfig() diff against literals pinned in meta.description
  • an explicit written commitment that openVault is not called until after this VIP executes
  • do not re-assert predictVaultAddress — post-deployment it is guaranteed to return the wrong address

C. Rewrite the meta.description for the world it will ship into. Delete the CREATE2/prediction
paragraph (:93-102); keep the ResourceNotContract fail-closed sentence, which is correct and is
the one genuinely useful thing the delta added. Fix the pre-deployment tense at :93, :119, :161
and the banner at :25-29. Correct the pre-Fundraising guardrail bullet at :146-149. Add the
approved vault terms.

Escalation

The round-2 recommendation stands, and Option A strengthens the case for it rather than satisfying
it.
Sequencing is an off-chain process control; it changes when the vault exists, not who can
act on it. The authority set is re-verified byte-for-byte unchanged, and this round adds three
primitives round 2 did not list — cancelVault, closeVault and the timing reachability of
openVault — all held by the same zero-delay 3-of-6 Safe, all now reachable during the governance
window precisely because the vault is deployed early. A full multi-researcher audit of the
InstitutionalVaultController trust boundary (vipsvenus-liquidity-hubfixed-rate-vaults)
should happen before the Ceffu leg ships, not before the first reallocation.

Note: the PR body above is still the stale 5-command / Ceffu-deferred / 19-of-19 text and the bot
cannot edit it; the on-chain meta.description is the artifact this review assesses.

…security note

Round-3 review fixes (all compatible with Option A — single 6-command VIP,
proposed only after the Ceffu vault is deployed):

- Sim: reframe the predictVaultAddress assertion as a build-time nonce-0
  preimage check, not a voter-reproducible safeguard. Verified on-chain that
  createVault increments institutionNonce, so post-deployment the predictor
  returns the institution's next clone, not CEFFU_VAULT (both already-deployed
  institutions sit at nonce 1 and predict a different, codeless address than
  their live vault). Assert nonce == 0 and document the caveat; make the etch
  test honest that the real vault cannot be exercised in a fork and is verified
  off-fork by the gate.
- VIP description: rewrite the Ceffu paragraph for the post-deployment ship
  state (no predictVaultAddress-as-voter-check; verify the deployed vault's
  code/asset/registration instead), and extend the security disclosure to the
  CriticalGuardian 3-of-6 Safe's zero-delay openVault/closeVault/cancelVault
  authority alongside sweep/setTreasury (verified on-chain).
- scripts/checkCeffuVaultReady.ts: deploy-first gate that fails unless the Ceffu
  vault holds code, is registered on the controller for the Ceffu institution,
  and reports asset() == USDT. Run before proposing.

Sim 27/27 green @116780000; eslint + prettier clean; gate script exits 1 today
(vault undeployed) as expected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread vips/vip-665/bscmainnet.ts Outdated
signature: "addResource(address,address)",
params: [CASH_PLUS_VAULT, ADAPTER_FRV],
},
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove setInnerWithdrawQueue, we prefer doing deposit/withdraw manually

Comment thread vips/vip-665/bscmainnet.ts Outdated
params: [CEFFU_VAULT, ADAPTER_FRV],
},
{
target: USDT_FRV_SOURCE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this cmd, same reason as above

trumpgpt-bot and others added 2 commits August 19, 2026 09:59
…gement)

Fred's review: remove setInnerWithdrawQueue on both FRV sources — same rationale
as the earlier setInnerDepositQueue removal (team prefers to wire queues manually).
VIP now has 4 commands (addResource + raiseYieldGroupCap per hub). Sim updated:
ACM role check trimmed, expectEvents drops InnerWithdrawQueueSet, post-VIP inner
queue tests now assert both queues remain empty. 25/25 green; eslint+prettier clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renumber vip-665 -> vip-657 (on-chain proposalCount 656) and correct the
description to state what is actually true at proposal time: the Ceffu USDT
vault at 0x086fd797...0103 is not yet deployed, so the deploy-first gate has
to pass before execution rather than before proposal. The VIP fails closed
atomically if it is still missing then.

Also drop inline backticks from meta.description (the venus.io governance UI
strips their contents) and fix the "six commands" count to four.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fred-venus fred-venus changed the title VDB-53: VIP-665: wire Cash+ FRV vault into U Liquidity Hub, raise FRV caps to 50% VDB-53: VIP-657: wire Cash+ and Ceffu FRV vaults into the Liquidity Hub, raise FRV caps to 50% Aug 19, 2026
@fred-venus

Copy link
Copy Markdown
Contributor

@fred-venus
fred-venus merged commit 92cfcb8 into main Aug 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants