In Railnet smart contracts, an Advanced Strategy is implemented as a Specialized Vehicle. The Policy Engine is a Zodiac Roles Modifier v2 contract enabled as a module on the Strategy’s Safe wallet.
supply, swap, mint, whatever) and that you already have a production signer. What you don’t yet know is the one extra step that routes your calldata through the Strategy’s Policy Engine so your mandate is enforced for you on-chain.
Good news for the PM: your current execution engine keeps doing everything it already does. You add one outer encode step and change the to address. That’s the whole integration.
Good news for the dev: it’s one ABI fragment, one function call, six args.
TL;DR
Mental model
- The Advanced Strategy is a Safe. It holds the capital. Shortened to “the Strategy” for the rest of this guide.
- The Policy Engine is a Zodiac Roles Modifier v2 contract, enabled as a module on the Strategy. It is a separate address from the Strategy — the administrator gives you both.
- Your member signer (EOA, hardware wallet, Fireblocks vault, Safe, KMS key, anything) is registered against a role key on the Policy Engine. That role defines exactly which functions, on which targets, with which argument shapes, you are allowed to push through.
msg.sender == STRATEGY_ADDRESS. No allowlist update, no integration change, nothing about your signer is visible to the target contract.
What the administrator gives you
| Name | Type | What it is |
|---|---|---|
STRATEGY_ADDRESS | address | The Safe that holds the capital and executes the protocol call |
POLICY_ENGINE_ADDRESS | address | The Roles Modifier v2 contract enabled as a module on the Strategy |
ROLE_KEY | bytes32 | Identifier for your member role on the Policy Engine |
STRATEGY_ADDRESS and POLICY_ENGINE_ADDRESS — ask the admin for both.
The role key
Two encodings are in the wild: Numeric index — auint left-padded to 32 bytes. Simplest single-role deployments use this.
"aave_usdc":
bytes32; you pass it through. If it’s wrong, the Policy Engine reverts with NoMembership.
The only ABI you need
This is the one contract surface you must paste into your ABI registry. Everything else about the Policy Engine is invisible to the execution path.to— the protocol target (Aave Pool, Uniswap Router, USDC token, …), not the Strategy.value— ETH to forward. Almost always0.data— your already-crafted inner calldata, untouched.operation— always0(CALL). Never1(DELEGATECALL); that path is reserved for internal tooling and has no valid use from an operator member.roleKey— thebytes32from the section above.shouldRevert— set totrue. Makes inner failures revert the whole transaction instead of silently returningfalse, so your ops pipeline sees real errors.
The flow in pseudocode
Language-agnostic. Steps 1, 3, and 4 are what you’re already doing for direct protocol calls today; step 2 is the entire integration.
That’s the contract: encode one extra layer, change the
to address, everything else stays.
Reference implementation
Generic version using TypeScript and viem. Replace the two marked variables with whatever your existing protocol-crafting code produces.privateKeyToAccount for your signer’s viem-compatible account object — Fireblocks, Turnkey, AWS KMS, Ledger Connect, Dynamic, Privy, and most others ship one. The rest of the snippet is identical regardless of backend.
Any signer, same bytes
The Policy Engine does not care how a transaction was signed, only that thefrom address is a registered member of the role. Anything that can sign a standard EIP-1559 contract call to POLICY_ENGINE_ADDRESS works out of the box.
Hot wallet / EOA
Hot wallet / EOA
As shown in the reference implementation above. Sign and broadcast directly with a private key using viem, ethers, or any web3 library.
Hardware wallet (Ledger, Trezor, GridPlus)
Hardware wallet (Ledger, Trezor, GridPlus)
User approves on device. The outer transaction is a plain contract call, so the device shows
to = Policy Engine, function = execTransactionWithRole.Institutional custody (Fireblocks, BitGo, Copper, Anchorage, Qredo)
Institutional custody (Fireblocks, BitGo, Copper, Anchorage, Qredo)
Create a contract-call transaction in the provider’s SDK with
to = POLICY_ENGINE_ADDRESS, data = wrapped_data. No custom integration on the provider side.Cloud KMS / MPC (AWS KMS, GCP KMS, Turnkey, Lit)
Cloud KMS / MPC (AWS KMS, GCP KMS, Turnkey, Lit)
Sign the raw transaction bytes exactly as you would any other contract call. The Policy Engine sees a standard
from address — it does not inspect the signing backend.A Safe multisig as the member
A Safe multisig as the member
Covered in the next section because it has two extra steps.
When the member signer is a Safe
Some teams register a dedicated Safe as the Policy Engine member so signing authority is already multi-party before any transaction touches the Strategy. The flow adds two steps around what you already built above.- Build
wrapped_dataexactly as in step 2 of the pseudocode flow (targeting the Policy Engine). - Wrap it a second time as a Safe transaction: a SafeTx with
to = POLICY_ENGINE_ADDRESS,data = wrapped_data,value = 0,operation = 0. - Collect owner signatures on the SafeTx hash via your existing Safe flow — Safe Transaction Service, in-house queue, whatever you already run.
- Execute the SafeTx. The on-chain path becomes:
@safe-global/protocol-kit (the rest — proposal, confirmation collection, execution — is your existing Safe infra):
What to read next
Policy engine
Understand how the on-chain policy engine governs what operators can do.
Advanced Strategies
Overview of Specialized Vehicles — custody, policy, and accounting.
Risk management
Risk frameworks and guardrails for strategy operations.