# FIND-223 + FIND-224: EIP-712 Domain Separation for Casper Contracts

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Integrate the `casper-eip-712` crate (v1.0.0) into cspr-bridge Casper contracts so all 4 attestation hash functions use proper EIP-712 domain separation, resolving Halborn findings FIND-223 (erc20_locker not bound into lock attestation) and FIND-224 (no deployment-specific domain on relayer attestation payloads).

**Architecture:** Each contract (TokenFactory, EthClient) gets its own EIP-712 domain separator computed on every hash call (Option A — no cached domain). Domain fields are: `name` (contract-specific constant), `version` ("1"), `chain_name` (stored `Var<String>`, set via `init` or admin setter for upgrades), `contract_package_hash` (from `self.env().self_address()` at runtime). TokenFactory additionally includes `erc20_locker` in its domain. The TypeScript CLI is updated to use `@casper-ecosystem/casper-eip-712` for Casper-side hash computations, ensuring cross-language hash equivalence. Cross-language test vectors prove Rust ↔ TypeScript produce identical digests.

**Tech Stack:** Rust (no_std, Odra 2.5.0), `casper-eip-712` v1.0.0 (crates.io), TypeScript/Jest, `@casper-ecosystem/casper-eip-712` v1.0.0 (npm)

**Branch:** `feat/find223-224-eip712-domain-separation`
**Tasks:** #328 (planning), #305 (FIND-223), #306 (FIND-224) — single branch, single commit resolving both.

---

## File Structure

| Action | File | Responsibility |
|--------|------|---------------|
| Modify | `contracts/casper/Cargo.toml` | Add `casper-eip-712` dependency |
| Modify | `contracts/casper/src/crypto.rs` | Refactor 4 hash functions to use `DomainBuilder` + `hash_typed_data`; add `Eip712Struct` impls for each attestation type |
| Modify | `contracts/casper/src/token_factory.rs` | Add `chain_name: Var<String>` (field 15/15); update `init()` signature; add `set_chain_name()` admin setter; update hash call sites to pass domain |
| Modify | `contracts/casper/src/eth_client.rs` | Add `chain_name: Var<String>` (field 8/15); update `init()` signature; add `set_chain_name()` admin setter; update hash call site to pass domain |
| Modify | `cli/package.json` | Add `@casper-ecosystem/casper-eip-712` dependency |
| Modify | `cli/src/shared/crypto/index.ts` | Replace Casper-side `solidityPackedKeccak256` with `hashTypedData` from npm package |
| Create | `cli/tests/unit/casper-eip712-cross-language.test.ts` | Cross-language test vectors proving Rust ↔ TypeScript hash equivalence |
| Update | `docs/CONTRACT_UPGRADE_PLAN.md` | Add EIP-712 domain separation upgrade steps (set_chain_name, attestation format change, relayer re-signing) to existing upgrade plan |

---

## Design Decisions

### Domain Separator Structure

**TokenFactory domain** (used by `compute_lock_attestation_hash`, `compute_burn_attestation_hash`, `compute_native_lock_attestation_hash`):
```
EIP712Domain(string name,string version,string chain_name,bytes32 contract_package_hash,address erc20_locker)
```
- `name`: `"CasperBridgeTokenFactory"` (constant)
- `version`: `"1"` (constant)
- `chain_name`: `"casper"` or `"casper-test"` (from `Var<String>`)
- `contract_package_hash`: `self.env().self_address()` serialized as bytes32
- `erc20_locker`: `self.erc20_locker.get()` as address (resolves FIND-223)

**EthClient domain** (used by `compute_block_header_attestation_hash`):
```
EIP712Domain(string name,string version,string chain_name,bytes32 contract_package_hash)
```
- `name`: `"CasperBridgeEthClient"` (constant)
- `version`: `"1"` (constant)
- `chain_name`: from `Var<String>`
- `contract_package_hash`: `self.env().self_address()` serialized as bytes32

### Why compute domain on every call (not cached)
- TokenFactory is at 15/15 Odra fields — storing a `DomainSeparator` would exceed the limit
- `erc20_locker` can change via `set_erc20_locker()` — caching would create stale-domain bugs
- Cost is a few keccak256 calls per verification — negligible vs. N signature recoveries

### Upgrade path
- `chain_name` is a new `Var<String>` — uninitialized after contract upgrade
- Hash functions **revert** with `Error::ChainNameNotSet` if `chain_name` is `None`
- Admin calls `set_chain_name("casper")` after upgrade (or bundles atomically in Casper 2.0)
- `init()` updated to accept `chain_name` for fresh deployments
- `docs/CONTRACT_UPGRADE_PLAN.md` documents the full procedure

### Self-address serialization
- Odra's `self.env().self_address()` returns an `Address` enum (Account or Contract)
- For `contract_package_hash`, we need the raw 32 bytes
- Use `address.to_bytes()` → take the 32-byte hash portion (skip the 1-byte tag prefix)
- This gives us the contract package hash that uniquely identifies the deployment

### What does NOT change
- Solidity contracts (EVM side already has EIP-712 domain separation from V7/FIND-001)
- The EVM domain and Casper domain are DIFFERENT — they verify different directions
- `verify_threshold_signatures()`, `recover_eth_address()`, `eth_signed_message_hash()` — unchanged
- P2P/relayer auth (separate finding, not in scope)

### Breaking change — hard cutover
- Old packed hashes and new EIP-712 hashes are incompatible — signatures from the old format will not verify against the new format
- This is intentional and correct: the whole point is to make old (domain-less) signatures invalid
- All relayers must upgrade simultaneously (coordinated deploy)

---

## Chunk 1: Rust — Cargo dependency + domain builder helpers

### Task 1: Add `casper-eip-712` dependency

**Files:**
- Modify: `contracts/casper/Cargo.toml`

- [ ] **Step 1: Add casper-eip-712 to [dependencies]**

Add after the `k256` dependency:

```toml
# EIP-712 typed data hashing and domain separation
casper-eip-712 = { version = "1.0.0", default-features = false }
```

Note: `default-features = false` is critical — this is a `no_std` contract. The crate's default is already `no_std`, but being explicit prevents future breakage if the crate adds a `std` default feature.

- [ ] **Step 2: Verify dependency resolves**

Run:
```bash
cd contracts/casper && cargo check 2>&1 | tail -5
```
Expected: Clean compilation (or only pre-existing warnings).

- [ ] **Step 3: Commit**

```bash
git add contracts/casper/Cargo.toml
git commit -m "chore: add casper-eip-712 dependency for FIND-223/FIND-224"
```

---

### Task 2: Add Eip712Struct implementations for attestation types

**Files:**
- Modify: `contracts/casper/src/crypto.rs`

These struct implementations define the EIP-712 type strings and encoding for each attestation payload. They sit in `crypto.rs` alongside the existing hash functions.

- [ ] **Step 1: Add imports at the top of crypto.rs**

