Arkonix Extensions¶
Everything on this page is Arkonix-owned. These contracts are the fork's entire delta against upstream Centrifuge V3: they plug into standard extension points — the Hub's feeHook, hub managers, balance-sheet managers, and ContractUpdater targets — without modifying any core contract (see the fork framing).
| Contract | Side | Status |
|---|---|---|
ArkonixFeeManager |
Hub | [SHIPPED] |
ArkonixMerkleProofManager (+ factory) |
Spoke | [SHIPPED] |
ArkonixProofManager (+ factory) |
Spoke | [SHIPPED] |
ArkonixScProofManager (+ factory) |
Spoke | [SHIPPED] |
| Index-strategy architecture | Hub + Spoke | [IN FLIGHT] |
JRRTVaultManager |
Hub | [IN FLIGHT] |
ArkonixFeeManager — [SHIPPED]¶
ArkonixFeeManager (BUSL-1.1) wraps the Hub's share-price update path to accrue fees transparently on-chain. Pool managers push the gross share price through it; the contract deducts fees, records the accruals, and forwards the net price to the Hub (with msg.value passed through for the cross-chain notify).
Fee model (per pool and share class):
- Management fee: an annual basis-point rate, time-prorated —
nav × rate/10000 × Δt/365d. - Performance fee: a basis-point rate on gains above a high-water mark that ratchets up as new highs are set.
- The forwarded net price is floored at 1.0 (D18); rounding favors the protocol.
- Enabling a previously-zero fee leg resets its anchor, so there is no retroactive charging; pending accruals settle before rate changes take effect.
- A pool auto-initializes on its first price update through the manager.
Access control:
- Pool functions require the caller to be the pool's hub manager (
hubRegistry.manager(poolId, msg.sender)). - Contract administration uses a two-step
admin/pendingAdmintransfer (file("admin", ...)followed byacceptAdmin()from the new admin).
Not ward-gated
Unlike the rest of the protocol, ArkonixFeeManager does not use the ward pattern for its admin. Its admin role is a deliberate two-step admin/pendingAdmin scheme.
API surface: updateSharePrice(poolId, scId, price, computedAt) (payable), initializePool, setFeeConfig(poolId, scId, mgmtBps, perfBps) (each capped at 10000 bps), resetAccrued; views accrued, feeConfig, feeState (last accrual timestamp, high-water mark, accrued management and performance fees), isInitialized.
Events: InitializePool, SetFeeConfig, FeesAccrued, ResetAccrued, File, StartAdminTransfer, AcceptAdminTransfer. Errors include NotAuthorized, AlreadyInitialized, NotInitialized, InvalidFeeRate, NothingToReset, ZeroAdmin, NotPendingAdmin, FileUnrecognizedParam.
The contract has a dedicated unit-test suite.
The proof-manager family — [SHIPPED]¶
The proof managers are the protocol's policy-gated execution layer: permissioned strategist/keeper execution over pool funds, with every policy decision made hub-side under timelocked governance. They are the on-chain half of the platform's execution stack — the off-chain services that build and submit execution batches are documented in Backend execution.
Shared governance channel¶
All four proof managers receive policy exclusively through one path:
Hub.updateContract(poolId, scId, target, payload) [hub manager, timelock-backed]
→ cross-chain message → MessageProcessor
→ ContractUpdater.trustedCall(poolId, scId, target, payload)
→ manager.trustedCall(...) [validates caller == ContractUpdater
+ pool/share-class scope]
All policy — merkle roots, authorized signers, target allow-lists — is set exclusively hub-side. No spoke-local path can change what a proof manager is allowed to execute.
Each manager is registered as a BalanceSheet manager for its pool, which is what authorizes the deposit/withdraw calls it executes, and each has a receive() function for value-bearing calls.
Comparison¶
| Contract | Authorization | Scope | Share class enforced in trustedCall |
Factory existence check | Duplicate guard |
|---|---|---|---|---|---|
MerkleProofManager (upstream) |
Merkle proof per strategist | Pool | No | Pool is active | None |
ArkonixMerkleProofManager |
Merkle proof per strategist | Pool + share class | Yes | Share token exists for (pool, scId) |
None |
ArkonixProofManager |
EIP-712 signature (KMS signer) + target allow-list | Pool | No | Pool is active | One per pool |
ArkonixScProofManager |
EIP-712 signature (KMS signer) + target allow-list | Pool + share class | Yes | Share token exists for (pool, scId) |
One per (pool, scId) |
The conceptual split: merkle managers pin who calls — the strategist is msg.sender, gated by that strategist's own merkle root — while KMS managers pin who signed — msg.sender is irrelevant, and any relayer may submit a batch signed by the authorized signer.
MerkleProofManager (upstream)¶
Merkle-gated execution, with a design inspired by Se7en-Seas Boring Vaults. Each strategist address maps to a policy merkle root.
- Policy: the trusted-call payload sets
policy[strategist] = newRootand emitsUpdatePolicy. - Execution:
execute(Call[] calls), where eachCallcarries{decoder, target, targetData, value, proof[]}. The caller must have a non-zero policy root. Per call, the manager staticcalls the decoder to extract the constrained address arguments, builds aPolicyLeaf {decoder, target, valueNonZero, selector, addresses}, verifies its hash against the strategist's root, then performs the call. Failures bubble up as ERC-7751 wrapped errors. - Deployed per pool by
MerkleProofManagerFactory.
ArkonixMerkleProofManager — per-share-class merkle variant¶
Identical execution logic to upstream, with an immutable share-class ID that is enforced in trustedCall — a policy update addressed to the wrong share class is rejected. Its factory validates that the share class exists before deploying, and deploys deterministically per (pool, shareClass).
Rationale: upstream's manager is pool-scoped, but Arkonix runs index strategies as share classes — potentially several in one pool. Per-share-class scoping confines a strategist's policy, and the funds it can reach, to one index vault.
Test coverage
ArkonixMerkleProofManager does not yet have a dedicated test file; its coverage is pending. The other proof managers and the decoders have dedicated unit tests (see below).
ArkonixProofManager — KMS/EIP-712, per pool¶
Replaces merkle proofs with a single KMS-held authorized signer plus an on-chain target allow-list, built for automated keeper pipelines.
struct ArkonixCall { address target; bytes data; uint256 value; }
execute(ArkonixCall[] calls, uint256 nonce, uint256 deadline,
uint8 v, bytes32 r, bytes32 s) // PERMISSIONLESS relayer
execute verifies: batch size between 1 and 50 calls; an unexpired deadline; a one-time nonce; an EIP-712 signature over Execute(bytes32 callsHash, uint256 nonce, uint256 deadline) under the "ArkonixProofManager" (version 1) domain, with v ∈ {27, 28} and low-s enforced; and that the recovered signer equals the authorized signer. Per call, the calldata must be at least a selector long and the target must be on the allow-list.
- Configuration via
trustedCall(payload = action + data): rotate the signer, set/unset a single target, or batch-update targets. - Events:
UpdateSigner,UpdateTarget,ExecuteCall,Executed. Errors includeEmptyBatch,BatchTooLarge,SignatureExpired,NonceAlreadyUsed,UnapprovedTarget,CallFailed,UnauthorizedSigner, plus signature- and configuration-validation errors. - The EIP-712 domain separator is recomputed if the chain ID changes, so signatures cannot replay across chain forks.
- Its factory (one manager per pool, duplicate-guarded) is deployed as part of the standard full-stack deployment.
ArkonixScProofManager — KMS/EIP-712, per pool + share class¶
The same signature model, scoped to (pool, shareClass): an additional immutable share-class ID enforced in trustedCall, and a distinct EIP-712 domain (a different struct type and domain name, with the verifying contract in the domain) so signatures can never replay across manager variants or instances. Its factory registers one manager per (pool, shareClass).
Decoders (merkle managers only)¶
Decoders are stateless calldata sanitizers. The merkle manager staticcalls the decoder with the target call's calldata; the decoder re-declares the function signature and returns the packed address arguments to constrain. Those addresses become part of the merkle leaf — so a policy root whitelists which concrete addresses each selector may touch. Unrecognized selectors revert; amounts are intentionally left unconstrained (policies constrain where funds can go, not how much moves).
| Decoder | Whitelisted operations (→ constrained arguments) |
|---|---|
BaseDecoder |
ERC-20/6909 approve (→ spender, and token ID where applicable); BalanceSheet deposit (→ pool, share class, asset, token ID); BalanceSheet withdraw (→ the same, plus receiver) |
VaultDecoder |
ERC-4626 deposit/mint/withdraw/redeem (→ receiver, owner); ERC-7540 requestDeposit/requestRedeem (→ controller, owner); ERC-7887 cancel and claim-cancel (→ controller, receiver) |
CircleDecoder |
CCTP depositForBurn (→ destination domain, mint recipient, burn token); receiveMessage (unconstrained) |
New protocols require new decoders — for example, a DEX-router decoder is planned for merkle-gated rebalance swaps.
Test coverage¶
- Unit: dedicated suites for the upstream
MerkleProofManager,ArkonixProofManager, andArkonixScProofManager(constructors, trusted-call success/failure including scope checks, execution success/failure including signature edge cases, factories), plus fuzzed decoder tests (VaultDecoder,CircleDecoder). - Integration: the merkle-gated execution path runs on the full protocol test stack, fuzzing BalanceSheet deposit/withdraw execution and the invalid-proof / not-a-strategist / not-a-balance-sheet-manager failure modes.
- Gap:
ArkonixMerkleProofManagerlacks a dedicated test file, as noted above.
Index strategy architecture — [IN FLIGHT]¶
In development
The architecture described in this section is under active development and subject to change before launch. Contract interfaces, parameters, thresholds, and identifiers are not final and must not be relied on. The scoped proof managers it builds on are shipped; the bespoke strategy contracts are not.
This is the Arkonix strategy-execution architecture: how an index product (see the SignalForge index catalog) becomes an on-chain vault with an auditable, policy-bounded rebalancing loop. JRRTVaultManager (below) is the first instance; the pattern generalizes to every future index product.
Design: two contracts per index¶
Each Arkonix index product runs as a Centrifuge share class and gets two dedicated on-chain components:
-
A bespoke Index Strategy Manager (hub-side, one per index). Custom-built per strategy, because every index has different constituents, oracles, weight sources, and rebalance rules. Its responsibilities:
- Index publication: the index provider posts periodic index values and target weights, creating an immutable on-chain audit trail (
IndexUpdatedevents and historical snapshots). - Rebalance computation: it reads live holdings from the pool escrow, values them with the strategy's own oracles, computes drift against target weights, and — above the strategy's drift threshold — emits deterministic
SwapInstructed(sellAsset, buyAsset, sellAmount)instructions. Triggering is permissionless. - Execution attestation: a keeper confirms execution (
RebalanceConfirmed), closing the instructed-to-executed audit loop.
- Index publication: the index provider posts periodic index values and target weights, creating an immutable on-chain audit trail (
-
A scoped proof manager instance (spoke-side, one per
(pool, shareClass)), deployed from the shipped Arkonix factories:ArkonixMerkleProofManager(merkle; human or multisig strategist) orArkonixScProofManager(KMS signature; automated keeper), depending on the strategy's operational model. It is the only contract with authority over that share class's funds — as its registered BalanceSheet manager — and its policy is set exclusively through the timelock-backedHub.updateContract→ContractUpdaterchannel.
End-to-end rebalance flow¶
1. publisher → IndexStrategyManager.updateIndex(value, weights, date) [hub]
2. anyone → IndexStrategyManager.rebalance()
→ emits SwapInstructed(...) [hub]
3. keeper bot observes SwapInstructed off-chain, builds the batch:
balanceSheet.withdraw(sell asset)
→ swap → balanceSheet.deposit(buy asset)
4. keeper → ScopedProofManager.execute(calls,
merkle proofs | KMS signature) [spoke]
every call verified against the strategy's policy
5. keeper → IndexStrategyManager.confirmRebalance(date) [hub]
Steps 3–5 are driven by the off-chain execution stack — see Backend execution.
Why bespoke contracts per strategy¶
- Heterogeneous oracles and valuation: each index needs its own price feeds, NAV attestations, or FX sources. Hardcoding valuation per strategy keeps it auditable and shrinks the governance-parameter attack surface.
- Heterogeneous policies: drift thresholds, staleness windows, idle-cash deployment rules, and constituent counts differ per index.
- Isolation: a bug or compromised role in one strategy contract cannot touch another index — each strategy pairs with its own
(pool, shareClass)-scoped proof manager, so the blast radius is one share class. - Regulatory and audit trail: each index gets a self-contained, queryable on-chain history that matches exactly what its provider published — relevant, for example, to Shariah-compliance attestations.
Why per-share-class proof managers¶
Upstream's MerkleProofManager is pool-scoped; Arkonix runs multiple strategies as share classes, potentially in a shared pool. The (pool, shareClass) variants confine each strategy's execution authority to its own share class. The KMS variants additionally remove per-transaction merkle-proof generation from automated pipelines: the keeper's KMS signs one EIP-712 batch, any relayer submits it, and the on-chain target allow-list bounds what the signature can reach.
New-index onboarding (high level)¶
Onboarding a new index follows a standard sequence:
- Pool and share class: create or reuse a pool, add the share class, notify the spoke, deploy and link vaults, and configure the transfer hook (
FullRestrictionsfor permissioned depositors). - Write the bespoke Index Strategy Manager: publisher-gated index updates (weight validation, monotonic report dates), strategy-specific oracle valuation, a permissionless, staleness-guarded, drift-thresholded
rebalance()that emitsSwapInstructed, keeper-gated confirmation, and a standard history/view surface — using the ward pattern, following theJRRTVaultManagertemplate. - Deploy a scoped proof manager from the appropriate factory (KMS or merkle variant) and register it as a BalanceSheet manager via the Hub.
- Author the policy: merkle route — build the leaf set from the decoders (selectors × targets × constrained addresses: escrows, vaults, approved DEX routers, receivers) and publish the root via
Hub.updateContract, adding a new decoder if the strategy touches protocols the existing decoders don't cover; KMS route — push the signer and target allow-list through the same channel. - Wire the off-chain services: the publisher listener (provider feed → index updates) and the keeper bot (swap instructions → execution batch → signature → execute → confirm).
- Fees: initialize the pool on
ArkonixFeeManager, set the fee configuration, and route share-price updates through it.
JRRTVaultManager — [IN FLIGHT]¶
In development
JRRTVaultManager is written but not yet finalized, committed, or deployed. Its interfaces, parameters, thresholds, and identifiers are subject to change before launch, and nothing here should be treated as a deployment reference.
JRRTVaultManager is the first bespoke index-strategy contract, managing the JRRT vault: a Shariah-compliant index of gold (PAXG) and inflation-linked USDi, with target weights published daily by Enduring Investments. It maintains an on-chain audit trail of index values and rebalance instructions.
Roles:
| Role | Held by | Authority |
|---|---|---|
publisher |
Index provider / Arkonix listener | Posts daily index values and target weights (updateIndex) |
keeper |
Arkonix execution bot | Confirms rebalance execution (confirmRebalance) |
| Ward admin | Governance (standard Auth) |
Rotates the publisher and keeper roles |
Mechanics:
updateIndex(publisher-only) records the index value and target weights; weights must fully allocate the portfolio, report dates must be strictly increasing, and each update snapshots into an on-chain index history.rebalance()is permissionless but reverts if the published data is stale. It first deploys any idle USDC pro-rata toward the target weights, then — if the PAXG/USDi split has drifted beyond the strategy's threshold — emits a directionalSwapInstructed(sellAsset, buyAsset, sellAmount)instruction and records the rebalance for the audit trail; otherwise it emitsRebalanceSkipped.confirmRebalance(keeper-only) attests that the instructed swaps were executed, closing the loop.- Valuation: PAXG is priced via a Chainlink PAXG/USD feed (non-positive answers are rejected); USDi is valued against a CPI-prorated index; live vault composition is read from the pool escrow's available balances.
- View surface: rebalance parameters, current composition, staleness, index history, rebalance records, and pending-swap status.
Events: IndexUpdated, SwapInstructed, RebalanceSkipped, RebalanceConfirmed, PublisherUpdated, KeeperUpdated.
For the product-level view of JRRT and the other indices, see the SignalForge index catalog.