Skip to main content

Payment Splitter Patterns

Purpose: Show when PaymentSplitter is the right downstream routing primitive in Octant v2. Audience: Developers deciding whether one donated yield stream should be split across several fixed onchain recipients. Level: Intermediate Source of truth: [email protected] and the PaymentSplitter contract reference for exact initializer and claim behavior. Use this page when: your strategy or vault needs fixed-share routing rather than custom allocation logic. Do not use this page for: vote-driven allocation design, recipient-specific business logic, or exact initializer details without the reference page.

Before you read this page

Use this page when a strategy or vault should fund more than one onchain recipient. It explains the practical role of PaymentSplitter in Octant v2 without sending you straight into the autogenerated reference page.

Read this alongside

Use this page for the routing pattern and decision-making. When you need exact function names or initializer details, keep PaymentSplitter open as the contract-reference layer.

What is a pull-payment model?

In a push model, the sender sends tokens to recipients automatically. In a pull model, the contract holds the tokens and each recipient must call a function to claim their share. Pull payments are safer (no risk of a failing recipient blocking everyone else) and let recipients claim on their own schedule.

Task card — integrating PaymentSplitter

Ask a coding assistant to follow this page in order and run the verification commands before moving on. It should not invent contract addresses, use placeholder implementation addresses, or skip fork/RPC prerequisites.

Goal: Deploy and wire a PaymentSplitter instance as the donation address for a strategy or vault, so that multiple onchain recipients can claim their fixed proportional share of routed yield.

Files to edit:

  • Your strategy or vault deployment script (e.g. script/DeployStrategy.s.sol)
  • Your deployment configuration / environment file (e.g. .env or deploy.config.ts)

Files NOT to edit:

  • src/core/PaymentSplitter.sol — implementation contract; interact only through the factory
  • src/factories/PaymentSplitterFactory.sol — deploy only through the factory's creation methods, not directly
  • Any autogenerated reference files under docs/developers/smart_contracts/

Required setup:

  • A local or forked RPC endpoint pointing at the correct network
  • The canonical PaymentSplitterFactory (see Deployed Addresses) reachable on that endpoint
  • Recipient addresses (payees) finalised before deployment — the split cannot be changed after initialization
  • octant-v2-core checked out at 36ed6ad or a compatible tag

Verification:

  1. After factory creation, call payee(index) on the returned splitter address for each recipient and confirm addresses match your inputs.
  2. Call shares(payeeAddress) for each recipient and confirm share units match your configuration.
  3. Route a small test amount to the splitter and call release(token, payeeAddress) for one recipient; confirm the correct proportional amount is transferred.
  4. Confirm no ETH or token balance remains attributable to a payee address that has already released its full entitlement.

Core target pin: octant-v2-core@36ed6ad

Do not:

  • Deploy PaymentSplitter directly — always use PaymentSplitterFactory
  • Invent factory addresses — use the canonical address from the Deployed Addresses page
  • Use this with rebasing or fee-on-transfer tokens without verifying compatibility
  • (See also: Agent Anti-Patterns)

What PaymentSplitter is for

PaymentSplitter is the most common donation address for an Octant deployment that has multiple downstream recipients.

Instead of sending donated strategy shares to a single address, you point the strategy or vault at a PaymentSplitter instance. The splitter then tracks how much each recipient is entitled to claim based on a fixed share schedule set at initialization.

Typical recipients include:

  • one or more allocation mechanisms,
  • a community staking contract such as RegenStaker,
  • an operational-cost address,
  • any other onchain recipient that should receive a fixed share of routed value.

When to use it

Check token compatibility first

PaymentSplitter assumes standard ERC-20 transfer semantics. It will not work correctly with rebasing tokens or fee-on-transfer assets. See Token compatibility notes before choosing this pattern.

Use PaymentSplitter when all of the following are true:

  • the incoming asset is the same for every downstream recipient,
  • the split is fixed rather than vote-driven,
  • recipients can claim asynchronously instead of receiving funds in the same transaction,
  • you want a simple routing layer between yield generation and the final beneficiaries.

Good examples:

  • 80% of donated shares to a TAM, 20% to an OpEx address,
  • 70% to a TAM, 20% to RegenStaker, 10% to treasury operations,
  • one strategy feeding a small set of fixed recipients with known addresses.

When not to use it

PaymentSplitter is usually the wrong tool when:

  • the split needs to change round by round,
  • recipient weights are determined by voting or proposal logic,
  • recipients need to be added or removed dynamically,
  • you need different downstream logic for different recipients rather than a pure proportional split.

In those cases, put a more specialized routing or allocation contract behind the donation flow instead of relying on PaymentSplitter alone.

The basic topology

Fragment — yield flow overview
Users deposit assets

Strategy or vault generates yield

Yield is represented as donated shares

PaymentSplitter receives the donated shares

Recipients claim their proportional share

A common Octant topology looks like this:

Fragment — common Octant topology
YDS or YSS
↓ donation address
PaymentSplitter
├─ 70% → Allocation mechanism
├─ 20% → RegenStaker
└─ 10% → OpEx / treasury address