After the existing imports, add:

```rust
use casper_eip_712::{
    DomainBuilder, DomainFieldValue, DomainSeparator,
    Eip712Struct, hash_typed_data,
    encoding::{encode_uint256, encode_uint64, encode_bytes32, encode_address},
};
```

- [ ] **Step 2: Define the LockAttestation EIP-712 struct**

Add after the existing `keccak256` function (before `eth_signed_message_hash`):

```rust
// =========================================================================
// EIP-712 Typed Data Structs
// =========================================================================

/// EIP-712 typed representation of a LockAttestation (Eth→Casper)
///
/// Type string: "LockAttestation(uint256 sourceChainId,uint256 ethBlockNumber,uint256 ethTxIndex,uint256 ethLogIndex,address erc20Token,address sender,uint256 amount,bytes32 casperRecipient,bytes32 receiptsRoot)"
pub struct LockAttestationData {
    pub source_chain_id: u64,
    pub eth_block_number: u64,
    pub eth_tx_index: u64,
    pub eth_log_index: u32,
    pub erc20_token: H160,
    pub sender: H160,
    pub amount: [u8; 32],
    pub casper_recipient: H256,
    pub receipts_root: H256,
}

impl Eip712Struct for LockAttestationData {
    fn type_string() -> &'static str {
        "LockAttestation(uint256 sourceChainId,uint256 ethBlockNumber,uint256 ethTxIndex,uint256 ethLogIndex,address erc20Token,address sender,uint256 amount,bytes32 casperRecipient,bytes32 receiptsRoot)"
    }

    fn encode_data(&self) -> Vec<u8> {
        let mut data = Vec::with_capacity(9 * 32);
        data.extend_from_slice(&encode_uint64(self.source_chain_id));
        data.extend_from_slice(&encode_uint64(self.eth_block_number));
        data.extend_from_slice(&encode_uint64(self.eth_tx_index));
        data.extend_from_slice(&encode_uint64(self.eth_log_index as u64));
        data.extend_from_slice(&encode_address(self.erc20_token));
        data.extend_from_slice(&encode_address(self.sender));
        data.extend_from_slice(&encode_uint256(self.amount));
        data.extend_from_slice(&encode_bytes32(self.casper_recipient));
        data.extend_from_slice(&encode_bytes32(self.receipts_root));
        data
    }
}
```

- [ ] **Step 3: Define the BlockHeaderAttestation EIP-712 struct**

```rust
/// EIP-712 typed representation of a BlockHeaderAttestation
///
/// Type string: "BlockHeaderAttestation(uint256 chainId,uint256 blockNumber,bytes32 blockHash,bytes32 stateRoot,bytes32 receiptsRoot)"
pub struct BlockHeaderAttestationData {
    pub chain_id: u64,
    pub block_number: u64,
    pub block_hash: H256,
    pub state_root: H256,
    pub receipts_root: H256,
}

impl Eip712Struct for BlockHeaderAttestationData {
    fn type_string() -> &'static str {
        "BlockHeaderAttestation(uint256 chainId,uint256 blockNumber,bytes32 blockHash,bytes32 stateRoot,bytes32 receiptsRoot)"
    }

    fn encode_data(&self) -> Vec<u8> {
        let mut data = Vec::with_capacity(5 * 32);
        data.extend_from_slice(&encode_uint64(self.chain_id));
        data.extend_from_slice(&encode_uint64(self.block_number));
        data.extend_from_slice(&encode_bytes32(self.block_hash));
        data.extend_from_slice(&encode_bytes32(self.state_root));
        data.extend_from_slice(&encode_bytes32(self.receipts_root));
        data
    }
}
```

- [ ] **Step 4: Define the BurnAttestation EIP-712 struct**

```rust
/// EIP-712 typed representation of a BurnAttestation (EVM wrapped burn → Casper native unlock)
///
/// Type string: "BurnAttestation(uint256 sourceChainId,uint256 ethBlockNumber,uint256 ethTxIndex,uint256 ethLogIndex,address wrappedToken,bytes32 cep18ContractHash,address sender,uint256 amount,bytes32 casperRecipient,uint256 nonce,bytes32 receiptsRoot)"
pub struct BurnAttestationData {
    pub source_chain_id: u64,
    pub eth_block_number: u64,
    pub eth_tx_index: u64,
    pub eth_log_index: u32,
    pub wrapped_token: H160,
    pub cep18_contract_hash: H256,
    pub sender: H160,
    pub amount: [u8; 32],
    pub casper_recipient: H256,
    pub nonce: u64,
    pub receipts_root: H256,
}

impl Eip712Struct for BurnAttestationData {
    fn type_string() -> &'static str {
        "BurnAttestation(uint256 sourceChainId,uint256 ethBlockNumber,uint256 ethTxIndex,uint256 ethLogIndex,address wrappedToken,bytes32 cep18ContractHash,address sender,uint256 amount,bytes32 casperRecipient,uint256 nonce,bytes32 receiptsRoot)"
    }

    fn encode_data(&self) -> Vec<u8> {
        let mut data = Vec::with_capacity(11 * 32);
        data.extend_from_slice(&encode_uint64(self.source_chain_id));
        data.extend_from_slice(&encode_uint64(self.eth_block_number));
        data.extend_from_slice(&encode_uint64(self.eth_tx_index));
        data.extend_from_slice(&encode_uint64(self.eth_log_index as u64));
        data.extend_from_slice(&encode_address(self.wrapped_token));
        data.extend_from_slice(&encode_bytes32(self.cep18_contract_hash));
        data.extend_from_slice(&encode_address(self.sender));
        data.extend_from_slice(&encode_uint256(self.amount));
        data.extend_from_slice(&encode_bytes32(self.casper_recipient));
        data.extend_from_slice(&encode_uint64(self.nonce));
        data.extend_from_slice(&encode_bytes32(self.receipts_root));
        data
    }
}
```

- [ ] **Step 5: Define the NativeLockAttestation EIP-712 struct**

```rust
/// EIP-712 typed representation of a NativeLockAttestation (Casper native lock → EVM wrapped mint)
///
/// Type string: "NativeLockAttestation(bytes32 casperTokenHash,uint256 amount,address ethRecipient,bytes32 casperEventKey,bytes32 casperStateRoot)"
pub struct NativeLockAttestationData {
    pub casper_token_hash: H256,
    pub amount: [u8; 32],
    pub eth_recipient: H160,
    pub casper_event_key: H256,
    pub casper_state_root: H256,
}

impl Eip712Struct for NativeLockAttestationData {
    fn type_string() -> &'static str {
        "NativeLockAttestation(bytes32 casperTokenHash,uint256 amount,address ethRecipient,bytes32 casperEventKey,bytes32 casperStateRoot)"
    }

    fn encode_data(&self) -> Vec<u8> {
        let mut data = Vec::with_capacity(5 * 32);
        data.extend_from_slice(&encode_bytes32(self.casper_token_hash));
        data.extend_from_slice(&encode_uint256(self.amount));
        data.extend_from_slice(&encode_address(self.eth_recipient));
        data.extend_from_slice(&encode_bytes32(self.casper_event_key));
        data.extend_from_slice(&encode_bytes32(self.casper_state_root));
        data
    }
}
```

