Hardening the XRPL EVM Node
A detailed look at the AI-assisted security audit behind XRPL EVM v11: 24 findings across validator management, IBC, module wiring, and the CI/CD pipeline, all fixed in 22 pull requests that shipped in node v11.0.0.

Hardening the XRPL EVM Node: Inside Our AI-Assisted Security Audit
A look at how we used AI to stress-test the XRPL EVM node, what it found, and the pull requests we opened in response.
When we shipped XRPL EVM v11 in July, we grouped its security fixes by area and held back the specifics, in line with coordinated disclosure. With v11 now running on the network, this post is the detailed write-up we promised.
Every fix described below shipped in node v11.0.0. They came out of the AI-assisted audit process described in the v11 announcement: a written scope and threat model, one AI reviewer per area of the codebase, and human triage before any fix lands. That process covered not just the chain's Go code but also module wiring, dependencies, and the CI/CD pipeline that builds the node, and the findings reflect that breadth.
The numbers
The audit produced 24 accepted findings with merged fixes across the xrplevm/node repository, with the following severity breakdown:
| Severity | Count |
|---|---|
| HIGH | 5 |
| MEDIUM | 9 |
| LOW | 7 |
| INFO | 3 |
Every finding went through human triage, and each of these is addressed by one of 22 merged pull requests on xrplevm/node. They are linked inline below and indexed at the bottom of this post for anyone who wants to read the diffs directly.
Rather than walk through every finding one by one, the rest of this post groups them by theme, because the patterns are more interesting than any single bug.
Theme 1: Tightening the validator set and the IBC perimeter
The findings with the highest blast radius all sit at the same boundary: the rules that decide who can become a validator and what can cross IBC.
Closing a gap in outbound IBC rate limiting. Rate limits on IBC transfers existed, but only for inbound packets. Outbound transfers went straight from the transfer keeper to the channel keeper, completely skipping the rate-limit middleware. We rewired the transfer stack so outbound packets now flow through transfer -> rateLimit -> channel, giving the chain symmetric protection in both directions. (PR #116)
Disabling the ICA host module. The Interchain Accounts host module was running with a wildcard AllowMessages, meaning any counterparty chain could ask the host to execute arbitrary messages. We wrote a v11.0.0 upgrade handler that disables the ICA host outright, removing that surface area until we have a concrete need and a tightly scoped allow list. (PR #127)
Defending the closed PoA validator set. The XRPL EVM uses Proof-of-Authority: validators are added exclusively through the x/poa module. The standard Cosmos MsgCreateValidator was unsatisfiable in practice (no user account holds bond-denom), but it had no explicit guard. We added a defense-in-depth ante check that blocks MsgCreateValidator post-genesis, with a height-zero carve-out so genesis bootstrapping still works as expected. We also patched a related bug in ExecuteAddValidator where an unbonding-delegation safety check was looking at the candidate as a validator (where they had no history) instead of as a delegator (where the actual records live). (PR #125, PR #126)
Theme 2: Module wiring and silent fallbacks
Another cluster of findings was less dramatic but no less important: the chain was technically functional, but several keepers and configuration paths were quietly initialized with the wrong values.
A precompile holding a zero-value keeper. The EVM gov precompile was constructed by passing app.GovKeeper by value before the keeper had been initialized. Because the precompile then took the address of its local copy, it permanently held a pointer to a keeper with nil internals. Reordering the keeper construction in app/app.go so GovKeeper is built before the EVM keeper restored the precompile to a working state. (PR #115)
Honest defaults instead of silent fallbacks. The chain ID parser silently fell back to a hardcoded 9999 whenever it encountered a malformed value, masking misconfiguration at startup. The keyring backend default was forced to test (unencrypted JSON files), so any operator who did not explicitly set --keyring-backend=os was using the insecure backend. The interface registry constructor's error was being thrown away. Each of these is now wired to fail loudly: errors surface, defaults are honest, and the node refuses to start in a bad state instead of papering over it. (PR #121, PR #120, PR #133)
Plumbing the rest of the keepers correctly. A small set of follow-on fixes registered the x/poa params subspace, removed a duplicate erc20 entry in the genesis module order that would have run the module's genesis initialization twice on a fresh chain, and removed a Cosmos SDK test gRPC service that had been mistakenly registered in the production app. We also added the missing StoreUpgrades switch cases for the v7-v10 upgrades. (PR #138, PR #118, PR #140, PR #129)
Theme 3: CI/CD as a security control
Modern node software is only as trustworthy as the pipeline that builds and ships it. A meaningful portion of the audit's findings, including several of the highest-severity ones, lived not in Go code but in our GitHub Actions workflows and Dockerfiles.
We consolidated the bulk of the CI hardening into a single PR (#117), which:
- Adds a top-level
permissions: {}(deny-all) block to every workflow and grants per-job least-privilege scopes, so a compromised step cannot reach beyond the resources it actually needs. - Pins every third-party action to a full commit SHA with a trailing version comment. Floating
@v3tags can be overwritten upstream, and pinning to immutable SHAs neutralizes that class of supply-chain attack. - Removes an SSH key that had been written to runner disk and copied into the released Docker image as part of an old workaround for a private Go module that is no longer in use.
On top of that, we:
- Pinned the Go base image in our Dockerfile to a
sha256digest, so a tag re-push on Docker Hub cannot silently change what we ship. (PR #119) - Hardened a release workflow that was interpolating user-controlled
workflow_dispatchinput directly into a shellrun:block, by moving the value into anenv:variable so the shell treats it as data, not script. (PR #123) - Retired our long-stale Cosmovisor tooling and its Docker image rather than continue publishing an outdated artifact. (PR #135)
Theme 4: Dependency and test hygiene
A few smaller but worthwhile cleanups:
ibc-gowas pinned to a development pseudo-version. Upstream had since published a stable tag (v10.4.0) for the same commit, so we swapped the label. (PR #122)cosmossdk.io/corewas being silently downgraded by areplacedirective (the manifest asked forv0.12.0, which is retracted upstream and incompatible with the SDK fork we use). We aligned the requirement with the version actually being resolved. (PR #134)- We migrated off the archived
github.com/golang/mock(no longer maintained) to its community-supported successorgo.uber.org/mock. (PR #130) - A shared test fixture in the PoA suite always returned success from staking hooks, so the error-handling path of validator removal was never exercised. Each test case now sets up its own expectations, including the hook-error cases. (PR #142)
What we took away
A few lessons from this round are worth carrying forward:
- Wiring order is a security property. Two of the high-severity findings came from keeper wiring: one keeper was copied before it had been initialized, and another was connected to the wrong layer of the IBC stack. Building keepers before they are passed along, and checking what each one is wired to, prevents this kind of mistake.
- Honest failure beats silent fallback. Magic defaults like a hardcoded
9999chain ID or atestkeyring backend make life easier for the operator who is using them correctly, and dangerous for the one who is not. Failing fast at startup is the kinder option for both. - Defense-in-depth at the ante layer is cheap insurance. Several of the additions are guards against situations that "cannot happen today." The cost is a few lines on the hot path; the upside is that a future regression elsewhere does not silently become an exploit here.
- CI is part of the chain's trust boundary. Permissions blocks, SHA-pinned actions, pinned base images and no secrets on disk each reduce the chance that a compromised dependency or workflow reaches what we ship.
- AI-assisted review is a force multiplier, not a replacement. The audit was particularly effective at the "thousand-paper-cuts" problems (dependency drift, missing tests, configuration defaults, wiring order) where traditional reviews can lose focus. It also raised findings that turned out not to apply on closer inspection, which is why every one went through human triage. It complements human auditors and our own engineering review; it does not replace them.
The XRPL EVM node will continue to receive this kind of attention as the network grows. If you want to follow along with the work in real time, the repository is open at github.com/xrplevm/node, and every change referenced in this post links to its pull request.
Appendix: Pull Request Index
| PR | Severity | Summary |
|---|---|---|
| #115 | HIGH | Pass initialized GovKeeper to gov precompile instead of zero-value |
| #116 | HIGH | Enforce rate limit on outbound IBC transfers |
| #117 | HIGH | Harden GitHub Actions workflows (permissions, SHA pins, SSH key removal) |
| #118 | MEDIUM | Remove duplicate erc20types entry from genesis module order |
| #119 | MEDIUM | Pin Dockerfile base image to SHA256 digest |
| #120 | MEDIUM | Remove keyring-backend=test default override |
| #121 | MEDIUM | Replace hardcoded EVM chain ID fallback with proper error handling |
| #122 | MEDIUM | Replace ibc-go pseudo-version with v10.4.0 tag |
| #123 | MEDIUM | Harden TAG workflow input to prevent shell injection |
| #125 | MEDIUM | Block MsgCreateValidator post-genesis in PoA ante decorator |
| #126 | MEDIUM | Check unbonding delegations from delegator perspective in ExecuteAddValidator |
| #127 | MEDIUM | Add v11 upgrade handler disabling ICA host module |
| #129 | LOW | Include v7-v10 upgrades in store upgrades switch |
| #130 | LOW | Migrate from archived golang/mock to go.uber.org/mock |
| #133 | LOW | Handle interface registry creation errors |
| #134 | LOW | Align cosmossdk.io/core requirement with resolved version |
| #135 | LOW | Remove stale Cosmovisor tooling |
| #137 | INFO | Remove unused PoA GovKeeper dependency |
| #138 | LOW | Register PoA params subspace |
| #139 | INFO | Remove stale staking_tokens attribute from PoA add_validator event |
| #140 | INFO | Remove SDK test gRPC service from production app |
| #142 | LOW | Cover staking-hook error handling in PoA MsgServer remove |
More news

XRPL EVM: Preventive Response to Cosmos EVM incident
A preventive security response to Cosmos EVM vulnerabilities, ensuring XRPL EVM remained secure and fully protected. No exploitation or loss of funds occurred.

XRPL EVM v11: Stronger Security, Safer Cross-Chain Connectivity
XRPL EVM Sidechain v11 is a security-focused upgrade that strengthens economic security, hardens IBC transfers, reduces attack surface, improves PoA validator management, and reinforces the network’s foundation for continued ecosystem growth.