Concrete configuration example

A practical configuration might look like this:

RecipientRoleShare UnitsPercentage
TAM (Tokenized Allocation Mechanism)Primary allocation7070%
RegenStakerCommunity staking rewards2020%
Operations treasuryMaintenance and overhead1010%
Total100100%

In this setup, every donated share minted by the strategy gets distributed according to those proportions. A strategy that donates 100 shares in a reporting period would result in 70 shares going to the TAM, 20 to RegenStaker, and 10 to the operations address. Each recipient then calls the token-specific release() function on the splitter to claim their share on their own schedule.

How the claiming model works

The following diagram shows how yield flows through the splitter to multiple recipients:

Illustrative only — claiming flow

PaymentSplitter uses a pull-payment model.

That means:

  • incoming ETH or ERC-20 tokens are accounted for by the splitter,
  • each payee has a fixed claim on that inflow based on its shares,
  • funds are released only when release is called.

For native ETH, the release path is the ETH-specific release function.

For ERC-20 tokens, including donated share tokens received from a strategy or vault, the release path is the token-specific release(token, account) function.

In practice this means recipients do not need to receive funds in the same transaction that routes yield. It also means claims can be operationally scheduled rather than pushed automatically during the report path.

Who can trigger a claim

The pull-payment model does not require the recipient itself to be the transaction sender. What matters is which account is released for. This makes it possible to automate claims operationally when appropriate.

What is fixed at initialization

The factory creation methods accept three arrays — payee addresses, payee names, and share counts — but the splitter's own initialize(address[] payees, uint256[] shares_) receives only two. Payee names are factory-layer metadata stored in the SplitterInfo struct; they do not exist on the PaymentSplitter contract itself.

At initialization, the splitter is given two arrays:

  • payee addresses,
  • share counts for those payees.

Payee names (human-readable labels such as "TAM", "RegenStaker", "OpEx") are recorded by the factory for off-chain indexing but are not part of the on-chain splitter state.

Those shares are proportional units, not percentages. A configuration like:

  • TAM: 70
  • RegenStaker: 20
  • OpEx: 10

means the recipients get 70%, 20%, and 10% respectively because the total is 100 share units.

The practical implication is important: treat the payee list and share schedule as part of your deployment design, not as a casual runtime setting.

The split is a deployment choice, not an operator setting

PaymentSplitter is designed for a fixed recipient set and a fixed proportional split. Do not treat it like a governance surface or an admin-tunable routing policy.

PaymentSplitterFactory creation method signatures

Signature as of 36ed6ad. Full reference: PaymentSplitterFactory.

MethodSignatureETH forwardedUser-predictable address
createPaymentSplittercreatePaymentSplitter(address[] payees, string[] payeeNames, uint256[] shares_) returns (address)NoYes — predict the next count-based address with predictDeterministicAddress(deployer)
createPaymentSplitterWithETHcreatePaymentSplitterWithETH(address[] payees, string[] payeeNames, uint256[] shares_) payable returns (address)YesYes — predict the next count-based address with predictDeterministicAddress(deployer)
createPaymentSplitterWithSaltcreatePaymentSplitterWithSalt(address[] payees, string[] payeeNames, uint256[] shares_, bytes32 salt) returns (address)NoYes — caller-supplied salt, predictable via predictDeterministicAddressWithSalt
createPaymentSplitterWithETHAndSaltcreatePaymentSplitterWithETHAndSalt(address[] payees, string[] payeeNames, uint256[] shares_, bytes32 salt) payable returns (address)YesYes — caller-supplied salt, predictable via predictDeterministicAddressWithSalt

All four methods use CREATE2 internally (OpenZeppelin Clones). The basic methods derive the salt from the deployer address and that deployer's current deployment counter. Call predictDeterministicAddress(deployer) to predict the next basic deployment from current factory state. This prediction is count-sensitive: if the same deployer creates another splitter before your transaction executes, the counter and predicted address change. The salt methods accept a caller-supplied salt and use predictDeterministicAddressWithSalt, giving governance and delayed-execution workflows a stable prediction that does not depend on the deployment counter.

The basic methods emit a PaymentSplitterCreated event; the salt-based methods emit a PaymentSplitterCreatedWithSalt event. All four record a SplitterInfo struct (including payee names) accessible via the factory. If you index these deployments off-chain, subscribe to both events — a filter on PaymentSplitterCreated alone silently misses every salt-based deployment.

How to deploy it correctly in Octant v2

For Octant v2, treat PaymentSplitter as implementation logic, not as the contract you deploy directly for live use.

The practical deployment path is to create an initialized splitter instance through the factory layer.

Use the current PaymentSplitterFactory from Deployed Addresses. In examples below, PAYMENT_SPLITTER_FACTORY means address constant PAYMENT_SPLITTER_FACTORY = <current deployed factory address>;.

That factory-based flow matters because the implementation contract is not meant to be used as a naive "deploy, then initialize later" instance.

The factory exposes four creation methods. All four use CREATE2 internally and take the same core arguments (payees, names, shares). They differ in whether they forward ETH to the new splitter and whether they accept a caller-supplied salt for user-predictable address computation (see the table above for details).

