For the complete documentation index, see llms.txt. This page is also available as Markdown.

Deploying Capital

description: Operational runbook for the OPERATOR_ROLE holder deploying capital and funding redemptions.

Deploying capital

OPERATOR_ROLE holds three functions on the Manager:

Function
Purpose

execute(targets[], data[])

Deploy capital to whitelisted destinations

transferToVault(token, amount)

Return capital so redemptions can be paid

redeemFor(vault, user)

Settle a user's matured redemption on their behalf

Before you start

MANAGER=<manager proxy>
VAULT=<vault proxy>
ASSET=<underlying token>
RPC=<rpc url>

execute — step by step

1

Confirm you hold the role

cast call $MANAGER "hasRole(bytes32,address)(bool)" \
  $(cast keccak "OPERATOR_ROLE") <your address> --rpc-url $RPC

false reverts with AccessControlUnauthorizedAccount.

2

Confirm every target is whitelisted

Check this before building anything else. The whitelist is the only constraint on where capital can go, and an unlisted target reverts the entire batch with NotWhitelisted.

cast call $MANAGER "whitelistedAddresses(address)(bool)" <target> --rpc-url $RPC

To see the full authorised set rather than checking one address at a time, read the event history:

cast logs --address $MANAGER "WhitelistUpdated(address,address,bool)" \
  --from-block <deployment block> --rpc-url $RPC

Whitelisting is ADMIN_ROLE, not yours. If a destination is missing, it must be added by the admin multisig before you can reach it — you cannot self-authorise.

3

Build the calldata

Each entry in data[] is a complete ABI-encoded call to the matching entry in targets[].

# Approve a protocol to pull USDC from the Manager
cast calldata "approve(address,uint256)" <spender> <amount>

# Transfer USDC to a whitelisted destination
cast calldata "transfer(address,uint256)" <recipient> <amount>

# Any protocol function
cast calldata "deposit(uint256,address)" <amount> $MANAGER
4

Simulate the batch

cast call $MANAGER "execute(address[],bytes[])(bytes[])" \
  "[<target1>,<target2>]" "[<data1>,<data2>]" \
  --from <operator> --rpc-url $RPC

This returns each call's return data without sending anything. A successful simulation confirms the guards pass and no target reverts — it does not confirm the amounts are correct.

5

Send

cast send $MANAGER "execute(address[],bytes[])" \
  "[<target1>,<target2>]" "[<data1>,<data2>]" \
  --rpc-url $RPC --private-key $PK
6

Verify

# Manager's remaining balance
cast call $ASSET "balanceOf(address)(uint256)" $MANAGER --rpc-url $RPC

# The batch, with full targets and calldata
cast logs --address $MANAGER "BatchExecuted(address,address[],bytes[])" \
  --from-block latest --rpc-url $RPC

BatchExecuted records the complete calldata of every call, so each deployment is publicly auditable after the fact.

Behaviour to know before you rely on it

The batch is atomic. One failing call reverts the entire transaction — no partial application. Ordering within a batch is therefore safe for dependent operations (approve then deposit in one batch is fine).

Reverts bubble up raw. A failing target's revert data is re-thrown verbatim, so what you see is the target's error, not a Manager error. Decode it against the target's ABI, not the Manager's:

execute cannot send native ETH. It is not payable and passes no value, so every call carries msg.value == 0. Native ETH held by the Manager cannot be moved through this function at all.

There is no reentrancy guard. execute makes raw calls with no lock. Batching calls to a target that can call back into the Manager has no protection.

The whitelist cannot include the vault or Yield contract. setWhitelistedAddresses rejects both. This is load-bearing: without it, execute could reach mintVaultToken and updateTotalAssets and forge share supply and vault accounting directly.

transferToVault — funding redemptions

The return leg. This is the function that makes withdrawals actually payable.

Two rough edges worth knowing:

  • It emits no event. Redemption funding is visible only as a plain ERC-20 Transfer into the vault. Do not build monitoring against a transferToVault event — there isn't one. (AssetsWithdrawn is declared in IManager but never emitted anywhere.)

  • It has no zero-amount check and no NatSpec — the only function in Manager missing both.

Anyone can also fund the vault with a plain ERC-20 transfer; the operator path is simply the intended one.

redeemFor — settling on a user's behalf

Bounded and safe: it pays only that user, only the amount already locked into their request. It cannot redirect funds or change amounts.

This path does not check the cooldown. It can settle a request before it has matured — paying the user their locked amount early. It cannot take anything from them, but it does end their position sooner than they planned.

Requires the vault to be funded, exactly like a user-initiated redemption.

Every guard on execute

Guard
Reverts with

Caller holds OPERATOR_ROLE

AccessControlUnauthorizedAccount

targets.length == data.length

LengthMismatch

Every target whitelisted

NotWhitelisted

Every call succeeds

the target's own revert data, bubbled raw

That is the complete list. Note what is absent: no amount limit, no destination-specific cap, no rate limit, no daily ceiling, no pause check.

The trust boundary

Operational discipline is the actual control here, not the contract. Practical guidance:

  • Simulate every batch, including ones identical to yesterday's.

  • Prefer exact approvals over infinite ones. execute can grant unlimited allowances to whitelisted targets; a compromised or upgradeable target with a standing infinite approval can drain the Manager without any further operator action.

  • Check the redemption backlog before deploying, not after. Deploying capital while matured redemptions sit unfunded strands users who have already given up their shares.

  • Reconcile deployed capital against reported P&L. BatchExecuted tells you what left; DistributeYield tells you what the strategy claims it earned. Nothing onchain connects the two — see Reporting P&L.

Last updated