Skip to main content
This page covers the smart contract implementation details. See Glossary.
Additional rewards are incentive tokens distributed outside the normal yield flow by DeFi protocols. These rewards — such as AAVE, COMP, MORPHO, or ENA tokens — arrive via side channels and can significantly enhance user returns. The key characteristic is that these rewards do not flow through the normal deposit/redeem lifecycle. They are distributed directly to vehicle addresses by external reward contracts and must be captured and distributed separately from base yield.

The interceptor standard

The Interceptor standard is Railnet’s solution for managing additional reward distribution. It provides a declarative on-chain configuration that off-chain indexing services read to correctly route rewards.

How it works

1

Declare interception rules

The Vehicle declares distribution rules via the interceptions() view function.
2

Indexers query rules

Off-chain indexers (e.g., reward distributors, airdrop systems) read these rules to determine how to route rewards.
3

Rewards are distributed

The indexer applies the routing logic and distributes rewards according to the declared rules.
By keeping distribution logic off-chain, Railnet achieves gas efficiency (no on-chain overhead for reward splitting), flexibility (rules can be updated without migrating assets), and cross-chain support (rules can specify chainId targets).

Core structures

struct Interception {
    address asset;              // Reward token (address(0) = all assets)
    Recipient[] recipients;     // Distribution rules
}

struct Recipient {
    address target;   // Where rewards go
    uint256 shareBps; // Share in basis points (10000 = 100%)
    uint256 chainId;  // Target chain (0 = all chains)
}
Key features:
  • Asset-specific rules — Different routing for different reward tokens
  • Multi-recipient — Split rewards among multiple addresses
  • Cross-chain support — Route rewards to different chains
  • Pass-through — Unallocated rewards (where total shareBps < 10,000) pass through to users
  • Last-matching-rule semantics — Later rules override earlier ones for the same asset, enabling powerful override patterns

Matching semantics

Interceptors use last-matching-rule semantics, similar to CSS or firewall rules:
  1. The indexer iterates through the interceptions array and selects the last entry that matches the reward token (either exactly or via the address(0) wildcard)
  2. Within the selected rule, for each unique target, only the last valid entry matching the current chainId (or the 0 wildcard) is used
This allows you to define a default rule for all tokens, then append overrides for specific high-value tokens.

Distribution strategies

All additional rewards flow directly to end users with no operator intervention.
// Empty interceptions = 100% pass-through
Interceptor.Interception[] memory interceptions =
    new Interceptor.Interception[](0);
multiVehicle.setInterceptions(interceptions);
Best for: Community-first strategies, DAOs with community governance, maximum transparency setups.

Reinvestment mechanics

When using the reinvestment strategy, the operator sells reward tokens and deposits the proceeds back into the Multi-Vehicle without minting new shares. This increases the share price for all holders.
// Before reinvestment:
// Total assets: 1,000,000 USDC
// Total supply:  1,000,000 shares
// Share price:   1.0 USDC/share

// Rewards earned and sold:
// 10,000 COMP -> 50,000 USDC
// 5,000 AAVE  -> 100,000 USDC
// Total to reinvest: 150,000 USDC

// Operator deposits without minting shares:
USDC.approve(address(sectorAccountingEngine), totalUsdc);
sectorAccountingEngine.deposit(
    totalUsdc,
    false  // allocate=false: assets go to DEPOSIT sector
);

// After reinvestment:
// Total assets: 1,150,000 USDC
// Total supply:  1,000,000 shares (unchanged)
// Share price:   1.15 USDC/share (+15%)
Calling sectorAccountingEngine.deposit() without minting shares increases totalAssets while keeping totalSupply constant. Since share price = totalAssets / totalSupply, all existing holders benefit proportionally.

Configuration

Prerequisites

  • A deployed Multi-Vehicle or Vehicle
  • The VEHICLE_SET_INTERCEPTIONS role scoped to the Vehicle contract

Asset-specific fees

Apply different fee rates to different reward tokens.
Interceptor.Interception[] memory interceptions = new Interceptor.Interception[](3);

// Default: 5% on all tokens
interceptions[0] = Interceptor.Interception({
    asset: address(0),
    recipients: new Interceptor.Recipient[](1)
});
interceptions[0].recipients[0] = Interceptor.Recipient({
    target: operatorFeeAddress,
    shareBps: 500,   // 5%
    chainId: 0
});

