Skip to content

Optional pre-sign transaction guard: catch approval/Permit2 drains before the wallet signs #1360

Description

@Kevthetech143

Summary

Sharing a small, optional pre-sign safety hook in case it is useful to AgentKit users, and to get the team's read on whether it belongs as a documented pattern. Feedback very welcome — including "this belongs in a wallet layer, not here."

Disclosure: I maintain chain-signer, the library referenced below. This is an optional integration suggestion, not a dependency ask.

The gap

AgentKit's wallet providers screen recipients (sanctions / known-bad addresses), which is great. What they don't do today is decode the calldata of the transaction the agent is about to sign. Most agent fund-loss in 2026 hasn't come from sending to a sanctioned address — it has come from an agent being talked into signing an unlimited approve(), a setApprovalForAll(true), a Permit2 allowance, or the same drain hidden inside a multicall / Safe / ERC-4337 execute. A recipient check doesn't see any of that, because the malicious party is the spender in the calldata, not the to of the tx. (Write-up with reproduced 2026 incident shapes: https://gist.github.com/Kevthetech143/f4b940b2b31d457bd593c3d6df9f65c4)

A small, dependency-light option

chain-signer is a pip-installable, offline, no-API-key, no-network static decoder. preflight(tx) decodes an unsigned EVM tx and returns HIGH/MED/LOW flags for the known drain patterns (unlimited/large approval, increaseAllowance, setApprovalForAll, ERC-20 transferFrom, ERC-721/1155 safeTransferFrom, ERC-2612/DAI/Permit2 permits, proxy upgradeTo, EIP-7702 delegation, and drains nested in multicall / Multicall3 / Safe multiSend / 4337 execute). It's a static complement to recipient screening and to simulation — not a replacement — and it runs in-process with no external call.

Proposed shape (opt-in wrapper, verified against coinbase-agentkit==0.7.4)

from chain_signer import preflight
from web3.types import TxParams
from coinbase_agentkit.wallet_providers import EthAccountWalletProvider  # or CdpEvmWalletProvider

class PreflightBlocked(Exception): ...

def preflight_guard(tx: dict, *, on_block: str = "raise"):
    report = preflight(tx)                      # offline, no key, no network
    highs = [f for f in report.get("risk_flags", []) if f.get("severity") == "HIGH"]
    if highs and on_block == "raise":
        raise PreflightBlocked("blocked before signing: " + ", ".join(f["code"] for f in highs))
    return None if highs else tx

class GuardedEthAccountWalletProvider(EthAccountWalletProvider):
    def send_transaction(self, transaction: TxParams):
        preflight_guard(dict(transaction), on_block="raise")   # raises BEFORE sign + broadcast
        return super().send_transaction(transaction)

Behavior (real provider instantiated, send_transaction called on a malicious unlimited approve() — raised before any broadcast):

BLOCKED pre-broadcast: chain-signer blocked a HIGH-risk tx before signing: unlimited_approval

Off by default; zero behavior change unless a user opts in.

Honest limits (stated up front)

This is static decoding, so it won't catch a novel/obfuscated drain it can't decode (those get a LOW "unknown" flag, not a block); it's EVM-only; and it does not stop a redirected legitimate transfer (moving real funds to an attacker) — that's a recipient-allowlist / value-cap concern, which AgentKit's policy layer already addresses and which chain-signer's separate check_action gate also covers. Best used alongside recipient screening + simulation for high-value actions.

Happy to open a PR adding this as a documented optional example if the team thinks it fits, or to just leave it here as a pattern for anyone who wants it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions