Skip to content

[VPD-1862]: VIP to Configure Centrifuge YieldGroup to Liquidity Hub - #761

Merged
fred-venus merged 18 commits into
mainfrom
vip/vpd-1862-centrifuge-adapter
Sep 17, 2026
Merged

fred-venus merged 18 commits into
mainfrom
vip/vpd-1862-centrifuge-adapter

Conversation

@GitGuru7

@GitGuru7 GitGuru7 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

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.

  • JTRSY: Vault: 0x6e6B8498415083a4386BE83DD59Edd4366402FFa; Share: 0xa5d465251fBCc907f5Dd6bB2145488DFC6a2627b.
  • JAAA: Vault: 0xcbAfe61d84C6Fb88252a6Adf1C9CB0B9D029cb99; Share: 0x58F93d6b1EF2F44eC379Cb975657C132CBeD3B6b.

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:

  • Normal Timelock: Signatures: 20; Surface: everything, including sweep which it alone holds.
  • Operator: Signatures: 16; Surface: both inner queues, the async lifecycle, the four claims, pauseResource, unpauseResource, updateResourceAdapter, the NAV band, setSpotAPYBps.
  • Keeper: Signatures: 4; Surface: the four claim functions, nothing else.
  • Guardian: Signatures: 8; Surface: pauseResource, unpauseResource, updateResourceAdapter, forceRemoveResource, the NAV band, setSpotAPYBps.

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.

  • JTRSY: Drift: 5.00%; Band up: 2%; Band down: 5%; Re-anchor: daily; Published APY: 3.37%.
  • JAAA: Drift: 5.50%; Band up: 2%; Band down: 5%; Re-anchor: daily; Published APY: 5.29%.

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)

  1. Grant DEFAULT_ADMIN_ROLE to the ACMCommandsAggregator, replay the grant batch, and revoke the
    role.
  2. Register both funds on the source behind AdapterCentrifuge (addResource).
  3. Set the source's inner withdraw queue to both funds, JTRSY first. The inner deposit queue is left
    unset, so an ordinary Hub deposit never routes into a fund that settles over days.
  4. Configure the NAV band on each fund, both sides armed.
  5. Publish the starting APY for each fund.
  6. Register the group on the Hub with an absolute cap of 5,000,000 USDT and a 25% cap on TVL.
  7. Append Centrifuge to the end of the Hub's withdraw cascade, leaving the existing order and the
    deposit queue untouched.

Deployed contracts (BNB Chain)

  • AdapterCentrifuge: 0x680cE4422264ecDAd3590cB50FE254D4c153f427
  • YieldGroupCentrifuge (implementation): 0x4996aa488f5269B5C544Dab1C2d1126F6bBdFF24
  • CentrifugeBeacon: 0xAe90Cfb3E2Bc97508F58E7e076Acf38f3bfC820f — owned by the Normal Timelock
  • CentrifugeSource_USDT: 0xDA5AFfeb43719f517676E031a727071c7D400983 — a BeaconProxy with no owner of its own, so
    every gated call on it is ACM-controlled

References

  • The BNB Chain Testnet proposal that onboarded the Centrifuge YieldGroup
  • HashDit audit of the Centrifuge YieldGroup

Validation

  • Mainnet simulation: 60 passing at BSC block 122188386.
  • Prettier and ESLint passed.
  • Mainnet files use vip-661; testnet files remain unchanged under vip-999.

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.
@GitGuru7
GitGuru7 marked this pull request as ready for review September 15, 2026 13:44
@greptile-apps

greptile-apps Bot commented Sep 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds BNB Chain and BNB Testnet VIPs and simulations for onboarding the asynchronous Centrifuge YieldGroup into the USDT Liquidity Hub.

  • Registers the JTRSY and JAAA resources and configures their NAV guards on mainnet.
  • Replaces the pre-audit Testnet deployment with a fresh beacon, implementation, adapter, and source.
  • Introduces role matrices and an ACM aggregator batch for governance, Operator, Keeper, and Guardian permissions.
  • Adds fork simulations covering registration, asynchronous requests, guards, queues, and access control.
  • Mainnet readiness is currently blocked by the zero-anchor guard configuration, absent share-class memberships, and unresolved keeper identity.

Confidence Score: 2/5

The 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

Important Files Changed

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

Comment thread vips/vip-999/bscmainnet.ts Outdated
Comment on lines +67 to +68
{ resource: JAAA_VAULT, driftBps: 450, upGapBps: 500, downGapBps: 500 },
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Guard starts without anchor

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.

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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread simulations/vip-661/bscmainnet.ts
Comment thread vips/vip-999/bscmainnet.ts Outdated
Comment thread vips/vip-999/bscmainnet.ts Outdated
// 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

@zed-venus zed-venus Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
export const CENTRIFUGE_PERCENTAGE_CAP_BPS = 2_000; // 20% of TVL
export const CENTRIFUGE_PERCENTAGE_CAP_BPS = 2_500; // 25% of TVL

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment thread vips/vip-999/bscmainnet.ts Outdated
// 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 },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
{ resource: JTRSY_VAULT, driftBps: 350, upGapBps: 500, downGapBps: 500 },
{ resource: JTRSY_VAULT, driftBps: 500, upGapBps: 200, downGapBps: 500 },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment thread vips/vip-999/bscmainnet.ts Outdated
// 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 },

@zed-venus zed-venus Sep 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
{ resource: JAAA_VAULT, driftBps: 450, upGapBps: 500, downGapBps: 500 },
{ resource: JAAA_VAULT, driftBps: 550, upGapBps: 200, downGapBps: 500 },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"setInnerDepositQueue(address[])",
"setInnerWithdrawQueue(address[])",
"pauseResource(address)",
"unpauseResource(address)",

@fred-venus fred-venus Sep 16, 2026

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.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

export const CENTRIFUGE_NAV_GUARD = [
"setNavGuardRate(address,uint16,uint16,uint16,uint32,bool,bool)",
"setNavGuardSnapshot(address,uint128,uint64)",
"setNavGuardEnabled(address,bool,bool)",

@fred-venus fred-venus Sep 16, 2026

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.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment thread vips/vip-661/bscmainnet.ts
Comment thread vips/vip-661/bscmainnet.ts
Debugger022
Debugger022 previously approved these changes Sep 16, 2026

@Debugger022 Debugger022 left a comment

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.

LGTM

@fred-venus

Copy link
Copy Markdown
Contributor

@fred-venus
fred-venus merged commit 3094ff9 into main Sep 17, 2026
2 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.

4 participants