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
This is the deepest conceptual gap in the epic. Octo's deposit model
(docs/deposit-model.md) gives every customer a muxed address: one base
account plus a 64-bit id, so a deposit lands in a single account already tagged with the customer
id — no new on-chain account, no reserve, no sweep. Store::allocate_address just bumps next_muxed_id in a transaction.
EVM has no such mechanism. Each customer needs a real, distinct, HD-derived EOA, which changes
the economics and the risk profile:
Sweeping costs gas at the deposit address, which holds no native token — so the sweeper must
fund it first, or use a smart-contract forwarder.
The address's private key must be derivable to sweep — so the server does hold keys that can
move customer funds, a departure from the non-custodial posture in 0012_client_custody.sql. AD-4 permits this
narrowly; it must be documented, bounded, and understood.
Implement EVM deposit-address allocation, preserving the atomicity guarantee the Stellar path has.
Requirements and context
Atomicity is non-negotiable. Two concurrent allocations must never receive the same
derivation index. Reuse the existing transaction + row-lock pattern in allocate_address.
Evaluate CREATE2 forwarders as an alternative and record the decision in the PR. Trade-off:
HD EOAs are simpler and need no deployment, but need pre-funding for gas and hold live keys.
CREATE2 forwarders can be counterfactual (address known before deployment) and let the sweep be
pull-based, but cost more gas and add contract risk. Either is acceptable; an undocumented
choice is not.
Security — this is the critical one: with non-hardened derivation, xpub + any one child
private key ⇒ every sibling private key. If the extended public key is exposed anywhere (an
API response, a log, a webhook payload) and a single deposit key ever leaks, every customer
deposit address on that wallet is compromised. Treat the xpub as a secret and say so in the
threat model.
The same address must be re-derivable deterministically from the seed + index for disaster
recovery. Store the index, not just the address.
Store the EIP-55 checksummed form for display but index and compare on the lowercase form —
otherwise a client sending a lowercase address will fail to match a checksummed stored row. Add
a functional index or a normalised column.
Implement ChainAdapter::derive_deposit_address for EvmAdapter; Stellar's implementation keeps
returning the muxed pair unchanged.
Update the addresses API in crates/api/src/routes/addresses.rs
to return chain-appropriate shapes — EVM responses must not carry memo_id, and clients must
not be encouraged to send a memo (there is nowhere for it to go).
Update docs/deposit-model.md with the EVM model, side by side with the
muxed model, including the economic differences.
Test and commit
Concurrency test: N parallel allocations on one wallet yield N distinct indexes and N distinct
addresses, with no gaps that would break recovery. Model it on the existing supervisor_concurrency_tests.rs.
Determinism test: the same seed + index always produces the same address across runs and processes.
Case-normalisation test: looking up a deposit address by its lowercase, uppercase, and
checksummed forms all resolve to the same row.
Test that a Stellar wallet still allocates muxed addresses with completely unchanged behaviour.
feat(store): EVM per-customer deposit addresses via HD derivation
Stellar muxed accounts have no EVM equivalent, so each customer gets a
real derived EOA at m/44'/60'/0'/0/i, allocated under the same row lock
that guarantees gap-free muxed ids today.
Addresses are stored EIP-55 checksummed for display but indexed
lowercase, so client lookups match regardless of casing.
Documents the non-hardened derivation risk: xpub plus one leaked child
key compromises every sibling.
Refs #220
Guidelines
Security-sensitive. The PR must state plainly which keys the server now holds, what they can move,
and what bounds the exposure.
Depends on: #214, #217. Blocks: #221, #224.
Description
This is the deepest conceptual gap in the epic. Octo's deposit model
(
docs/deposit-model.md) gives every customer a muxed address: one baseaccount plus a 64-bit id, so a deposit lands in a single account already tagged with the customer
id — no new on-chain account, no reserve, no sweep.
Store::allocate_addressjust bumpsnext_muxed_idin a transaction.EVM has no such mechanism. Each customer needs a real, distinct, HD-derived EOA, which changes
the economics and the risk profile:
fund it first, or use a smart-contract forwarder.
move customer funds, a departure from the non-custodial posture in
0012_client_custody.sql. AD-4 permits thisnarrowly; it must be documented, bounded, and understood.
Implement EVM deposit-address allocation, preserving the atomicity guarantee the Stellar path has.
Requirements and context
derivation index. Reuse the existing transaction + row-lock pattern in
allocate_address.HD EOAs are simpler and need no deployment, but need pre-funding for gas and hold live keys.
CREATE2 forwarders can be counterfactual (address known before deployment) and let the sweep be
pull-based, but cost more gas and add contract risk. Either is acceptable; an undocumented
choice is not.
addresses.derivation_indexcolumn from feat(store): Multi-chain database schema migration #214 must agree. BIP-44'snon-hardened index level is bounded at
2^31 - 1.private key ⇒ every sibling private key. If the extended public key is exposed anywhere (an
API response, a log, a webhook payload) and a single deposit key ever leaks, every customer
deposit address on that wallet is compromised. Treat the xpub as a secret and say so in the
threat model.
recovery. Store the index, not just the address.
Suggested execution
Branch:
feat/evm-deposit-addressesImplement changes
Store::allocate_address(or add a chain-dispatched sibling) to allocate an EVM address:bump a per-wallet
next_derivation_indexunder the same row lock, derive via feat(evm-core): secp256k1 keys, BIP-44 derivation, and EIP-55 addresses #217, and insert withchain_id.otherwise a client sending a lowercase address will fail to match a checksummed stored row. Add
a functional index or a normalised column.
ChainAdapter::derive_deposit_addressforEvmAdapter; Stellar's implementation keepsreturning the muxed pair unchanged.
crates/api/src/routes/addresses.rsto return chain-appropriate shapes — EVM responses must not carry
memo_id, and clients mustnot be encouraged to send a memo (there is nowhere for it to go).
docs/deposit-model.mdwith the EVM model, side by side with themuxed model, including the economic differences.
Test and commit
addresses, with no gaps that would break recovery. Model it on the existing
supervisor_concurrency_tests.rs.checksummed forms all resolve to the same row.
docs/threat-model.mdwith the xpub + sibling-key-derivation risk.Example commit message
Guidelines
Security-sensitive. The PR must state plainly which keys the server now holds, what they can move,
and what bounds the exposure.