Skip to content

cosmos/group-evm-demo

Repository files navigation

group-evm-demo

A runnable demo: a Cosmos x/group proposal executes an EVM call and moves an ERC20, with no EVM private key and no preset nonce.

It answers a specific question that comes up when a chain runs governance through x/group but holds value on the EVM side: how does a group decision move tokens that live in the EVM, when the group account has no Ethereum key and can't sign an EVM transaction?

What it shows

[1] group + policy created
[2] GroupSafe vault deployed for policy
[3] vault funded: 1000 TEST (ERC20)
[4] proposal: MsgExecuteEVM transfer 250 -> bob
[5] vote yes -> EXECUTED
vault:  750 TEST
bob:    250 TEST

The ERC20 moves because a group proposal passed. No account signs an EVM transaction, and no nonce is set anywhere.

Run it

Prerequisites: Go 1.25+, jq, python3, and Foundry (the script adds ~/.foundry/bin to PATH).

git clone https://github.com/cosmos/group-evm-demo
cd group-evm-demo
./demo.sh

The script builds the chain, starts a fresh single-node devnet, deploys the contracts, and runs the group vote end to end. It cleans up the node on exit.

How it works

The problem is a mismatch between two systems that don't normally talk this direction:

  • A group proposal executes by routing its inner messages through the message router, and it skips the ante handler on purpose.
  • The EVM's normal entry point (MsgEthereumTx) depends on the ante handler for signature, nonce, and fee checks, and it authenticates by recovering a 20-byte Ethereum key. A group policy account is a 32-byte account with no key.

So you can't put a raw EVM transaction inside a group proposal, and nothing in the stack offers a path from a group vote to an EVM call. This demo adds that path with two small pieces.

A per-policy vault contract (GroupSafe.sol). Each group policy gets a vault. The vault is the policy's EVM identity: it holds and receives ERC20s like any contract, so there is no bank-to-EVM balance mapping and no bridging. Its immutable MODULE is the owning policy's derived EVM address, so only that policy can drive it (onlyModule).

One message (MsgExecuteEVM). A group proposal carries it. When the proposal passes, the handler:

  1. Confirms the authority is a real group policy, so only a group vote reaches here. This is what stands in for the skipped ante check. The group's own rule, that every signer must equal the policy account, is the authorization.
  2. Derives the policy's EVM caller address from the authority account (the trailing 20 bytes of the account address).
  3. ABI-encodes vault.execute(to, data, value) for the vault named in the message.
  4. Calls the EVM on the internal path (CallEVMWithData) with from set to that per-policy address. Because it is the internal path, there is no signed transaction and no nonce to predict.
  5. The vault's onlyModule check passes only because that address equals the vault's immutable MODULE, which was set to the same derivation at deploy time. The vault runs the real call, for example an ERC20 transfer, moving its own tokens.

Isolation is enforced per-policy at the vault, not in module state. Because each vault's MODULE is its owning policy's derived caller, a proposal from any other policy calls from a different address and reverts at onlyModule. The vault can therefore travel in the message safely, and the module keeps no policy-to-vault table.

The module is a thin adapter, not a fork of x/group or of the EVM. Its whole job is to turn "the group approved this" into "the EVM ran this."

Where this belongs

In this repo the adapter is a standalone module (x/groupevm) so the repo builds on published cosmos/evm without patching it. For a production integration the cleaner home is x/vm itself, which already owns the EVM keeper and CallEVMWithData. Moving it there drops the separate module entirely; the message and handler are identical. The tradeoff is that it requires a change to cosmos/evm (a fork, ideally upstreamed).

Layout

app/           chain app (based on cosmos/evm's evmd), wires x/group + x/groupevm
cmd/evmd/      chain binary
x/groupevm/    the adapter module: MsgExecuteEVM + stateless handler
contracts/     GroupSafe.sol (the vault), MockERC20.sol
tools/         derives a policy's per-policy EVM address used as GroupSafe.MODULE
proto/         MsgExecuteEVM definition
demo.sh        the end-to-end script

Caveats

This is a demo, not production code.

  • No reentrancy guard on the inner call. Add one before executing untrusted targets.
  • Dependency license. The demo depends on github.com/cosmos/cosmos-sdk/enterprise/group, which is under the Cosmos Labs Source Available Evaluation License (non-commercial, non-production, no redistribution). That module is a separate, non-open-source dependency. The code in this repo is Apache-2.0.

License

Apache-2.0. See LICENSE. The enterprise/group dependency carries its own license, noted above.

About

Demo: a Cosmos x/group or x/gov proposal executes an EVM call and moves an ERC20, with no EVM key and no preset nonce

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors