EVM policies

Build with -Pkeeper.features=evm -Pkeeper.platforms=ecc. Submit the unsigned transaction bytes as Base64 in artifact.message64.

Every transaction authority requires config.chainId. Legacy transactions use EIP-155 to bind signatures to that network. The signer rejects transactions with a missing or different chainId. Verification also requires the chain ID in the transaction.

transaction has type Transaction for native transfers and declared contract calls. Functions can read type, chainId, nonce, gasPrice, gasLimit, maxPriorityFeePerGas, maxFeePerGas, to, value, data and selector through this object. A fee field is null when it does not apply to the envelope. data has type Bytes. Values are wei.

Native transfers also expose the root inputs chainId, to, value, gasLimit and effects. Declare contract calls and ABI signatures in the Edict document:

authority "test:evm/mainnet-usdc-transfer" {
    intent = "evm.transaction"
    version = "1.0.0"
    title = "Mainnet USDC transfer limit"

    config {
        chainId = 1
    }

    contract token: evm {
        address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
        fn transfer(to: address, amount: uint256)
    }

    policy "test-evm-mainnet-usdc-transfer" {
        allow "allow-small-usdc-transfer" when {
            transaction.chainId == 1
            transaction.value == 0
            token.transfer.to == "0x1111111111111111111111111111111111111111"
            token.transfer.amount > 0
            token.transfer.amount <= 100000000
        }
    }
}

address and uint256 are EVM wire types. The decoder binds the destination, selector, and argument layout to this declaration. A call to another contract or method fails intent validation. transaction.chainId, transaction.value and transaction.gasLimit come from the native transaction. Native contract binding requires a chain id committed by the transaction. Use a chain-bound typed transaction such as EIP-1559.

The sample uses test addresses. Replace the contract and recipient with the intended deployed addresses. Token amounts are raw integers: for a six-decimal token, 100000000 means 100 tokens. transaction.value is the native value sent with the call, independently of the token amount.

For an allowance, declare fn approve(spender: address, amount: uint256) and constrain both fields. Declare the signature and local structs required for a vault call or tuple argument. Do not describe effects through a JSON contract catalog.

Authorizations and permits

The EVM feature also registers these signing intents:

Intent Artifact field Policy roots
evm.7702 authorization chainId, address, nonce
evm.2612 permit domain, owner, spender, value, nonce, deadline
evm.3009 authorization domain, operation, authorizer, nonce, transfer

evm.7702 signs the EIP-7702 authorization digest keccak256(0x05 || rlp([chainId, address, nonce])). Supply those three fields in artifact.authorization. address identifies the delegate contract. Setting address to zero revokes the delegation. The nonce must be between 0 and 2^64 - 2. A zero chain ID authorizes use across chains. An authority with config.chainId = 1 rejects it. Omit the chain pin or set it to zero only when the policy deliberately permits that scope.

authority "test:evm/delegate" {
    intent = "evm.7702"
    version = "1.0.0"

    config {
        chainId = 1
    }

    policy "delegate" {
        allow when {
            address == "0x1111111111111111111111111111111111111111"
            nonce == 7
        }
    }
}

For ERC-2612, supply domain, owner, spender, value, nonce and deadline in artifact.permit. The domain contains exactly name, version, chainId and verifyingContract. Signing uses that EIP-712 domain layout. Other domain layouts and DAI-style permits are rejected. Integer fields accept exact JSON integers or decimal strings.

authority "test:evm/permit" {
    intent = "evm.2612"
    version = "1.0.0"

    config {
        chainId = 1
    }

    policy "permit" {
        allow when {
            domain.name == "USD Coin"
            domain.version == "2"
            domain.verifyingContract == "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
            spender == "0x1111111111111111111111111111111111111111"
            value <= 100000000
            deadline <= 1800000000
        }
    }
}

ERC-3009 uses the same domain layout. Set operation to transferWithAuthorization, receiveWithAuthorization or cancelAuthorization inside artifact.authorization. Transfer and receive require from, to, value, validAfter, validBefore and a 32-byte hexadecimal nonce. Cancellation requires authorizer and nonce. Each operation uses its own EIP-712 type hash.

In policy facts, authorizer is the signed from or authorizer address. transfer contains from, to, value, validAfter and validBefore for transfer and receive. It is null for cancellation. Guard access with transfer != null and constrain operation explicitly. Policy addresses use lowercase. The nonce has type Bytes. Compare it with a literal such as b"0000000000000000000000000000000000000000000000000000000000000001".

authority "test:evm/receive" {
    intent = "evm.3009"
    version = "1.0.0"

    config {
        chainId = 1
    }

    policy "receive" {
        allow when {
            operation == "receiveWithAuthorization"
            domain.name == "USD Coin"
            domain.version == "2"
            domain.verifyingContract == "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
            transfer != null
            transfer.to == "0x1111111111111111111111111111111111111111"
            transfer.value <= 100000000
            transfer.validBefore <= 1800000000
        }
    }
}

Keeper requires secp256k1 and verifies that owner, from or authorizer matches the requested signing key, including its tweak. config.chainId, when present, must match the signed chain ID exactly. Policies must pin the token domain and the delegate or recipient. Add amount and validity bounds appropriate to the operation. Chain state determines whether a nonce remains usable and whether a deadline has expired.

Use signature().sign(...) with the SDK artifacts Evm7702Authorization, Evm2612Permit or Evm3009Authorization. The returned recoverable ECDSA signature signs the protocol digest directly. Submit the signature through the corresponding authorization tuple or contract method. Composing an outer transaction requires a separate transaction intent.

Compose

var command = Command.of("test:evm/mainnet-usdc-transfer", new UnsignedEvmTransaction(message64));
var result = client.signature().compose(Sign.of(keyId, command), SignedEvmTransaction.class);

The composer returns signed transaction hex and its hash. It supports legacy and typed envelopes supported by Signet. Blob broadcasting still needs its sidecar. The wallet supplies nonces and fees. After approval, it broadcasts the bytes and tracks settlement.

See Edict for rules and Authorities for OCI publication.