Skip to main content
The PortfolioStrategy manages a weighted basket of up to 20 tokens. On execute it swaps the vault’s asset into each token at the target weight; on settle it sells everything back to the asset. The proposer can rebalance at any time while the proposal is Executed, either by selling everything and re-buying at current weights or by using Chainlink Data Streams prices to swap only the deltas (gas-efficient). Swaps go through UniswapSwapAdapter (Base) or SynthraSwapAdapter / SynthraDirectAdapter (Robinhood L2). Route selection is done CLI-side: the CLI probes direct asset→token pools at multiple fee tiers and falls back to an asset→WETH→token multi-hop when no direct pool exists, then encodes the chosen route into each token’s swapExtraData. The on-chain UniswapSwapAdapter does not auto-detect — it reads a leading mode byte from extraData and executes exactly that route. This is how you can hold tokens without liquid USDC pairs.

swapExtraData encoding

extraData is a 1-byte mode prefix followed by the mode’s ABI-encoded route data:
ModeRouteEncoding (bytes1(mode)abi.encode(...))
0V3 single-hopbytes1(0x00)abi.encode(uint24 fee)
1V3 multi-hopbytes1(0x01)abi.encode(bytes v3Path, uint16 perHopSlippageBps)
Mode-1 carries perHopSlippageBps — manual integrators must include it. For a multi-hop route the adapter pre-quotes each hop via quoter.quoteExactInputSingle(...) and floors each non-terminal hop’s output at quoted * (10000 - perHopSlippageBps) / 10000 (perHopSlippageBps must be ≤ 10000). Omitting the trailing uint16 makes the adapter’s abi.decode(routeData, (bytes, uint16)) revert. The CLI sets this for you; only hand-encoded swapExtraData needs to supply it. (Sherlock run-2 #11 / PR #354.)

Architecture

Lifecycle

Pending → execute() → Executed → (rebalance?) → settle() → Settled
PhaseWhat happensWho calls
ExecutePull asset → swap to each basket token at target weight (via SwapAdapter)Governor (proposal execution)
ExecutedProposer can rebalance(), rebalanceDelta(reports), or update weights/slippageProposer
SettleSwap all tokens back to asset → push to vaultGovernor (proposal settlement)

Batch Calls

Execute

[asset.approve(strategy, totalAmount), strategy.execute()]

Settle

[strategy.settle()]

InitParams

Init data is an ABI-encoded positional tuple (not a named struct). Tokens, weights, swap data, and price-feed decimals are passed as parallel arrays (same length, same order), not as a TokenAllocation[]:
(
    address   asset,            // Vault asset (USDC on Base, WETH on Robinhood L2)
    address   swapAdapter,      // UniswapSwapAdapter or SynthraSwapAdapter
    address   chainlinkVerifier,// Used by rebalanceDelta / live-NAV; pass address(0) to opt out
    address[] tokens,           // Basket tokens
    uint256[] weightsBps,       // Target weight per token in bps (sum = 10000)
    uint256   totalAmount,      // Total asset to deploy
    uint256   maxSlippageBps,   // Per-swap slippage cap — REQUIRED, 1–9999 bps
    bytes[]   swapExtraData,    // Per-token adapter route data (mode byte + encoded path)
    uint8[]   priceDecimals     // Per-token Chainlink feed scale (8 for stocks, 18 for crypto)
)
  • Max basket size: 20 tokens
  • Weights are in basis points and must sum to 10000
  • maxSlippageBps applies to every swap (entry, settle, and rebalance). It is required: initialization reverts with InvalidSlippage if it is 0 or >= 10000. (The 500 you see in CLI examples is a CLI-side default, not a contract default.)
  • priceDecimals[i] must be <= 36 (InvalidPriceDecimals otherwise) so the decimal-scaling math in live NAV stays safe.

Rebalancing

While the proposal is Executed, the proposer can rebalance two ways:
MethodGas costWhen to use
rebalance()High — sells all, re-buys at current weightsWeights changed, or simple periodic rebalance
rebalanceDelta(ChainlinkReport[])Low — swaps only the deltasFrequent rebalances with fresh oracle prices
rebalanceDelta reads a signed Chainlink Data Streams V3 report for each token and rejects any report past its own expiresAt timestamp (StalePrice). The separate 5-minute MAX_PRICE_AGE window gates the live-NAV price cache — it controls when positionValue() flips to valid=false, not which reports rebalanceDelta accepts. This path is only available on chains with a Chainlink verifier proxy.

Tunable Parameters (Executed state)

ParameterDescription
targetWeightBpsPer-token target weight (must still sum to 10000)
maxSlippageBpsPer-swap slippage cap
swapExtraDataPer-token extra data for the swap adapter (e.g. path override, fee tier)

Risk Notes

  • Swap impact: Large allocations in thin pools can eat into P&L — set maxSlippageBps conservatively.
  • Oracle staleness: rebalanceDelta rejects any report past its expiresAt. The 5-minute MAX_PRICE_AGE cache window governs live NAV: once the cached price for any token is older than 5 minutes, positionValue() returns valid=false and the vault falls back to the async-redeem queue. Keepers call refreshPrices(reports) (~every 3 min) to keep live NAV available; if the feed goes stale, fall back to the full rebalance().
  • Settle path: settle() sells every token back to asset in a single transaction. A single illiquid token can revert the entire settlement; the proposer can update swapExtraData before settlement to route around it.

CLI Usage

sherwood strategy propose portfolio \
  --vault 0x... \
  --amount 10000 \
  --tokens TSLA,AMZN,PLTR,NFLX,AMD \
  --weights 2500,2500,2000,1500,1500 \
  --max-slippage 500 \
  --name "Stock Basket" \
  --performance-fee 1000 --duration 7d
FlagDescriptionDefault
--amount <n>Total asset to allocaterequired
--tokens <list>Comma-separated token addresses or symbolsrequired
--weights <list>Comma-separated weights in bps (sum = 10000)required
--max-slippage <bps>Per-swap slippage cap500
--fee-tier <n>Uniswap V3 pool fee tier3000
--swap-adapter <address>Override swap adapter addressauto-detected

Addresses

ContractBaseRobinhood L2 Testnet
PortfolioStrategy template0x42069e51c415f4BF4442D80F1532Bd38140Bd1350xAe981882923E0C76A7F10E7cAa3782023c0abd9B
Swap adapter0x679400a781A66d801C20DfD9E0020704e21e9d54 (Uniswap)0x39a37537E179919cb2dDDb1D6920dD11bAf3aDF0 (Synthra)
Chainlink Verifier Proxy0xDE1A28D87Afd0f546505B28AB50410A5c3a7387a0x72790f9eB82db492a7DDb6d2af22A270Dcc3Db64
Robinhood L2 (chain 46630) is the sole testnet network kept in beta because Synthra DEX + Chainlink Data Streams + the tokenized stock universe (TSLA / AMZN / PLTR / NFLX / AMD) are testnet-only deployments today.