- [ ] **Step 6: Add domain builder helper functions**

```rust
// =========================================================================
// Domain Separator Builders
// =========================================================================

/// Build the TokenFactory EIP-712 domain separator.
///
/// Includes `erc20_locker` to bind signatures to a specific EVM locker (FIND-223).
/// Includes `chain_name` + `contract_package_hash` for deployment-specific domain (FIND-224).
pub fn build_token_factory_domain(
    chain_name: &str,
    contract_package_hash: H256,
    erc20_locker: H160,
) -> DomainSeparator {
    DomainBuilder::new()
        .name("CasperBridgeTokenFactory")
        .version("1")
        .custom_field("chain_name", DomainFieldValue::String(chain_name.into()))
        .custom_field("contract_package_hash", DomainFieldValue::Bytes32(contract_package_hash))
        .custom_field("erc20_locker", DomainFieldValue::Address(erc20_locker))
        .build()
}

/// Build the EthClient EIP-712 domain separator.
///
/// Includes `chain_name` + `contract_package_hash` for deployment-specific domain (FIND-224).
pub fn build_eth_client_domain(
    chain_name: &str,
    contract_package_hash: H256,
) -> DomainSeparator {
    DomainBuilder::new()
        .name("CasperBridgeEthClient")
        .version("1")
        .custom_field("chain_name", DomainFieldValue::String(chain_name.into()))
        .custom_field("contract_package_hash", DomainFieldValue::Bytes32(contract_package_hash))
        .build()
}

/// Extract the 32-byte hash from an Odra Address for use as contract_package_hash.
///
/// Odra `Address::to_bytes()` returns a 1-byte tag + 32-byte hash.
/// We skip the tag byte and use the hash directly.
pub fn address_to_bytes32(address: &odra::prelude::Address) -> H256 {
    let bytes = address.to_bytes().expect("Address serialization cannot fail");
    let mut hash = [0u8; 32];
    // bytes[0] is the variant tag (0 = Account, 1 = Contract), bytes[1..33] is the hash
    hash.copy_from_slice(&bytes[1..33]);
    hash
}
```

- [ ] **Step 7: Verify compilation**

```bash
cd contracts/casper && cargo check 2>&1 | tail -10
```
Expected: Compiles cleanly (or only pre-existing warnings).

- [ ] **Step 8: Commit**

```bash
git add contracts/casper/src/crypto.rs
git commit -m "feat: add EIP-712 struct impls and domain builders for FIND-223/FIND-224"
```

---

## Chunk 2: Rust — Refactor hash functions to use EIP-712

### Task 3: Refactor the 4 attestation hash functions

**Files:**
- Modify: `contracts/casper/src/crypto.rs`

Each function changes from `keccak256(abi.encodePacked(...))` to `hash_typed_data(domain, &struct)`. The function signatures change to accept a `&DomainSeparator` parameter.

**IMPORTANT:** The old function signatures are kept temporarily as `#[deprecated]` wrappers (or simply removed if no external callers exist outside the two contract files). The call sites in `token_factory.rs` and `eth_client.rs` are updated in Task 4 and 5.

- [ ] **Step 1: Refactor `compute_lock_attestation_hash`**

Replace the existing function body with:

```rust
/// Compute EIP-712 typed data hash for a LockAttestation (Eth→Casper)
///
/// Uses EIP-712 domain separation to bind the hash to a specific deployment,
/// chain, and EVM locker contract (FIND-223, FIND-224).
///
/// # Arguments
/// * `domain` - TokenFactory EIP-712 domain separator
/// * `source_chain_id` - EVM chain ID where the lock occurred
/// * `eth_block_number` - Block number containing the lock event
/// * `eth_tx_index` - Transaction index within the block
/// * `eth_log_index` - Log index within the transaction
/// * `erc20_token` - ERC-20 token address that was locked
/// * `sender` - Address that locked the tokens
/// * `amount` - Amount of tokens locked (as 32-byte big-endian)
/// * `casper_recipient` - Recipient's Casper account hash
/// * `receipts_root` - Block's receipts root
///
/// # Returns
/// 32-byte EIP-712 typed data hash
pub fn compute_lock_attestation_hash(
    domain: &DomainSeparator,
    source_chain_id: u64,
    eth_block_number: u64,
    eth_tx_index: u64,
    eth_log_index: u32,
    erc20_token: H160,
    sender: H160,
    amount: &[u8; 32],
    casper_recipient: H256,
    receipts_root: H256,
) -> H256 {
    let data = LockAttestationData {
        source_chain_id,
        eth_block_number,
        eth_tx_index,
        eth_log_index,
        erc20_token,
        sender,
        amount: *amount,
        casper_recipient,
        receipts_root,
    };
    hash_typed_data(domain, &data)
}
```

- [ ] **Step 2: Refactor `compute_block_header_attestation_hash`**

```rust
/// Compute EIP-712 typed data hash for a BlockHeaderAttestation
///
/// Uses EIP-712 domain separation to bind the hash to a specific
/// EthClient deployment and chain (FIND-224).
pub fn compute_block_header_attestation_hash(
    domain: &DomainSeparator,
    chain_id: u64,
    block_number: u64,
    block_hash: H256,
    state_root: H256,
    receipts_root: H256,
) -> H256 {
    let data = BlockHeaderAttestationData {
        chain_id,
        block_number,
        block_hash,
        state_root,
        receipts_root,
    };
    hash_typed_data(domain, &data)
}
```

- [ ] **Step 3: Refactor `compute_burn_attestation_hash`**

```rust
/// Compute EIP-712 typed data hash for a BurnAttestation
///
/// Uses EIP-712 domain separation to bind the hash to a specific
/// TokenFactory deployment and chain (FIND-224).
pub fn compute_burn_attestation_hash(
    domain: &DomainSeparator,
    source_chain_id: u64,
    eth_block_number: u64,
    eth_tx_index: u64,
    eth_log_index: u32,
    wrapped_token: H160,
    cep18_contract_hash: H256,
    sender: H160,
    amount: &[u8; 32],
    casper_recipient: H256,
    nonce: u64,
    receipts_root: H256,
) -> H256 {
    let data = BurnAttestationData {
        source_chain_id,
        eth_block_number,
        eth_tx_index,
        eth_log_index,
        wrapped_token,
        cep18_contract_hash,
        sender,
        amount: *amount,
        casper_recipient,
        nonce,
        receipts_root,
    };
    hash_typed_data(domain, &data)
}
```

- [ ] **Step 4: Refactor `compute_native_lock_attestation_hash`**

