Skip to content

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/pendingAdmin transfer (file("admin", ...) followed by acceptAdmin() 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] = newRoot and emits UpdatePolicy.
  • Execution: execute(Call[] calls), where each Call carries {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 a PolicyLeaf {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 include EmptyBatch, 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, and ArkonixScProofManager (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: ArkonixMerkleProofManager lacks 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:

  1. 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 (IndexUpdated events 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.
  2. A scoped proof manager instance (spoke-side, one per (pool, shareClass)), deployed from the shipped Arkonix factories: ArkonixMerkleProofManager (merkle; human or multisig strategist) or ArkonixScProofManager (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-backed Hub.updateContract → ContractUpdater channel.

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:

  1. 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 (FullRestrictions for permissioned depositors).
  2. 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 emits SwapInstructed, keeper-gated confirmation, and a standard history/view surface — using the ward pattern, following the JRRTVaultManager template.
  3. Deploy a scoped proof manager from the appropriate factory (KMS or merkle variant) and register it as a BalanceSheet manager via the Hub.
  4. 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.
  5. Wire the off-chain services: the publisher listener (provider feed → index updates) and the keeper bot (swap instructions → execution batch → signature → execute → confirm).
  6. 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 directional SwapInstructed(sellAsset, buyAsset, sellAmount) instruction and records the rebalance for the audit trail; otherwise it emits RebalanceSkipped.
  • 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.