Shield

Zero-trust transaction validation for Yield.xyz integrations.

Overview

When interacting with on-chain yield protocols through the Yield API, transaction integrity is critical. Even a small modification to an unsigned transaction can redirect funds to an attacker's address, trigger unintended contract calls, or drain token approvals.

Shield is a validation layer that lets you verify every unsigned transaction returned by the Yield API before presenting it for signing. It decodes each transaction into a typed structure — ABI decoding for EVM, instruction parsing for Solana, and TronWeb decoding for Tron — validates each decoded field against a verified template for the specified yield integration, and re-encodes EVM calldata to detect any byte-level tampering. If anything deviates from the expected pattern, Shield flags it immediately.

Shield ships as both a TypeScript library and standalone binaries, so you can embed it directly into your integration regardless of tech stack.

📘

Resources


Key features

  • Zero-trust verification — Every transaction must match a pre-audited pattern. Shield trusts nothing by default.
  • Multi-chain support — Works across EVM (14 networks), Solana, and Tron. Solana DeFi protocol validators, Perps API transaction coverage, and additional EVM protocol support are in development.
  • 1,486 vault coverage — Generic ERC-4626 validator covers 98% of all ERC-4626 yields the API supports, plus dedicated validators for Lido and Rocket Pool.
  • Standalone binaries — Pre-built for Linux, macOS (ARM + Intel), and Windows. No Node.js required.
  • Language-agnostic — JSON protocol over stdin/stdout with integration guides for Go, Python, and Rust.
  • Tamper detection — Byte-for-byte calldata re-encoding catches any modification to EVM transactions, including appended or corrupted data.
  • Clear error reporting — Immediate, structured feedback on exactly why a transaction failed validation.

Modes

Shield supports two operating modes. Your choice depends on where you are in your integration lifecycle.

ModeBehaviorUse case
MonitorValidates every transaction and logs the result, but never blocks. Invalid transactions produce warnings.Initial rollout. Observe Shield's behavior against real production traffic without risk of false-positive disruption.
DefenseValidates every transaction and blocks any that fail. Invalid transactions are rejected before they reach the signing flow.Production hardening. Active protection against tampering, with server-side enforcement. See Defense Mode for full details.

You can run Shield client-side in either mode. For server-side enforcement in Defense Mode, see the dedicated Defense Mode documentation.


Installation

TypeScript / Node.js

npm install @yieldxyz/shield

Standalone binary (any language)

Download the binary for your platform from the latest release:

PlatformBinary
Linux x64shield-linux-x64
macOS ARM (Apple Silicon)shield-darwin-arm64
macOS Intelshield-darwin-x64
Windows x64shield-windows-x64.exe
# Download
curl -LO https://github.com/stakekit/shield/releases/latest/download/shield-darwin-arm64

# Verify integrity
curl -LO https://github.com/stakekit/shield/releases/latest/download/shield-darwin-arm64.sha256
shasum -a 256 -c shield-darwin-arm64.sha256  # Expected output: OK

# Make executable
chmod +x shield-darwin-arm64

Every release includes SHA-256 checksums and GitHub artifact attestations — cryptographic proof that each binary was built by our CI pipeline, not modified after the fact.

You can verify the attestation using the GitHub CLI:

gh attestation verify shield-darwin-arm64 --repo stakekit/shield

Quick start

TypeScript

import { Shield } from '@yieldxyz/shield';

const shield = new Shield();

// 1. Get transaction from Yield API
const response = await fetch('https://api.yield.xyz/v1/actions/enter', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': process.env.YIELD_API_KEY,
  },
  body: JSON.stringify({
    yieldId: 'ethereum-eth-lido-staking',
    address: userWalletAddress,
    arguments: { amount: '0.01' },
  }),
});

const action = await response.json();

// 2. Validate each transaction before signing
for (const tx of action.transactions) {
  const result = shield.validate({
    unsignedTransaction: tx.unsignedTransaction,
    yieldId: action.yieldId,
    userAddress: userWalletAddress,
  });

  if (!result.isValid) {
    throw new Error(`Invalid transaction: ${result.reason}`);
  }
}

// 3. Proceed with signing only after validation passes

Standalone binary (any language)

Shield accepts JSON on stdin and returns JSON on stdout. Every request requires an operation field:

echo '{
  "apiVersion": "1.0",
  "operation": "validate",
  "unsignedTransaction": "0x...",
  "yieldId": "ethereum-eth-lido-staking",
  "userAddress": "0x..."
}' | ./shield-darwin-arm64

Response:

{
  "ok": true,
  "apiVersion": "1.0",
  "result": {
    "isValid": true,
    "detectedType": "STAKE"
  },
  "meta": {
    "requestHash": "a3f2c1..."
  }
}
📘

Library vs binary response shapes

The TypeScript library (shield.validate()) returns the unwrapped result object directly. The CLI/binary protocol wraps the result in { ok, apiVersion, result, meta } so non-TypeScript integrations can correlate requests and handle protocol-level errors separately from validation failures.

See the full integration guides for Go, Python, and Rust.


How it works