```rust
/// Compute EIP-712 typed data hash for a NativeLockAttestation
///
/// Uses EIP-712 domain separation to bind the hash to a specific
/// TokenFactory deployment and chain (FIND-224).
pub fn compute_native_lock_attestation_hash(
    domain: &DomainSeparator,
    casper_token_hash: H256,
    amount: &[u8; 32],
    eth_recipient: H160,
    casper_event_key: H256,
    casper_state_root: H256,
) -> H256 {
    let data = NativeLockAttestationData {
        casper_token_hash,
        amount: *amount,
        eth_recipient,
        casper_event_key,
        casper_state_root,
    };
    hash_typed_data(domain, &data)
}
```

- [ ] **Step 5: Update all tests in crypto.rs**

All existing tests need to be updated to pass a domain separator. The test domain should use known constant values so test vectors are deterministic.

Add a test helper at the top of `mod tests`:

```rust
fn test_token_factory_domain() -> DomainSeparator {
    build_token_factory_domain(
        "casper-test",
        [0xAA; 32],  // mock contract_package_hash
        [0x11; 20],  // mock erc20_locker
    )
}

fn test_eth_client_domain() -> DomainSeparator {
    build_eth_client_domain(
        "casper-test",
        [0xBB; 32],  // mock contract_package_hash
    )
}
```

Then update every `compute_*` call to pass `&test_token_factory_domain()` or `&test_eth_client_domain()` as the first argument.

**CRITICAL:** All existing `assert_eq!` against hardcoded expected hashes must be REMOVED or UPDATED. The hashes WILL change because the format changed from packed to EIP-712. Replace with:
- Determinism tests (same inputs → same hash)
- Differentiation tests (different domain/inputs → different hash)
- Cross-domain tests (same payload, different domain → different hash — this is the whole point)

Add new test:

```rust
#[test]
fn test_same_lock_attestation_different_domain_different_hash() {
    let domain_a = build_token_factory_domain("casper-test", [0xAA; 32], [0x11; 20]);
    let domain_b = build_token_factory_domain("casper", [0xAA; 32], [0x11; 20]);
    let domain_c = build_token_factory_domain("casper-test", [0xBB; 32], [0x11; 20]);
    let domain_d = build_token_factory_domain("casper-test", [0xAA; 32], [0x22; 20]);

    let amount = [0u8; 32];
    let hash_a = compute_lock_attestation_hash(&domain_a, 11155111, 100, 1, 0, [0x11; 20], [0x22; 20], &amount, [0x33; 32], [0x44; 32]);
    let hash_b = compute_lock_attestation_hash(&domain_b, 11155111, 100, 1, 0, [0x11; 20], [0x22; 20], &amount, [0x33; 32], [0x44; 32]);
    let hash_c = compute_lock_attestation_hash(&domain_c, 11155111, 100, 1, 0, [0x11; 20], [0x22; 20], &amount, [0x33; 32], [0x44; 32]);
    let hash_d = compute_lock_attestation_hash(&domain_d, 11155111, 100, 1, 0, [0x11; 20], [0x22; 20], &amount, [0x33; 32], [0x44; 32]);

    assert_ne!(hash_a, hash_b, "Different chain_name must produce different hash (FIND-224)");
    assert_ne!(hash_a, hash_c, "Different contract_package_hash must produce different hash (FIND-224)");
    assert_ne!(hash_a, hash_d, "Different erc20_locker must produce different hash (FIND-223)");
}
```

- [ ] **Step 6: Verify compilation and tests**

```bash
cd contracts/casper && cargo test 2>&1 | tail -20
```
Expected: All tests pass.

- [ ] **Step 7: Commit**

```bash
git add contracts/casper/src/crypto.rs
git commit -m "feat: refactor attestation hashes to EIP-712 typed data (FIND-223, FIND-224)"
```

---

## Chunk 3: Rust — Update contract call sites (TokenFactory + EthClient)

### Task 4: Update TokenFactory — add chain_name storage and domain construction

**Files:**
- Modify: `contracts/casper/src/token_factory.rs`

- [ ] **Step 1: Add `chain_name` field to the struct**

Add after the `fee_config` field (making it field 15 of 15):

```rust
    /// Chain name for EIP-712 domain separation (e.g., "casper", "casper-test")
    /// Set via init() for fresh deployments or set_chain_name() for upgrades.
    chain_name: Var<String>,
```

- [ ] **Step 2: Add `ChainNameNotSet` error variant**

Add to the Error enum in token_factory.rs:

```rust
    /// Chain name not configured (required for EIP-712 domain separation)
    ChainNameNotSet = 50020,
```