// AAVE rewards: 10% fee
interceptions[1] = Interceptor.Interception({
    asset: address(AAVE_TOKEN),
    recipients: new Interceptor.Recipient[](1)
});
interceptions[1].recipients[0] = Interceptor.Recipient({
    target: operatorFeeAddress,
    shareBps: 1000,  // 10%
    chainId: 1       // Ethereum mainnet only
});

// COMP rewards: 3% fee
interceptions[2] = Interceptor.Interception({
    asset: address(COMP_TOKEN),
    recipients: new Interceptor.Recipient[](1)
});
interceptions[2].recipients[0] = Interceptor.Recipient({
    target: operatorFeeAddress,
    shareBps: 300,   // 3%
    chainId: 0
});

multiVehicle.setInterceptions(interceptions);

Multi-recipient split

Split fees between multiple addresses.
// Split: 3% to operator, 2% to DAO treasury, 95% passes through to users
Interceptor.Interception[] memory interceptions = new Interceptor.Interception[](1);
interceptions[0] = Interceptor.Interception({
    asset: address(0),
    recipients: new Interceptor.Recipient[](2)
});
interceptions[0].recipients[0] = Interceptor.Recipient({
    target: operatorAddress,
    shareBps: 300,   // 3%
    chainId: 0
});
interceptions[0].recipients[1] = Interceptor.Recipient({
    target: daoTreasuryAddress,
    shareBps: 200,   // 2%
    chainId: 0
});
// Remaining 95% passes through to users

multiVehicle.setInterceptions(interceptions);

Hybrid fee + reinvestment

Take a small operator fee and reinvest the rest.
// 2% fee to operator, 98% reinvested for users
Interceptor.Interception[] memory interceptions = new Interceptor.Interception[](1);
interceptions[0] = Interceptor.Interception({
    asset: address(0),
    recipients: new Interceptor.Recipient[](2)
});
interceptions[0].recipients[0] = Interceptor.Recipient({
    target: operatorFeeAddress,
    shareBps: 200,    // 2% fee
    chainId: 0
});
interceptions[0].recipients[1] = Interceptor.Recipient({
    target: operatorRewardVault,
    shareBps: 9800,   // 98% reinvest
    chainId: 0
});

multiVehicle.setInterceptions(interceptions);

Reinvestment workflow

After rewards accumulate at the operator reward vault:
1

Sell reward tokens for the base asset

Use a DEX aggregator (e.g. 1inch, Paraswap) to sell accumulated reward tokens for the Multi-Vehicle’s base asset (e.g. USDC).
2

Deposit back into the Multi-Vehicle

Call deposit() on the Sector Accounting Engine with allocate=false. This increases totalAssets without minting new shares, raising the share price for all holders.Requires: MULTI_VEHICLE_DEPOSIT role scoped to the Sector Accounting Engine.
// Approve and deposit reinvested rewards
IERC20(usdc).approve(address(sectorAccountingEngine), totalRewards);
sectorAccountingEngine.deposit(totalRewards, false);
// allocate=false: assets go to the deposit sector as idle liquidity
3

Verify share price impact

After reinvestment, totalAssets increases while totalSupply stays the same, resulting in a higher share price.
// Before reinvestment: totalAssets = 1,000,000, totalSupply = 1,000,000
// Share price = 1.0

// After reinvesting 150,000 USDC from rewards:
// totalAssets = 1,150,000, totalSupply = 1,000,000
// Share price = 1.15 (+15%)

Advanced patterns

Let users keep governance tokens while reinvesting yield tokens.
// Default: Reinvest all
interceptions[0] = Interceptor.Interception({
    asset: address(0),
    recipients: reinvestRecipients  // 100% to operator
});

// COMP: Pass through to users (for governance voting)
interceptions[1] = Interceptor.Interception({
    asset: address(COMP_TOKEN),
    recipients: new Interceptor.Recipient[](0)  // 100% pass-through
});

// AAVE: Pass through to users
interceptions[2] = Interceptor.Interception({
    asset: address(AAVE_TOKEN),
    recipients: new Interceptor.Recipient[](0)
});

Strategy comparison

StrategyUser experienceOperator complexityShare price impactGovernance rights
Pass-throughUsers claim multiple tokensNoneNoneUsers keep tokens
Fee collectionUsers claim reduced amountsLowNoneUsers keep most tokens
ReinvestmentSimple (share price grows)High (selling + redepositing)Positive (+APY)Operator controls
HybridSimple + small fee deductionHighPositiveOperator controls most

Required role

Updating interception rules requires the VEHICLE_SET_INTERCEPTIONS role:
multiVehicle.setInterceptions(interceptions);
// Caller must have Roles.VEHICLE_SET_INTERCEPTIONS