Shield decodes each transaction into a typed structure, validates every decoded field against a verified template for the specified yield integration, and re-encodes EVM calldata to detect byte-level tampering.

For every transaction, Shield:

  1. Identifies the transaction type — Auto-detects whether the transaction is a deposit, withdrawal, approval, wrap, unwrap, stake, swap, or claim based on the calldata's function selector and the target contract.
  2. Validates the target contract — Confirms tx.to is a whitelisted address. For ERC-4626, this is checked against an embedded registry of 1,486 vaults. For Lido and Rocket Pool, addresses are hardcoded.
  3. Decodes and inspects calldata — Parses function parameters and verifies that the receiver, owner, and spender addresses match the expected values. The receiver must be the user's address (or an explicitly approved receiver via args.receiverAddress).
  4. Detects tampering — Re-encodes the decoded EVM calldata and compares byte-for-byte against the original. Any discrepancy (appended bytes, modified parameters, corrupted data) is flagged.
  5. Enforces chain scope — Vault addresses are scoped to their chain using a composite chainId:address key, preventing cross-chain confusion attacks.

If the transaction doesn't match the safe pattern — for example, if a withdrawal's owner doesn't match the user's address — Shield immediately returns a structured error:

{
  "isValid": false,
  "reason": "Owner address does not match user address",
  "detectedType": "WITHDRAW"
}

You decide how to handle the result: block the signing flow, display a warning, or log it for review.


Coverage

Dedicated validators

ChainYield IDDescriptionTransaction types
Ethereumethereum-eth-lido-stakingLido ETH stakingSTAKE, UNSTAKE, CLAIM_UNSTAKED
Ethereumethereum-eth-reth-stakingRocket Pool ETH stakingSTAKE, APPROVAL, SWAP
Solanasolana-sol-native-multivalidator-stakingNative SOL stakingSTAKE, UNSTAKE, WITHDRAW, WITHDRAW_ALL, SPLIT
Trontron-trx-native-stakingTRX staking, resource management, and votingSee types below

Tron transaction types:

CategoryTypes
Resource managementFREEZE_BANDWIDTH, FREEZE_ENERGY, UNFREEZE_BANDWIDTH, UNFREEZE_ENERGY, UNDELEGATE_BANDWIDTH, UNDELEGATE_ENERGY
Legacy resourceUNFREEZE_LEGACY_BANDWIDTH, UNFREEZE_LEGACY_ENERGY
GovernanceVOTE
LifecycleWITHDRAW, CLAIM_REWARDS

Generic ERC-4626 validator

The ERC-4626 validator covers 1,486 vaults across 13 protocols — 98% of all ERC-4626 yields the API supports. The vault registry is embedded in the Shield binary as a whitelist.

Covered protocols:

ProtocolVaults
Euler738
Morpho442
Yearn V3176
Curve38
Fluid29
Lista18
SummerFi13
Gearbox11
Angle6
Idle Finance5
YO Protocol4
Venus-Flux4
Sky2

Validated transaction types (all 5 ERC-4626 operations):

TypeWhat's validated
APPROVALapprove() spender must be a whitelisted vault; token must be the vault's input token
SUPPLYdeposit() / mint() into a whitelisted vault; receiver must be the user
WITHDRAWwithdraw() / redeem() from a whitelisted vault; owner must be the user
WRAPWETH.deposit() / WBNB.deposit() for native-token vaults
UNWRAPWETH.withdraw() / WBNB.withdraw() post-withdrawal

Additional checks applied to all types:

  • Calldata tamper detection (re-encode and byte-compare)
  • Receiver/owner === userAddress enforcement
  • Chain-scoped vault whitelist (chainId:address composite key)
  • Allocator vault (OAV) address whitelisting — 82 vaults with 195 allocator addresses

Supported EVM chains: Ethereum, Arbitrum, Base, Optimism, Polygon, Avalanche, BSC, Sonic, Unichain, Linea, Monad, Plasma, Katana, HyperEVM

What's not covered yet

These protocols require dedicated validators due to non-standard transaction flows:

ProtocolReasonStatus
Aave (17 vaults)Non-standard ERC-4626 implementationPlanned
Spark (9 vaults)Overrides deposit() with referral-code variantPlanned
Maple (2 vaults)Completely non-standard ABI (SyrupRouter, queue-based withdrawals)Planned
Morpho claim rewardsUses separate Merkle-proof-based distributor contractPlanned

When using the Shield library directly, these yields return { isValid: false } — fail-safe by default. When Shield is integrated server-side via the Yield API, unsupported yields are currently skipped (not blocked) to avoid disrupting yields that don't yet have validators.

Check coverage programmatically

// Check if a specific yield is supported
shield.isSupported('ethereum-eth-lido-staking'); // true

// Get all supported yield IDs
shield.getSupportedYieldIds();
// ['ethereum-eth-lido-staking', 'solana-sol-native-multivalidator-staking', ...]

API reference

shield.validate(request)

Validates a transaction by auto-detecting its type.

Parameters