(Use the next available error code — verify it doesn't conflict.)

- [ ] **Step 3: Add helper method to build domain**

Add a private helper method to the `impl TokenFactory` block:

```rust
    /// Build the EIP-712 domain separator for this TokenFactory instance.
    /// Reverts if chain_name has not been set (upgrade safety).
    fn eip712_domain(&self) -> DomainSeparator {
        let chain_name = self.chain_name.get().unwrap_or_else(|| {
            self.env().revert(Error::ChainNameNotSet);
        });
        let contract_hash = crate::crypto::address_to_bytes32(&self.env().self_address());
        let erc20_locker = self.erc20_locker.get().unwrap_or_else(|| {
            self.env().revert(Error::Unauthorized);
        });
        crate::crypto::build_token_factory_domain(&chain_name, contract_hash, erc20_locker)
    }
```

- [ ] **Step 4: Update `init()` to accept `chain_name`**

Change the `init` signature:

```rust
    pub fn init(
        &mut self,
        admin: Address,
        eth_client: Address,
        erc20_locker: H160,
        bridged_token_factory: Address,
        chain_name: String,
    ) {
```

Add validation and storage after the existing init logic:

```rust
        if chain_name.is_empty() {
            self.env().revert(Error::ChainNameNotSet);
        }
        self.chain_name.set(chain_name);
```

- [ ] **Step 5: Add `set_chain_name()` admin setter**

Add near the other admin setters (`set_erc20_locker`, etc.):

```rust
    /// Set the chain name for EIP-712 domain separation (admin only).
    ///
    /// Required after contract upgrade — must be called before any attestation
    /// verification will succeed. For fresh deployments, use the `init()` parameter instead.
    ///
    /// # Arguments
    /// * `chain_name` - Network name (e.g., "casper", "casper-test")
    pub fn set_chain_name(&mut self, chain_name: String) {
        self.require_admin();
        if chain_name.is_empty() {
            self.env().revert(Error::ChainNameNotSet);
        }
        self.chain_name.set(chain_name);
    }

    /// Get the configured chain name (for diagnostics)
    pub fn get_chain_name(&self) -> Option<String> {
        self.chain_name.get()
    }
```

- [ ] **Step 6: Update `mint_with_attestation` call site**

In `mint_with_attestation()`, change the hash computation from:

```rust
        let message_hash = compute_lock_attestation_hash(
            attestation.source_chain_id,
            ...
        );
```

to:

```rust
        let domain = self.eip712_domain();
        let message_hash = compute_lock_attestation_hash(
            &domain,
            attestation.source_chain_id,
            ...
        );
```

- [ ] **Step 7: Update `unlock_native` call site**

Same pattern — add `let domain = self.eip712_domain();` and pass `&domain` as first arg to `compute_burn_attestation_hash`.

- [ ] **Step 8: Update any other call sites (search for all uses)**

Run:
```bash
grep -n "compute_lock_attestation_hash\|compute_burn_attestation_hash\|compute_native_lock_attestation_hash" contracts/casper/src/token_factory.rs | grep -v "test\|//"
```

Update ALL non-test call sites. For test call sites, update them in Step 10.

- [ ] **Step 9: Add the crypto imports**

Add to the import section at the top of `token_factory.rs`:

```rust
use crate::crypto::build_token_factory_domain;
```

(Only needed if `eip712_domain()` helper doesn't already use the full path.)

- [ ] **Step 10: Update TokenFactory tests**

All tests that call `init()` need the new `chain_name` parameter. Search for all `init(` calls in the test module and add `chain_name: "casper-test".to_string()` (or the equivalent for Odra test syntax).

All tests that directly call `compute_lock_attestation_hash` or `compute_burn_attestation_hash` need a domain parameter — use a test helper domain.

- [ ] **Step 11: Verify compilation and tests**

```bash
cd contracts/casper && cargo test 2>&1 | tail -20
```
Expected: All tests pass. Pre-existing `server/ws/index.ts` errors are NOT your fault — ignore.

- [ ] **Step 12: Commit**

```bash
git add contracts/casper/src/token_factory.rs
git commit -m "feat(token-factory): add EIP-712 domain separation with chain_name (FIND-223, FIND-224)"
```

---

### Task 5: Update EthClient — add chain_name storage and domain construction

**Files:**
- Modify: `contracts/casper/src/eth_client.rs`

- [ ] **Step 1: Add `chain_name` field to the struct**

Add after `latest_blocks` (making it field 8 of 15):

```rust
    /// Chain name for EIP-712 domain separation (e.g., "casper", "casper-test")
    chain_name: Var<String>,
```

- [ ] **Step 2: Add `ChainNameNotSet` error variant**

Add to the EthClient Error enum:

```rust
    /// Chain name not configured (required for EIP-712 domain separation)
    ChainNameNotSet = 60013,
```

- [ ] **Step 3: Add helper method to build domain**

```rust
    fn eip712_domain(&self) -> crate::crypto::DomainSeparator {
        let chain_name = self.chain_name.get().unwrap_or_else(|| {
            self.env().revert(Error::ChainNameNotSet);
        });
        let contract_hash = crate::crypto::address_to_bytes32(&self.env().self_address());
        crate::crypto::build_eth_client_domain(&chain_name, contract_hash)
    }
```

- [ ] **Step 4: Update `init()` to accept `chain_name`**

```rust
    pub fn init(&mut self, admin: Address, relayer_eth_addresses: Vec<H160>, threshold: u32, chain_name: String) {
```

Add after the existing init logic:

```rust
        if chain_name.is_empty() {
            self.env().revert(Error::ChainNameNotSet);
        }
        self.chain_name.set(chain_name);
```

- [ ] **Step 5: Add `set_chain_name()` and `get_chain_name()`**

Same pattern as TokenFactory:

```rust
    pub fn set_chain_name(&mut self, chain_name: String) {
        self.require_admin();
        if chain_name.is_empty() {
            self.env().revert(Error::ChainNameNotSet);
        }
        self.chain_name.set(chain_name);
    }

    pub fn get_chain_name(&self) -> Option<String> {
        self.chain_name.get()
    }
```

- [ ] **Step 6: Update `submit_block_header_with_signatures` call site**

Change:

```rust
        let message_hash = compute_block_header_attestation_hash(
            chain_id,
            ...
        );
```

to:

```rust
        let domain = self.eip712_domain();
        let message_hash = compute_block_header_attestation_hash(
            &domain,
            chain_id,
            ...
        );
```

- [ ] **Step 7: Update EthClient tests**

All `init()` calls in tests need `chain_name: "casper-test".to_string()` parameter.
All `compute_block_header_attestation_hash` calls need a domain parameter.

- [ ] **Step 8: Verify compilation and tests**

```bash
cd contracts/casper && cargo test 2>&1 | tail -20
```
Expected: All tests pass.

- [ ] **Step 9: Commit**

```bash
git add contracts/casper/src/eth_client.rs
git commit -m "feat(eth-client): add EIP-712 domain separation with chain_name (FIND-224)"
```

---

## Chunk 4: TypeScript — Update CLI crypto to use @casper-ecosystem/casper-eip-712

### Task 6: Add npm dependency

**Files:**
- Modify: `cli/package.json`

- [ ] **Step 1: Install the package**

```bash
cd cli && npm install @casper-ecosystem/casper-eip-712@1.0.0
```

- [ ] **Step 2: Commit**

```bash
git add cli/package.json cli/package-lock.json
git commit -m "chore: add @casper-ecosystem/casper-eip-712 dependency"
```

---

### Task 7: Refactor Casper-side hash functions in TypeScript

**Files:**
- Modify: `cli/src/shared/crypto/index.ts`

The functions that need to change are the ones computing hashes for Casper-side verification:
- `computeLockAttestationHash` — currently `solidityPackedKeccak256`, needs EIP-712
- `computeWrappedBurnAttestationHash` — currently `solidityPackedKeccak256`, needs EIP-712
- `computeBlockHeaderAttestationHash` — currently `solidityPackedKeccak256`, needs EIP-712

The EVM-side functions (`computeUnlockAttestationHash`, `computeWrappedMintAttestationHash`) already use EIP-712 with standard EVM domain — those do NOT change.

- [ ] **Step 1: Add imports**

At the top of `cli/src/shared/crypto/index.ts`, add:

```typescript
import {
    hashTypedData,
    buildDomain,
    CASPER_DOMAIN_TYPES,
    encodeAddress,
    encodeUint256,
    encodeUint64,
    encodeBytes32,
    toHex,
    fromHex,
} from '@casper-ecosystem/casper-eip-712';
```

- [ ] **Step 2: Add Casper domain builder**

```typescript
/**
 * Domain type fields for TokenFactory EIP-712 domain
 */
const TOKEN_FACTORY_DOMAIN_TYPES = [
    { name: 'name', type: 'string' },
    { name: 'version', type: 'string' },
    { name: 'chain_name', type: 'string' },
    { name: 'contract_package_hash', type: 'bytes32' },
    { name: 'erc20_locker', type: 'address' },
];

/**
 * Domain type fields for EthClient EIP-712 domain
 */
const ETH_CLIENT_DOMAIN_TYPES = [
    { name: 'name', type: 'string' },
    { name: 'version', type: 'string' },
    { name: 'chain_name', type: 'string' },
    { name: 'contract_package_hash', type: 'bytes32' },
];

/**
 * Build the TokenFactory EIP-712 domain for Casper-side hash computation
 */
export function buildTokenFactoryDomain(
    chainName: string,
    contractPackageHash: string,
    erc20Locker: string,
): Record<string, unknown> {
    return {
        name: 'CasperBridgeTokenFactory',
        version: '1',
        chain_name: chainName,
        contract_package_hash: contractPackageHash,
        erc20_locker: erc20Locker,
    };
}

/**
 * Build the EthClient EIP-712 domain for Casper-side hash computation
 */
export function buildEthClientDomain(
    chainName: string,
    contractPackageHash: string,
): Record<string, unknown> {
    return {
        name: 'CasperBridgeEthClient',
        version: '1',
        chain_name: chainName,
        contract_package_hash: contractPackageHash,
    };
}
```

- [ ] **Step 3: Define TypeScript attestation type definitions for EIP-712**

```typescript
/**
 * EIP-712 type definitions for Casper-side attestation hashing
 */
const LOCK_ATTESTATION_TYPES = {
    LockAttestation: [
        { name: 'sourceChainId', type: 'uint256' },
        { name: 'ethBlockNumber', type: 'uint256' },
        { name: 'ethTxIndex', type: 'uint256' },
        { name: 'ethLogIndex', type: 'uint256' },
        { name: 'erc20Token', type: 'address' },
        { name: 'sender', type: 'address' },
        { name: 'amount', type: 'uint256' },
        { name: 'casperRecipient', type: 'bytes32' },
        { name: 'receiptsRoot', type: 'bytes32' },
    ],
};

const BLOCK_HEADER_ATTESTATION_TYPES = {
    BlockHeaderAttestation: [
        { name: 'chainId', type: 'uint256' },
        { name: 'blockNumber', type: 'uint256' },
        { name: 'blockHash', type: 'bytes32' },
        { name: 'stateRoot', type: 'bytes32' },
        { name: 'receiptsRoot', type: 'bytes32' },
    ],
};

const BURN_ATTESTATION_TYPES = {
    BurnAttestation: [
        { name: 'sourceChainId', type: 'uint256' },
        { name: 'ethBlockNumber', type: 'uint256' },
        { name: 'ethTxIndex', type: 'uint256' },
        { name: 'ethLogIndex', type: 'uint256' },
        { name: 'wrappedToken', type: 'address' },
        { name: 'cep18ContractHash', type: 'bytes32' },
        { name: 'sender', type: 'address' },
        { name: 'amount', type: 'uint256' },
        { name: 'casperRecipient', type: 'bytes32' },
        { name: 'nonce', type: 'uint256' },
        { name: 'receiptsRoot', type: 'bytes32' },
    ],
};

const NATIVE_LOCK_ATTESTATION_TYPES = {
    NativeLockAttestation: [
        { name: 'casperTokenHash', type: 'bytes32' },
        { name: 'amount', type: 'uint256' },
        { name: 'ethRecipient', type: 'address' },
        { name: 'casperEventKey', type: 'bytes32' },
        { name: 'casperStateRoot', type: 'bytes32' },
    ],
};
```

- [ ] **Step 4: Refactor `computeLockAttestationHash`**

The function signature changes to accept domain parameters:

```typescript
export function computeLockAttestationHash(
    attestation: LockAttestation,
    casperDomain: { chainName: string; contractPackageHash: string; erc20Locker: string },
): string {
    const domain = buildTokenFactoryDomain(
        casperDomain.chainName,
        casperDomain.contractPackageHash,
        casperDomain.erc20Locker,
    );
    const message = {
        sourceChainId: attestation.sourceChainId,
        ethBlockNumber: attestation.evmBlockNumber,
        ethTxIndex: attestation.evmTxIndex,
        ethLogIndex: BigInt(attestation.evmLogIndex),
        erc20Token: attestation.erc20Token,
        sender: attestation.sender,
        amount: attestation.amount,
        casperRecipient: attestation.casperRecipient,
        receiptsRoot: attestation.receiptsRoot,
    };
    return toHex(hashTypedData(domain, TOKEN_FACTORY_DOMAIN_TYPES, 'LockAttestation', LOCK_ATTESTATION_TYPES.LockAttestation, message));
}
```

**NOTE:** The exact `hashTypedData` call signature must match the npm package's API. Verify against `@casper-ecosystem/casper-eip-712/js/src/hash.ts` — it takes `(domain, domainTypes, primaryType, types, message)`. Adjust if the API differs.

- [ ] **Step 5: Refactor `computeWrappedBurnAttestationHash`**

Same pattern — accept `casperDomain` parameter, use `hashTypedData`.

- [ ] **Step 6: Refactor `computeBlockHeaderAttestationHash`**

Same pattern — uses `buildEthClientDomain` and `ETH_CLIENT_DOMAIN_TYPES`.

- [ ] **Step 7: Update all callers**

Search for all call sites of the changed functions across the CLI:

```bash
grep -rn "computeLockAttestationHash\|computeWrappedBurnAttestationHash\|computeBlockHeaderAttestationHash" cli/src/ --include="*.ts" | grep -v "test"
```

Each caller needs to pass the Casper domain parameters. These come from the bridge configuration (chain name, contract package hash, erc20 locker address).

- [ ] **Step 8: Update sign/verify/recover helpers**

Functions like `signLockAttestation`, `verifyLockAttestationSignature`, `recoverLockAttestationSigner`, etc. that call the hash functions — their signatures also need the domain parameter threaded through.

- [ ] **Step 9: Verify TypeScript compiles**

```bash
cd cli && npx tsc --noEmit 2>&1 | tail -20
```

- [ ] **Step 10: Commit**

```bash
git add cli/src/
git commit -m "feat(cli): use @casper-ecosystem/casper-eip-712 for Casper-side hashes (FIND-223, FIND-224)"
```

---

## Chunk 5: Cross-language test vectors + upgrade docs

### Task 8: Cross-language test vectors

**Files:**
- Create: `cli/tests/unit/casper-eip712-cross-language.test.ts`

These tests prove that the Rust and TypeScript implementations produce identical hashes for the same inputs and domain. The Rust side produces the expected hash (computed via `cargo test` with `println!`), and the TypeScript test asserts the same value.

- [ ] **Step 1: Generate Rust test vectors**

Add a test in `contracts/casper/src/crypto.rs` that prints hex-encoded hashes for known inputs:

```rust
#[test]
fn generate_cross_language_vectors() {
    let tf_domain = build_token_factory_domain("casper-test", [0xAA; 32], [0x11; 20]);
    let ec_domain = build_eth_client_domain("casper-test", [0xBB; 32]);

    // Vector 1: Lock attestation
    let lock_hash = compute_lock_attestation_hash(
        &tf_domain, 11155111, 12345678, 42, 7,
        [0xaa; 20], [0xbb; 20],
        &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0xe0, 0xb6, 0xb3, 0xa7, 0x64, 0x00, 0x00],
        [0xcc; 32], [0xdd; 32],
    );

    // Vector 2: Block header attestation
    let bh_hash = compute_block_header_attestation_hash(
        &ec_domain, 11155111, 12345678,
        [0x11; 32], [0x22; 32], [0x33; 32],
    );

    // Vector 3: Burn attestation
    let burn_hash = compute_burn_attestation_hash(
        &tf_domain, 11155111, 10045512, 3, 5,
        [0x55; 20], [0x66; 32], [0x77; 20],
        &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x5b, 0xcd, 0x15],
        [0x88; 32], 42, [0x99; 32],
    );

    // Vector 4: Native lock attestation
    let native_hash = compute_native_lock_attestation_hash(
        &tf_domain, [0x11; 32],
        &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
          0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x88],
        [0x22; 20], [0x33; 32], [0x44; 32],
    );

    // Print for cross-language test file (run with `cargo test -- --nocapture`)
    eprintln!("VECTOR_LOCK: 0x{}", hex::encode(lock_hash));
    eprintln!("VECTOR_BLOCK_HEADER: 0x{}", hex::encode(bh_hash));
    eprintln!("VECTOR_BURN: 0x{}", hex::encode(burn_hash));
    eprintln!("VECTOR_NATIVE_LOCK: 0x{}", hex::encode(native_hash));
}
```

Run: `cd contracts/casper && cargo test generate_cross_language_vectors -- --nocapture 2>&1 | grep VECTOR`

Record the 4 hex values.

- [ ] **Step 2: Write the TypeScript cross-language test**

Create `cli/tests/unit/casper-eip712-cross-language.test.ts`:

```typescript
import { describe, it, expect } from '@jest/globals';
import {
    computeLockAttestationHash,
    computeBlockHeaderAttestationHash,
    computeWrappedBurnAttestationHash,
    buildTokenFactoryDomain,
    buildEthClientDomain,
} from '../../src/shared/crypto/index.js';

/**
 * Cross-language test vectors: Rust ↔ TypeScript
 *
 * These vectors were generated by running `cargo test generate_cross_language_vectors`
 * in contracts/casper/. If these tests break, the Rust and TypeScript implementations
 * have diverged — signatures will fail cross-chain.
 *
 * Domain parameters (shared across all vectors):
 *   TokenFactory: name="CasperBridgeTokenFactory", version="1", chain_name="casper-test",
 *                 contract_package_hash=0xAA*32, erc20_locker=0x11*20
 *   EthClient:    name="CasperBridgeEthClient", version="1", chain_name="casper-test",
 *                 contract_package_hash=0xBB*32
 */
describe('Cross-language EIP-712 test vectors (Rust ↔ TypeScript)', () => {
    const TF_DOMAIN = {
        chainName: 'casper-test',
        contractPackageHash: '0x' + 'aa'.repeat(32),
        erc20Locker: '0x' + '11'.repeat(20),
    };

    const EC_DOMAIN = {
        chainName: 'casper-test',
        contractPackageHash: '0x' + 'bb'.repeat(32),
    };

    it('Vector 1: LockAttestation hash matches Rust', () => {
        const hash = computeLockAttestationHash({
            sourceChainId: 11155111n,
            evmBlockNumber: 12345678n,
            evmTxIndex: 42n,
            evmLogIndex: 7,
            erc20Token: '0x' + 'aa'.repeat(20),
            sender: '0x' + 'bb'.repeat(20),
            amount: BigInt('0x0de0b6b3a7640000'),
            casperRecipient: '0x' + 'cc'.repeat(32),
            receiptsRoot: '0x' + 'dd'.repeat(32),
        }, TF_DOMAIN);

        expect(hash.toLowerCase()).toBe('VECTOR_LOCK_VALUE_FROM_RUST');
    });

    it('Vector 2: BlockHeaderAttestation hash matches Rust', () => {
        const hash = computeBlockHeaderAttestationHash({
            chainId: 11155111,
            blockNumber: 12345678n,
            blockHash: '0x' + '11'.repeat(32),
            stateRoot: '0x' + '22'.repeat(32),
            receiptsRoot: '0x' + '33'.repeat(32),
        }, EC_DOMAIN);

        expect(hash.toLowerCase()).toBe('VECTOR_BLOCK_HEADER_VALUE_FROM_RUST');
    });

    it('Vector 3: BurnAttestation hash matches Rust', () => {
        const hash = computeWrappedBurnAttestationHash({
            sourceChainId: 11155111n,
            evmBlockNumber: 10045512n,
            evmTxIndex: 3n,
            evmLogIndex: 5,
            wrappedToken: '0x' + '55'.repeat(20),
            cep18ContractHash: '0x' + '66'.repeat(32),
            sender: '0x' + '77'.repeat(20),
            amount: BigInt('0x075bcd15'),
            casperRecipient: '0x' + '88'.repeat(32),
            nonce: 42n,
            receiptsRoot: '0x' + '99'.repeat(32),
        }, TF_DOMAIN);

        expect(hash.toLowerCase()).toBe('VECTOR_BURN_VALUE_FROM_RUST');
    });

    // Vector 4: NativeLock — add when compute_native_lock_attestation_hash is exposed in TS

    it('different domains produce different hashes', () => {
        const att = {
            sourceChainId: 11155111n,
            evmBlockNumber: 100n,
            evmTxIndex: 1n,
            evmLogIndex: 0,
            erc20Token: '0x' + '11'.repeat(20),
            sender: '0x' + '22'.repeat(20),
            amount: 1000n,
            casperRecipient: '0x' + '33'.repeat(32),
            receiptsRoot: '0x' + '44'.repeat(32),
        };

        const hash1 = computeLockAttestationHash(att, TF_DOMAIN);
        const hash2 = computeLockAttestationHash(att, {
            ...TF_DOMAIN,
            chainName: 'casper', // different chain
        });
        const hash3 = computeLockAttestationHash(att, {
            ...TF_DOMAIN,
            erc20Locker: '0x' + '99'.repeat(20), // different locker (FIND-223)
        });

        expect(hash1).not.toBe(hash2);
        expect(hash1).not.toBe(hash3);
    });
});
```

**NOTE:** Replace `VECTOR_*_VALUE_FROM_RUST` placeholders with actual values from Step 1.

- [ ] **Step 3: Run tests**

```bash
cd cli && NODE_OPTIONS='--experimental-vm-modules' npx jest tests/unit/casper-eip712-cross-language.test.ts 2>&1 | tail -20
```
Expected: All pass.

- [ ] **Step 4: Commit**

```bash
git add cli/tests/unit/casper-eip712-cross-language.test.ts contracts/casper/src/crypto.rs
git commit -m "test: cross-language EIP-712 test vectors for Rust ↔ TypeScript (FIND-223, FIND-224)"
```

---

### Task 9: Update existing CONTRACT_UPGRADE_PLAN.md

The file `docs/CONTRACT_UPGRADE_PLAN.md` already contains a complete V6→V10 EVM upgrade plan and Casper in-place upgrade steps. It does NOT need to be rewritten. Only targeted edits are needed.

**Files:**
- Edit: `docs/CONTRACT_UPGRADE_PLAN.md`

**Edits required:**

- [ ] **Edit 1: Add new section "What Changed — Casper (EIP-712 Domain Separation)" after the existing "What Changed — Casper (Undeployed FIND Fixes)" section**

Add a new table row and description covering:
- FIND-223: `erc20_locker` bound into TokenFactory attestation domain separator
- FIND-224: Deployment-specific domain separation (`chain_name` + `contract_package_hash`) on all attestation payloads
- All 4 attestation hash functions now use EIP-712 typed data hashing via `casper-eip-712` crate
- Breaking change: old-format relayer signatures are incompatible with new hash format

- [ ] **Edit 2: Add `set_chain_name()` call to "Step 7: Upgrade Casper Contracts In-Place"**

After each Casper contract upgrade (`cargo odra deploy`), add:
```
# Set chain name for EIP-712 domain separation (FIND-223/224)
# CRITICAL: attestation verification reverts with ChainNameNotSet until this is called
bridge-cli casper TokenFactory set_chain_name --chain-name "casper-test"  # testnet
bridge-cli casper EthClient set_chain_name --chain-name "casper-test"    # testnet
# Use "casper" for mainnet
```

- [ ] **Edit 3: Add domain separator verification to "Step 8: Post-Casper Verification"**

Add these checks:
```
# Verify EIP-712 chain name is set (FIND-223/224)
bridge-cli casper TokenFactory get_chain_name
bridge-cli casper EthClient get_chain_name
# Expected: "casper-test" (testnet) or "casper" (mainnet)
```

- [ ] **Edit 4: Update the "Known Remaining Gaps" table**

Remove FIND-223 and FIND-224 if they appear, since they are now resolved.

- [ ] **Edit 5: Add note to "Step 1: Stop All Relayers"**

Add: "Relayer CLI must be rebuilt with `@casper-ecosystem/casper-eip-712` v1.0.0 BEFORE restarting. Old CLI produces incompatible Casper-side attestation hashes."

- [ ] **Commit:**
```bash
git add docs/CONTRACT_UPGRADE_PLAN.md
git commit --amend --no-edit  # amend into the squashed commit, not a separate commit
```

---

### Task 10: Final squash and verification

- [ ] **Step 1: Run full Rust test suite**

```bash
cd contracts/casper && cargo odra test 2>&1 | tail -30
```
Expected: All tests pass.

- [ ] **Step 2: Run full TypeScript test suite**

```bash
cd cli && NODE_OPTIONS='--experimental-vm-modules' npx jest tests/unit/ 2>&1 | tail -30
```
Expected: All tests pass (some existing tests may need domain parameter updates).

- [ ] **Step 3: Squash into final commit**

Single commit resolving both findings:

```bash
git rebase -i origin/master
# Squash all commits into one with message:
# feat: add EIP-712 domain separation to Casper attestation hashes
#
# Resolves FIND-223: erc20_locker bound into TokenFactory domain separator
# Resolves FIND-224: deployment-specific domain (chain_name + contract_package_hash) on all payloads
#
# - Integrated casper-eip-712 v1.0.0 (Rust) and @casper-ecosystem/casper-eip-712 v1.0.0 (TypeScript)
# - All 4 attestation hash functions use EIP-712 typed data hashing
# - TokenFactory domain: name, version, chain_name, contract_package_hash, erc20_locker
# - EthClient domain: name, version, chain_name, contract_package_hash
# - chain_name stored as Var<String>, set via init() or set_chain_name() for upgrades
# - Cross-language test vectors prove Rust <> TypeScript hash equivalence
# - CONTRACT_UPGRADE_PLAN.md updated with domain separation upgrade steps
```

- [ ] **Step 4: Push branch**

```bash
git push origin feat/find223-224-eip712-domain-separation
```

---

### Task 11: Close FIND-223 and FIND-224 in CCC

After the branch is merged to master:

- [ ] **Step 1: Set #305 (FIND-223) to Done with resolution**

```bash
CCC_KEY=$(cat /home/jeanclaude/.openclaw/workspace/.secrets/ccc-api-key.txt)
# Get the merge commit hash after merge
COMMIT_HASH=$(cd /home/jeanclaude/workspace/cspr-bridge && git rev-parse HEAD)
curl -X PATCH "http://localhost:18500/api/projects/tasks/305" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $CCC_KEY" \
  -d "{\"status\":\"done\",\"resolution\":{\"type\":\"fixed\",\"commit_url\":\"https://github.com/make-software/cspr-bridge/commit/$COMMIT_HASH\",\"comment\":\"\"}}"
```

- [ ] **Step 2: Set #306 (FIND-224) to Done with same resolution**

Same as above but for task 306.

- [ ] **Step 3: Set #328 (this task) to Done**

Same commit URL, resolution type "fixed".

- [ ] **Step 4: Verify all three tasks show as Done with "Fixed" resolution in CCC**

These resolutions will be used to submit remediation to the Halborn portal.

---

## Acceptance Criteria

- [x] Design plan produced and saved to plan file
- [ ] All 4 Rust attestation hashes use casper-eip-712 `hash_typed_data` with domain separation
- [ ] `erc20_locker` bound into TokenFactory domain separator (FIND-223)
- [ ] Deployment domain (`contract_package_hash` + `chain_name`) on all payloads (FIND-224)
- [ ] `chain_name: Var<String>` added to both TokenFactory and EthClient
- [ ] `set_chain_name()` admin setter for upgrade path
- [ ] Hash functions revert with `ChainNameNotSet` if chain_name uninitialized
- [ ] `init()` updated to accept `chain_name` for fresh deployments
- [ ] TypeScript CLI uses `@casper-ecosystem/casper-eip-712` for Casper-side hashes
- [ ] Cross-language test vectors pass (Rust ↔ TypeScript identical hashes)
- [ ] `cargo odra test` passes in `contracts/casper/`
- [ ] CLI unit tests pass
- [ ] `docs/CONTRACT_UPGRADE_PLAN.md` updated (not created) with EIP-712 upgrade steps
- [ ] Single branch, single squashed commit resolving both FIND-223 and FIND-224
- [ ] #305 (FIND-223) closed as Done/Fixed with merge commit URL
- [ ] #306 (FIND-224) closed as Done/Fixed with merge commit URL
- [ ] #328 (this task) closed as Done/Fixed with merge commit URL
