Skip to main content
This page is for integrators, operators, and auditors working against the Red Potion contracts. It explains how the system’s three layers fit together, the two design patterns that recur everywhere (hub-and-spoke and the batch lifecycle), and the operational points that matter when deploying and running a fund. Red Potion is built with Foundry, Solidity 0.8.34, and OpenZeppelin v5 upgradeable contracts. Every deployed contract is a TransparentUpgradeableProxy. All module state uses ERC-7201 namespaced storage (neobank.storage.*) to stay upgrade-safe.

The three layers

Protocol root. The FundManagerDeployer is the single contract the protocol team operates. It holds the canonical implementation address for every contract type, creates one FundManager per tenant, and stores the protocol fee recipient that every fund resolves live. Tenant. Each FundManager is a per-tenant launchpad that owns eight Factory instances — one per component — and wires a complete fund (hub plus six spokes) in a single createFund transaction. It also deploys strategies on behalf of its funds and controls which implementation future components use. Fund instance. Each fund is a hub-and-spoke cluster: the Fund hub plus the FundShare, DepositQueue, RedeemQueue, Oracle, FeeManager, and RiskManager spokes, plus any number of Strategy execution wallets.

Pattern 1: Star (hub-and-spoke)

The Fund is the hub. Spokes never talk to each other — only to the Fund, which mediates all inter-spoke communication. Just as important, spokes hold no role state of their own: their admin functions authorize by calling back into the Fund’s access control. The practical result is that every role for a fund and its five governed spokes is granted and revoked in one place — on the Fund. See Access Control & Roles. The Fund contract itself is thin; most of its behavior comes from composable modules mixed into it:

Pattern 2: The batch lifecycle

There is no continuous AMM-style pricing. The Oracle divides time into batches separated by cutoff times. Deposits and redemptions queue into the current batch; after the cutoff an off-chain reporter submits a per-asset NAV price; after a mandatory review delay the report is accepted, which settles all deposit and redeem batches at a single fair price and accrues fees — all inside one Fund.acceptReport transaction. Key conventions:
  • Prices are 1e18-scaled asset-per-share: shares = amount × 1e18 / price.
  • Native ETH is supported everywhere via the sentinel 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE.
  • Fees are paid in newly minted shares, never in assets.
  • Requests are cancellable until their batch settles; claims never expire.
  • Redemption payouts are snapshotted at settlement, so later fee/price changes can’t desynchronize accounting.

Settlement in one transaction

Fund.acceptReport(nextCutoffTime) is the heart of the system. In a single call it:
1

Assemble the asset set

Assembles the asset set = union(deposit assets, redeem assets) + the fee base asset.
2

Accept the Oracle report

Calls Oracle.acceptReport, which validates the batch is closed, no suspicious price is pending, and every asset is inside its accept window — then consumes pending reports, updates lastAcceptedPrice, advances the batch, and sets the next cutoff.
3

Accrue fees

For the fee base asset, calls FeeManager.accrueFees and mints management/performance/protocol fee shares.
4

Settle deposits and redemptions

For each asset, mints user and entry-fee shares for the deposit batch (settling it), and burns redeemed shares while snapshotting the redeem payout net of exit fee (settling that batch).
A parallel entrypoint, acceptSuspiciousReport, does the same but bypasses the suspicious-price check and requires a stronger role — for use after manual review.

Redemption funding is a second step

Settlement only records what each redeem batch is owed. Paying it out is separate and role-gated, because at settlement the fund’s assets are usually deployed. Assets return to the Fund one of two ways: the Fund pulls them from a Strategy (a strategy can never refuse a pull), or an external wallet / bridge transfers them back to the Fund address (operational trust — the Fund cannot pull from an external wallet). Once the Fund holds enough, FUND_REDEEM_ROLE calls fundRedeem to deliver the snapshotted amount to the RedeemQueue and mark the batch claimable.

Strategies: bounded execution

A Strategy is a fund-controlled wallet that deploys capital into external protocols, but only through allowlisted calls. The allowlist is keyed per (caller, target, selector), optionally with pinned calldata words for constrained calls (e.g. allow transfer(address,uint256) only to a specific recipient, leaving the amount free). Two invariants bound the operator: the Fund can always pull assets back (pullAsset is onlyFund), and a strategy can never call the Fund (target == fund is rejected). For capital that must execute on a different chain than the fund, a StandaloneStrategy uses the identical allowlist engine, detached from Fund control. Assets are bridged operationally and results are reflected into NAV via the Oracle.

Upgrade model

Roles govern behavior; ProxyAdmin ownership governs code. Every contract is a TransparentUpgradeableProxy whose auto-deployed ProxyAdmin is owned by the proxyAdmin address chosen at tenant/fund creation — giving the tenant upgrade authority over all of its funds’ contracts. Changing an implementation on the FundManagerDeployer or a Factory affects only future deployments; existing proxies are upgraded through their own ProxyAdmins. The protocol operator cannot push code into a live fund.

Operational notes

  • Settlement is all-or-nothing per report. acceptReport reverts if any pending report in the asset set is flagged suspicious. Either reject and resubmit the bad price, or use acceptSuspiciousReport with the stronger role after review.
  • The fee base asset must always be reported. Time-based fees and risk valuation depend on it, so it’s always part of the settlement asset set.
  • External-wallet and cross-chain legs are trust-based. The Fund cannot pull assets it pushed to an external wallet or bridged to a StandaloneStrategy; returning them is the controller’s responsibility. Size operational trust accordingly.
  • Deployment is multi-step. A fund is created by FundManager.createFund, which wires the hub and all spokes; the fund’s DEFAULT_ADMIN_ROLE must then grant operational roles (reporter, acceptor, allocator, etc.) before the fund can run.

Source and further reading

Per-contract detail — responsibilities, flows, and full function tables — lives in the Contract Reference. The authoritative source is the zentryHQ/red-potion-contract repository.