{
  unsignedTransaction: string;    // The unsigned transaction from the Yield API
  yieldId: string;                // Yield integration ID (e.g. 'ethereum-eth-lido-staking')
  userAddress: string;            // User's wallet address
  args?: ActionArguments;         // Optional: original action arguments
                                  //   - args.receiverAddress overrides the default
                                  //     "receiver must equal userAddress" check
                                  //     (custodial flows)
  context?: ValidationContext;    // Optional: additional validation context
}

Returns

{
  isValid: boolean;          // Whether the transaction passed validation
  reason?: string;           // Why validation failed (human-readable)
  details?: object;          // Additional error context (attempted types, diagnostics)
  detectedType?: string;     // Auto-detected transaction type (STAKE, SUPPLY, APPROVAL, etc.)
}

The args.receiverAddress parameter

By default, Shield requires that the decoded receiver in the transaction calldata matches userAddress. This covers the standard case where a user is depositing or withdrawing to their own wallet.

For custodial flows — where a service deposits on behalf of a user and the receiver is a different address — pass args.receiverAddress to declare intent:

const result = shield.validate({
  unsignedTransaction: tx.unsignedTransaction,
  yieldId: action.yieldId,
  userAddress: serviceWalletAddress,
  args: {
    receiverAddress: userCustodialAddress, // Receiver differs from signer
  },
});

Shield then validates that the decoded receiver matches args.receiverAddress instead of userAddress. If the calldata's receiver doesn't match either, the transaction is blocked — this catches tampering even in custodial flows.

shield.isSupported(yieldId)

Returns true if Shield has a validator for the given yield integration.

shield.isSupported('ethereum-eth-lido-staking'); // true
shield.isSupported('unsupported-yield');          // false

shield.getSupportedYieldIds()

Returns an array of all yield IDs that Shield can validate.

const ids = shield.getSupportedYieldIds();
// ['ethereum-eth-lido-staking', 'ethereum-eth-reth-staking', ...]

Common validation errors

ContextErrorMeaning
Any EVMTransaction validation failed: No matching operation pattern foundThe transaction doesn't match any known-safe pattern. Likely malicious or corrupted.
ERC-4626Vault address not whitelistedThe transaction targets a contract address not in Shield's embedded registry.
ERC-4626Owner address does not match user addressA redeem() or withdraw() call has an owner that isn't the user — potential fund redirection.
ERC-4626Approval spender is not a whitelisted vaultAn approve() call targets a spender that isn't a known vault address.
ERC-4626Transaction calldata has been tampered withRe-encoded calldata doesn't match the original bytes — the transaction was modified.
ERC-4626Supply amount is zero (surfaced as No matching operation pattern found)A deposit() or mint() call where the decoded amount is zero. Typically means the requested amount was too small and rounded to zero. Validate that amounts are non-trivial before calling the API.
ERC-4626Withdraw amount is zero (surfaced as No matching operation pattern found)A withdraw() or redeem() call where the decoded amount is zero. Same root cause as above — the amount rounded down to zero.
LidoTransaction not to Lido stETH contractThe transaction targets an address that isn't the verified Lido contract.
Rocket PoolTransaction not to RocketPool SwapRouter contractThe STAKE transaction targets a contract that isn't the verified Rocket Pool swap router.
Rocket PoolSWAP receiver does not match user addressThe rETH swap routes output to a different address — potential fund redirection.
SolanaTransfer recipient does not match new stake accountSOL is being sent to a different wallet instead of the intended stake account.
SolanaDelegate authority is not user addressSomeone else would gain control over the user's staked SOL.

Why Shield matters

Each API layer in a DeFi integration increases potential attack surfaces. In a recent infrastructure compromise, an attacker modified API responses to redirect deposit transactions to their own address — the transactions were structurally valid ERC-4626 calls that executed successfully on-chain, but the receiver was changed.

Shield enforces a zero-trust model: it doesn't care whether the API, the network, or any intermediate layer claims the transaction is safe. It independently verifies that the transaction the user is about to sign is exactly the one intended by the Yield API — correct contract, correct function, correct parameters, correct receiver.

By validating before signing, you:

  • Prevent tampering — Fund redirection, approval hijacking, and calldata manipulation are caught before signing
  • Fail safe — Unsupported or unrecognized transactions are blocked, not approved
  • Maintain user trust — Transparent, verifiable security without sacrificing UX
  • Add structural security — Defense in depth that works even if other layers are compromised

Releases

VersionDateHighlights
v1.2.5March 2026Soak fixes — args.receiverAddress for custodial flows; zero-amount ERC-20 approval support (USDT reset pattern); vault registry re-export (1,486 covered vaults); BSC WBNB isWethVault patch; OAV allocator vaults for new partner deployments
v1.2.4March 2026OAV allocator vault registry update, monitor mode integration in monorepo
v1.2.3March 2026Rocket Pool validator (STAKE, APPROVAL, SWAP)
v1.2.2March 2026Generic ERC-4626 validator — 13 protocols, 14 chains
v1.2.0February 2026Standalone binaries, language-agnostic JSON protocol, Go/Python/Rust examples
v1.0.1December 2025Initial release — Lido, Solana native staking, Tron native staking

Next steps