You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
POST /submit-signed is the non-custodial core: the client
signs locally, Octo validates the signed envelope
(crates/api/src/submit_validation.rs), relays to
Horizon, and records history. The private key never touches the server. Deliver the EVM equivalent.
The validation layer matters as much as the relay. Octo's Stellar validator deliberately refuses to
be a blind signing oracle — it inspects the envelope and enforces policy. The EVM version must do
the same against RLP-encoded, EIP-1559 (type 2) transactions.
Requirements and context
Support EIP-1559 (type 2) as the default and legacy (type 0) for compatibility. Decode with a
strict RLP parser; reject unknown transaction types rather than guessing.
Recover the sender from the signature and verify it matches the authorised wallet. This is the
central check. Also verify the chainId in the signature matches the target chain — a
transaction signed for chain A must never be relayed to chain B
(EIP-155 replay protection exists precisely because
this was once possible).
Enforce policy on the decoded transaction, mirroring submit_validation.rs: destination
against the withdrawal allowlist
(0013_withdrawal_allowlist.sql),
token contract against the registry (feat(store): ERC-20 token registry #223), and amount limits. For an ERC-20 transfer this means decoding the calldata — transfer(address,uint256) selector 0xa9059cbb — because the
transaction's to is the token contract, not the recipient. Validating to alone would allow
transfers to any recipient.
The withdrawal-OTP flow in submit.rs must work identically
for EVM. Reuse it; do not fork it.
Generalise explain_code (Stellar result codes) into ChainAdapter::explain_failure, decoding
EVM revert reasons (Error(string) selector 0x08c379a0) into readable messages.
Security: never accept a raw pre-signed blob and relay it unvalidated. Never log the full
signed transaction. Enforce a body-size limit — RLP decoding untrusted input is an attack surface;
use a size cap and a recursion-depth-bounded parser.
Suggested execution
Branch: feat/evm-signed-tx-relay
Implement changes
Add crates/api/src/evm_submit_validation.rs: decode the signed transaction, recover the sender,
verify chain id and sender, decode ERC-20 calldata, and enforce allowlist/registry/limit policy.
Extend the submit route to dispatch on the wallet's chain_id, sharing the OTP, audit
(crates/api/src/audit.rs), and webhook paths with Stellar.
Implement explain_failure for EVM: revert-reason decoding plus common
pre-broadcast failures (nonce too low, replacement transaction underpriced, insufficient funds for gas * price + value).
Record the relayed transaction with chain_id, hash, and block, feeding the same ledger the
ingest worker writes to.
Test and commit
Test that a transaction signed for chain A is rejected when submitted to chain B.
Test that a transaction whose recovered sender is not the authorised wallet is rejected.
Test calldata decoding: an ERC-20 transfer to a non-allowlisted recipient is rejected even
though the transaction's to (the token contract) is registered. This is the test that proves
the validator is not fooled by the indirection.
Test revert-reason decoding produces a readable message.
Malformed-input tests in the style of crates/api/tests/malformed_body_tests.rs:
truncated RLP, oversized body, deeply nested RLP, unknown tx type — none may panic.
feat(api): EVM signed-transaction relay with policy validation
Mirrors the Stellar /submit-signed contract: the client signs locally
and Octo validates, relays, and records — never a blind signing oracle.
Validation recovers the sender from the signature, pins the EIP-155
chain id so a transaction signed for one chain cannot be relayed to
another, and decodes ERC-20 calldata so allowlist checks apply to the
actual recipient rather than to the token contract in `to`.
Refs #225
Guidelines
Security-sensitive. The calldata-decoding check is the piece reviewers will scrutinise — make it
obvious and well-commented.
Depends on: #218, #223. Blocks: #224, #226, #227.
Description
POST /submit-signedis the non-custodial core: the clientsigns locally, Octo validates the signed envelope
(
crates/api/src/submit_validation.rs), relays toHorizon, and records history. The private key never touches the server. Deliver the EVM equivalent.
The validation layer matters as much as the relay. Octo's Stellar validator deliberately refuses to
be a blind signing oracle — it inspects the envelope and enforces policy. The EVM version must do
the same against RLP-encoded, EIP-1559 (type 2) transactions.
Requirements and context
strict RLP parser; reject unknown transaction types rather than guessing.
central check. Also verify the
chainIdin the signature matches the target chain — atransaction signed for chain A must never be relayed to chain B
(EIP-155 replay protection exists precisely because
this was once possible).
submit_validation.rs: destinationagainst the withdrawal allowlist
(
0013_withdrawal_allowlist.sql),token contract against the registry (feat(store): ERC-20 token registry #223), and amount limits. For an ERC-20 transfer this means
decoding the calldata —
transfer(address,uint256)selector0xa9059cbb— because thetransaction's
tois the token contract, not the recipient. Validatingtoalone would allowtransfers to any recipient.
submit.rsmust work identicallyfor EVM. Reuse it; do not fork it.
explain_code(Stellar result codes) intoChainAdapter::explain_failure, decodingEVM revert reasons (
Error(string)selector0x08c379a0) into readable messages.signed transaction. Enforce a body-size limit — RLP decoding untrusted input is an attack surface;
use a size cap and a recursion-depth-bounded parser.
Suggested execution
Branch:
feat/evm-signed-tx-relayImplement changes
crates/api/src/evm_submit_validation.rs: decode the signed transaction, recover the sender,verify chain id and sender, decode ERC-20 calldata, and enforce allowlist/registry/limit policy.
chain_id, sharing the OTP, audit(
crates/api/src/audit.rs), and webhook paths with Stellar.explain_failurefor EVM: revert-reason decoding plus commonpre-broadcast failures (
nonce too low,replacement transaction underpriced,insufficient funds for gas * price + value).chain_id, hash, and block, feeding the same ledger theingest worker writes to.
Test and commit
transferto a non-allowlisted recipient is rejected eventhough the transaction's
to(the token contract) is registered. This is the test that provesthe validator is not fooled by the indirection.
crates/api/tests/malformed_body_tests.rs:truncated RLP, oversized body, deeply nested RLP, unknown tx type — none may panic.
docs/openapi.yaml,docs/api.md, anddocs/non-custodial-flow.md.Example commit message
Guidelines
Security-sensitive. The calldata-decoding check is the piece reviewers will scrutinise — make it
obvious and well-commented.