createPaymentSplitter — basic deployment

Fragment — createPaymentSplitter call
address splitter = PaymentSplitterFactory(PAYMENT_SPLITTER_FACTORY)
.createPaymentSplitter(
payees, // address[] — recipient addresses
payeeNames, // string[] — human-readable labels (e.g., "TAM", "OpEx")
shares // uint256[] — proportional share units
);

Use this when you need a new splitter instance and do not need a predictable address or initial ETH balance.

createPaymentSplitterWithETH — deploy and fund with ETH

Fragment — createPaymentSplitterWithETH call
address splitter = PaymentSplitterFactory(PAYMENT_SPLITTER_FACTORY)
.createPaymentSplitterWithETH{value: msg.value}(
payees,
payeeNames,
shares
);

Same as the basic method, but forwards the attached ETH to the new splitter at creation time. Useful when the splitter should hold an initial ETH balance before any yield is routed.

createPaymentSplitterWithSalt — deterministic CREATE2 deployment

Fragment — createPaymentSplitterWithSalt call
address splitter = PaymentSplitterFactory(PAYMENT_SPLITTER_FACTORY)
.createPaymentSplitterWithSalt(
payees,
payeeNames,
shares,
salt // bytes32 — caller-chosen salt for address prediction
);

Deploys to a deterministic CREATE2 address. The factory derives the final salt from four inputs: the caller address (msg.sender), the payees array, the shares array, and the caller-provided salt. To predict the address off-chain before deployment, call predictDeterministicAddressWithSalt(deployer, payees, shares, salt) on the factory — it returns the exact address that createPaymentSplitterWithSalt will deploy to.

Use this when you need the splitter address before deployment, for example when a governance proposal must reference the splitter address before it exists on-chain.

createPaymentSplitterWithETHAndSalt — deterministic deployment with initial ETH

Fragment — createPaymentSplitterWithETHAndSalt call
address splitter = PaymentSplitterFactory(PAYMENT_SPLITTER_FACTORY)
.createPaymentSplitterWithETHAndSalt{value: msg.value}(
payees,
payeeNames,
shares,
salt // bytes32 — caller-chosen salt for address prediction
);

Combines both features: deterministic address via CREATE2 and ETH forwarding at creation time.

Which method to use

NeedMethod
Simple splitter, no special requirementscreatePaymentSplitter
Splitter should hold ETH immediatelycreatePaymentSplitterWithETH
Address must be known before deployment (governance, multisig proposals)createPaymentSplitterWithSalt
Both predictable address and initial ETHcreatePaymentSplitterWithETHAndSalt

A safe mental model for the flow is:

  1. the factory manages the deployable implementation layer,
  2. the factory creates a new splitter instance initialized with payees, names, and shares,
  3. your strategy or vault points its donation flow at that initialized splitter instance.

A worked pattern

Suppose a capital provider wants one strategy to fund three downstream recipients:

  • a tokenized allocation mechanism,
  • a community staking pool,
  • an operational address.

A reasonable flow is:

  1. Deploy the strategy or vault.
  2. Create and initialize a PaymentSplitter instance through the factory with the three recipient addresses and share weights.
  3. Set the strategy or vault donation address to that splitter instance.
  4. Run the normal report() cycle.
  5. Let downstream recipients claim on their own schedule, using the token-specific or ETH-specific release path as appropriate.

This keeps the yield-generation logic separate from the distribution logic.

Design questions to answer before deployment

Before using PaymentSplitter, decide:

  1. Who are the final recipients? Their addresses should be known and reviewed before initialization.
  2. Should the split be fixed or governed elsewhere? If it is not fixed, PaymentSplitter may be the wrong primitive.
  3. Who is responsible for claiming? Each recipient, or an agreed operator, needs a process for calling the correct release path.
  4. What asset is being routed? All recipients must be able to handle the same incoming token or share token.
  5. Do you need one splitter or several? Sometimes it is cleaner to place one splitter upstream and a more specialized mechanism downstream.

Token compatibility notes

PaymentSplitter is best suited to standard ETH and standard ERC-20-style inflows.

Be cautious if the routed asset has non-standard transfer behavior.

In particular, do not assume this pattern is appropriate for:

  • rebasing tokens,
  • fee-on-transfer tokens,
  • other assets whose transfer semantics break simple balance-based accounting.

If your downstream asset has unusual accounting behavior, validate compatibility before using PaymentSplitter as the routing layer.

Common mistakes to avoid

  • Treating PaymentSplitter like a dynamic policy engine. It is a fixed proportional splitter, not a governance system.
  • Forgetting the pull-payment model. Recipients do not get paid automatically just because yield was routed.
  • Describing the deployment as "deploy the splitter and initialize it later" instead of using the factory-based instance flow.
  • Using the ETH release path when the routed asset is actually an ERC-20 or donated share token.
  • Using it where downstream recipients need different business logic rather than a simple split.
  • Treating share numbers as meaningful on their own. Only the ratio between shares matters.

Where this fits in the docs

Read this page alongside: