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
- GitHub repository: stakekit/shield
- NPM package: @yieldxyz/shield
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.
| Mode | Behavior | Use case |
|---|---|---|
| Monitor | Validates 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. |
| Defense | Validates 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/shieldStandalone binary (any language)
Download the binary for your platform from the latest release:
| Platform | Binary |
|---|---|
| Linux x64 | shield-linux-x64 |
| macOS ARM (Apple Silicon) | shield-darwin-arm64 |
| macOS Intel | shield-darwin-x64 |
| Windows x64 | shield-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-arm64Every 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/shieldQuick 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 passesStandalone 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-arm64Response:
{
"ok": true,
"apiVersion": "1.0",
"result": {
"isValid": true,
"detectedType": "STAKE"
},
"meta": {
"requestHash": "a3f2c1..."
}
}
Library vs binary response shapesThe TypeScript library (
shield.validate()) returns the unwrappedresultobject 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:
- 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.
- Validates the target contract — Confirms
tx.tois 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. - 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). - 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.
- Enforces chain scope — Vault addresses are scoped to their chain using a composite
chainId:addresskey, 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
| Chain | Yield ID | Description | Transaction types |
|---|---|---|---|
| Ethereum | ethereum-eth-lido-staking | Lido ETH staking | STAKE, UNSTAKE, CLAIM_UNSTAKED |
| Ethereum | ethereum-eth-reth-staking | Rocket Pool ETH staking | STAKE, APPROVAL, SWAP |
| Solana | solana-sol-native-multivalidator-staking | Native SOL staking | STAKE, UNSTAKE, WITHDRAW, WITHDRAW_ALL, SPLIT |
| Tron | tron-trx-native-staking | TRX staking, resource management, and voting | See types below |
Tron transaction types:
| Category | Types |
|---|---|
| Resource management | FREEZE_BANDWIDTH, FREEZE_ENERGY, UNFREEZE_BANDWIDTH, UNFREEZE_ENERGY, UNDELEGATE_BANDWIDTH, UNDELEGATE_ENERGY |
| Legacy resource | UNFREEZE_LEGACY_BANDWIDTH, UNFREEZE_LEGACY_ENERGY |
| Governance | VOTE |
| Lifecycle | WITHDRAW, 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:
| Protocol | Vaults |
|---|---|
| Euler | 738 |
| Morpho | 442 |
| Yearn V3 | 176 |
| Curve | 38 |
| Fluid | 29 |
| Lista | 18 |
| SummerFi | 13 |
| Gearbox | 11 |
| Angle | 6 |
| Idle Finance | 5 |
| YO Protocol | 4 |
| Venus-Flux | 4 |
| Sky | 2 |
Validated transaction types (all 5 ERC-4626 operations):
| Type | What's validated |
|---|---|
| APPROVAL | approve() spender must be a whitelisted vault; token must be the vault's input token |
| SUPPLY | deposit() / mint() into a whitelisted vault; receiver must be the user |
| WITHDRAW | withdraw() / redeem() from a whitelisted vault; owner must be the user |
| WRAP | WETH.deposit() / WBNB.deposit() for native-token vaults |
| UNWRAP | WETH.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:addresscomposite 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:
| Protocol | Reason | Status |
|---|---|---|
| Aave (17 vaults) | Non-standard ERC-4626 implementation | Planned |
| Spark (9 vaults) | Overrides deposit() with referral-code variant | Planned |
| Maple (2 vaults) | Completely non-standard ABI (SyrupRouter, queue-based withdrawals) | Planned |
| Morpho claim rewards | Uses separate Merkle-proof-based distributor contract | Planned |
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)
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)
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'); // falseshield.getSupportedYieldIds()
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
| Context | Error | Meaning |
|---|---|---|
| Any EVM | Transaction validation failed: No matching operation pattern found | The transaction doesn't match any known-safe pattern. Likely malicious or corrupted. |
| ERC-4626 | Vault address not whitelisted | The transaction targets a contract address not in Shield's embedded registry. |
| ERC-4626 | Owner address does not match user address | A redeem() or withdraw() call has an owner that isn't the user — potential fund redirection. |
| ERC-4626 | Approval spender is not a whitelisted vault | An approve() call targets a spender that isn't a known vault address. |
| ERC-4626 | Transaction calldata has been tampered with | Re-encoded calldata doesn't match the original bytes — the transaction was modified. |
| ERC-4626 | Supply 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-4626 | Withdraw 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. |
| Lido | Transaction not to Lido stETH contract | The transaction targets an address that isn't the verified Lido contract. |
| Rocket Pool | Transaction not to RocketPool SwapRouter contract | The STAKE transaction targets a contract that isn't the verified Rocket Pool swap router. |
| Rocket Pool | SWAP receiver does not match user address | The rETH swap routes output to a different address — potential fund redirection. |
| Solana | Transfer recipient does not match new stake account | SOL is being sent to a different wallet instead of the intended stake account. |
| Solana | Delegate authority is not user address | Someone 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
| Version | Date | Highlights |
|---|---|---|
| v1.2.5 | March 2026 | Soak 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.4 | March 2026 | OAV allocator vault registry update, monitor mode integration in monorepo |
| v1.2.3 | March 2026 | Rocket Pool validator (STAKE, APPROVAL, SWAP) |
| v1.2.2 | March 2026 | Generic ERC-4626 validator — 13 protocols, 14 chains |
| v1.2.0 | February 2026 | Standalone binaries, language-agnostic JSON protocol, Go/Python/Rust examples |
| v1.0.1 | December 2025 | Initial release — Lido, Solana native staking, Tron native staking |
Next steps
- Defense Mode — Server-side enforcement for enterprise integrations
- GitHub repository — Source code, issues, and release artifacts
- NPM package — TypeScript library
Updated 16 days ago
