[VPD-1862]: VIP to Configure Centrifuge YieldGroup to Liquidity Hub - #761
Conversation
Grants the ACM roles on CentrifugeSource_USDT, registers a Centrifuge
ERC-7540 fund behind AdapterCentrifuge, and adds the group to Hub_USDT.
Centrifuge is the first asynchronous yield source on the Hub: requests are
escrowed and settled later by the fund manager at a published NAV, so the
group adds request, cancel and claim operations the synchronous
IYieldGroupBase surface cannot express.
The registered fund is a testnet mock controlled by Venus. Centrifuge has no
BSC-testnet deployment, so there is no real ERC-7540 fund on chain 97.
Centrifuge joins the WITHDRAW queue only. Settlement takes days, so routing
an ordinary user deposit there would park that user's capital in a pending
request, unwithdrawable, for a product they never chose; capital enters only
through an Operator reallocation targeted at the vault. It must still appear
in the withdraw queue, because the Hub rejects a queue omitting a registered
group that holds a balance.
The source is granted pauseHub() on the Hub — a grant to a contract, not to a
human role. The drop guard's only reaction is the group calling pauseHub()
with no try/catch, so without it a genuine breach makes the permissionless
enforceDropGuard revert rather than pause: the breaker would be dead, not
degraded.
No price guard is armed. Each needs a sizing decision against observed NAV
behaviour and an over-tight value is a self-inflicted halt; the roles are
granted so arming them later needs no further VIP.
Two constraints found by simulating rather than reasoning:
- granting all three timelocks pushed the proposal to 30,151,896 gas, past
the 30M block limit. Narrowed to the Normal Timelock, matching VIP-650's
testnet proposal; now 9.55M to propose, 3.63M to execute.
- testnet collapses Operator, Keeper and Guardian into one multisig, so
pauseResource(address) was granted twice and produced byte-identical
proposal actions, which GovernorBravo rejects with "identical proposal
action already queued at eta". Grants are now deduplicated by
(account, signature); on mainnet the holders differ and nothing is removed.
Simulation: 58 passing against a bsctestnet fork at block 128,298,000.
…missions the guardian lacks
Greptile SummaryThis PR adds BNB Chain and BNB Testnet VIPs and simulations for onboarding the asynchronous Centrifuge YieldGroup into the USDT Liquidity Hub.
Confidence Score: 2/5The PR is not safe to execute as written because the initial NAV guard can overstate losses, successful settlement depends on missing external memberships, and routine claim permissions target the Critical Guardian instead of the keeper. Three concrete mainnet failures remain: empty resources receive an enabled zero-width NAV floor, the end-to-end flow relies on memberlist state absent from production, and the ACM batch grants claims to the unresolved placeholder address. Files Needing Attention: vips/vip-999/bscmainnet.ts, simulations/vip-999/bscmainnet.ts, vips/vip-999/scripts/acmPermissions.ts
|
| Filename | Overview |
|---|---|
| vips/vip-999/bscmainnet.ts | Defines the mainnet onboarding proposal; its enabled guards start without valid anchors, and its keeper constant identifies the Critical Guardian. |
| simulations/vip-999/bscmainnet.ts | Provides extensive fork coverage but both demonstrates the zero-anchor overvaluation and injects the missing external memberlist prerequisite before its successful flow. |
| vips/vip-999/permissions-bscmainnet.ts | Defines the mainnet role surfaces, including four claim-only keeper permissions and deliberately broad Guardian NAV controls. |
| vips/vip-999/scripts/acmPermissions.ts | Builds the preloaded ACM batch from the configured identities, propagating the incorrect keeper address. |
| vips/vip-999/bsctestnet-addendum.ts | Atomically replaces and deregisters the retired Testnet source while preserving the intended Hub routing. |
| simulations/vip-999/bsctestnet-addendum.ts | Exercises the Testnet replacement, storage isolation, permissions, queues, and asynchronous lifecycle. |
Reviews (1): Last reviewed commit: "feat: keep centrifuge sweep with the nor..." | Re-trigger Greptile
| { resource: JAAA_VAULT, driftBps: 450, upGapBps: 500, downGapBps: 500 }, | ||
| ]; |
There was a problem hiding this comment.
Both NAV-guard sides are enabled while each resource is empty, so the guard begins with a zero anchor and a zero-width band. After the first allocation, a genuine loss is clamped back to cost basis until a later accrual re-anchors the guard. The simulation shows that even when the observed value falls by 50%, source.totalAssets() remains at or above the original tranche, causing the Hub to overstate NAV during this interval. Initialize the snapshot from a funded position before enabling the floor, or leave the guard disabled until a valid anchor exists.
There was a problem hiding this comment.
i think this makes sense but its ok because the fund wont automatically flow into this group. We could reallocate + setNavGuardSnapshot when first time deposit to alleviate the issue
There was a problem hiding this comment.
That mitigation is reasonable: because the inner queues are unset, the zero-anchor state is inert while the source is unfunded. The important invariant is that each first reallocate must be followed immediately by setNavGuardSnapshot for that resource, using its post-allocation value, before Hub valuation or flows rely on the guard. Treat those calls as one runbook step (or execute them atomically if the operator path permits); otherwise the interval between them can still expose the zero-anchor behavior. I’m fine treating this as an operational requirement rather than a proposal blocker, but it should be documented and covered by the first-funding simulation.
| // Caps for Hub.addYieldGroup(source, absoluteCap, percentageCapBps). | ||
| // --------------------------------------------------------------------------------------------------- | ||
| export const CENTRIFUGE_ABSOLUTE_CAP = parseUnits("5000000", 18).toString(); | ||
| export const CENTRIFUGE_PERCENTAGE_CAP_BPS = 2_000; // 20% of TVL |
There was a problem hiding this comment.
| export const CENTRIFUGE_PERCENTAGE_CAP_BPS = 2_000; // 20% of TVL | |
| export const CENTRIFUGE_PERCENTAGE_CAP_BPS = 2_500; // 25% of TVL |
| // Drift is each fund's own realized rate since launch (JTRSY 3.40%, JAAA 4.31% a year). The 5% band | ||
| // is a backstop against an absurd reading, not a tracking budget: neither fund has strayed past 0.4%. | ||
| export const NAV_GUARDS = [ | ||
| { resource: JTRSY_VAULT, driftBps: 350, upGapBps: 500, downGapBps: 500 }, |
There was a problem hiding this comment.
| { resource: JTRSY_VAULT, driftBps: 350, upGapBps: 500, downGapBps: 500 }, | |
| { resource: JTRSY_VAULT, driftBps: 500, upGapBps: 200, downGapBps: 500 }, |
| // is a backstop against an absurd reading, not a tracking budget: neither fund has strayed past 0.4%. | ||
| export const NAV_GUARDS = [ | ||
| { resource: JTRSY_VAULT, driftBps: 350, upGapBps: 500, downGapBps: 500 }, | ||
| { resource: JAAA_VAULT, driftBps: 450, upGapBps: 500, downGapBps: 500 }, |
There was a problem hiding this comment.
| { resource: JAAA_VAULT, driftBps: 450, upGapBps: 500, downGapBps: 500 }, | |
| { resource: JAAA_VAULT, driftBps: 550, upGapBps: 200, downGapBps: 500 }, |
| "setInnerDepositQueue(address[])", | ||
| "setInnerWithdrawQueue(address[])", | ||
| "pauseResource(address)", | ||
| "unpauseResource(address)", |
There was a problem hiding this comment.
i noticed for updateResourceAdapter and unpauseResource, only NT is granted the permission ( this is also the case for existing 3 yieldGroup contract), i prefer we do NT, Guardian and Operator for all 4 group contract so that we still have the chance to perform quick fix if anything goes wrong
| export const CENTRIFUGE_NAV_GUARD = [ | ||
| "setNavGuardRate(address,uint16,uint16,uint16,uint32,bool,bool)", | ||
| "setNavGuardSnapshot(address,uint128,uint64)", | ||
| "setNavGuardEnabled(address,bool,bool)", |
There was a problem hiding this comment.
for those 3 pls include Operator addr as well, for example it could be helpful in https://github.com/VenusProtocol/vips/pull/761/changes#r4022586005
VIP-661 [BNB Chain] Liquidity Hub (USDT) — onboard the Centrifuge YieldGroup
Summary
Onboards the Centrifuge YieldGroup to the Liquidity Hub (USDT) on BNB Chain: grants the ACM roles
on the newly deployed source, registers Centrifuge's two live BNB Chain funds — JTRSY and JAAA —
behind AdapterCentrifuge, sets the source's inner withdraw queue, configures a NAV band and
publishes a starting APY on each fund, and adds the group to the Hub.
Centrifuge is the first asynchronous yield source on the Hub: deposits and redemptions are escrowed
and settled later by the fund manager at a published NAV.
No capital moves in this proposal. Before capital can be allocated, Centrifuge must add the source to both share-class memberlists; this proposal does not grant that membership.
The two funds
Live ERC-7540 vaults on BNB Chain denominated in USDT, sharing one Centrifuge AsyncRequestManager.
Roles
84 grants, already seeded on chain into the ACMCommandsAggregator
(0x8b443Ea6726E56DF4C4F62f80F0556bB9B2a7c64) at grant batch index 5, because they do not fit inline in
a single propose() transaction. Seeded by
0xf4a2c8411e83488ea28a6fab507d1d8c0ee82f84b0498d19411203fe3a128111 in block 122188221, and readable
with grantPermissions(5, i) for i in 0..83.
The proposal lends the aggregator DEFAULT_ADMIN_ROLE on the AccessControlManager, replays that
batch, and revokes the role in the same transaction, so the aggregator holds ACM admin only inside
this proposal.
On the new Centrifuge source:
The Fast-Track and Critical timelocks are granted nothing, matching the rest of the Liquidity Hub.
The batch also grants updateResourceAdapter and unpauseResource to the Operator and the Guardian
on the nine sources already live under the USDT, USDC and U hubs, where both are Normal-Timelock-only
today. Both accounts already hold pauseResource on those sources.
NAV band
Each fund gets a band around the value it reports, held to an anchor that drifts at a published rate
and re-anchors daily. A reading outside the band is reported at the edge of it; it never reverts.
Centrifuge publishes no rate on chain, so the APY each fund reports is set by setSpotAPYBps. Left
unset the group would report zero and drag the Hub's advertised APY down.
Actions (12 commands, executed atomically in order)
role.
unset, so an ordinary Hub deposit never routes into a fund that settles over days.
deposit queue untouched.
Deployed contracts (BNB Chain)
every gated call on it is ACM-controlled
References
Validation