feat(wormhole): bridge contract hardening + E2E inbound#24
feat(wormhole): bridge contract hardening + E2E inbound#24skansal-rome wants to merge 17 commits into
Conversation
…m wormhole-adapter Selectively brings wormhole-specific files onto master without the stale SPL token library. Contracts compile against master's full SplTokenLib. Adds wormhole SDK file: dependencies and viaIR compiler settings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 36 tests across 2 test files for the RomeWormholeBridge contract hardening. 20 tests fail (RED phase) covering planned features that don't exist yet: BridgeSend/BridgeClaim events, input validation (zero amount, zero target, zero chain), and emergency pause via Ownable + Pausable. 16 tests pass covering existing correct functionality (encoding library, SPL approve encoding, EmptyAccounts). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…bridge contract Add BridgeSend/BridgeClaim events, input validation (zero amount, invalid target, invalid chain), and OpenZeppelin Ownable+Pausable emergency controls to RomeWormholeBridge. All 36 tests now pass (20 previously failing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused IX_INITIALIZE/IX_ATTEST_TOKEN constants from encoding library - Deduplicate encodeTransferNative/encodeTransferWrapped via shared _encodeTransfer helper - Fix authoritySignerPda view->pure to eliminate compiler warning - Extract filterEventLogs test helper to remove 5 duplicated event decode blocks - Remove redundant defensive assert.rejects in non-owner pause test - Add missing trailing newline to README.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add fee-exceeds-amount validation to _validateSendParams: revert early when fee > amount instead of letting the CPI to Wormhole Token Bridge fail with an opaque error. Three new tests cover the fee boundary (sendTransferNative/sendTransferWrapped revert, fee==amount passes). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove hardcoded private key from native_solana_claim.ts (CRITICAL), replaced with process.env.EVM_PRIVATE_KEY with existence check - Add _requireNotPaused() guard to invoke(), invokeWormholeCore(), invokeTokenBridge(), and invokeSplToken() (HIGH) - Add NatSpec documenting why programId is caller-supplied (Solana runtime validates CPI account constraints) - Enhance event test assertions to verify field values (sender, amount, nonce, claimer, tokenBridgeProgramId, accountCount) - Add pause guard tests for invoke() and invokeWormholeCore() - Document viaIR requirement in hardhat.config.ts (stack-too-deep) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rk configs The send phase runs on Sepolia where the bridge contract doesn't exist. Derive the Rome PDA locally using findProgramAddressSync instead of calling bridgeUserPda() on-chain. Also adds sepolia_env and monti_spl_env network configs for non-interactive E2E testing without the Hardhat keystore. Updates bridge deployment address to new hardened contract. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…vnet Fix PDA derivation in send script to use EXTERNAL_AUTHORITY seed (matching RomeEVMAccount.pda() on-chain) instead of rome_evm_user. Use actual Rome EVM program ID for the target rollup. Switch all network configs to montispl.devnet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Rome EVM proxy's Mollusk SVM emulator cannot simulate complex CPI chains like Wormhole completeWrapped, rejecting the tx during gas estimation. Send the claim as a native Solana transaction instead, which executes successfully on-chain. E2E verified: Sepolia lock -> VAA -> post to Solana -> claim -> 0.001 WETH minted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
E2E Inbound Transfer Verified ✅Full Sepolia → Rome inbound bridge tested on
Balance verified from both Solana devnet ( Note on claim approachThe claim step sends a native Solana transaction directly (bypassing Rome EVM proxy) because the Mollusk SVM emulator in the proxy cannot simulate complex CPI chains like Wormhole's
🤖 This comment was generated by Claude Code. |
️✅ There are no secrets present in this pull request anymore.If these secrets were true positive and are still valid, we highly recommend you to revoke them. 🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request. |
…ssage account The CPI precompile can only sign for the user's PDA and PDAs derived from salt seeds — it cannot sign for arbitrary keypairs. Wormhole's transfer instructions require a fresh message account signer. This solves the outbound blocker by deriving the message account as a PDA from the user's seed + a random salt, using invoke_signed to have the CPI precompile sign for it. Contract changes: - Add messageSalt (bytes32) parameter to sendTransferNative/sendTransferWrapped - Add _invokeSigned internal method (delegatecall to CPI invoke_signed) - Add deriveMessagePda view function for on-chain PDA derivation - Approve step uses invoke (user PDA only), transfer uses invoke_signed (+ message PDA) SDK changes (wormhole-sdk-ts): - Update ABI with messageSalt param and BridgeSend/BridgeClaim events - Update encode functions with messageSalt - Add deriveMessagePda helper to accountMeta.ts Scripts: - New wormhole_rome_to_sepolia.ts: full outbound flow (Rome wrapped WETH → Sepolia ETH) - Updated wormhole_transfer.ts: replace Keypair.generate() with PDA-derived message Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tive network The Rome EVM proxy emulator cannot simulate write-CPI (SPL Token transfers, Wormhole bridge operations) through the CPI precompile — it hits CannotRevertIrreversibleCall on the atomic endpoint and generic revert on the iterative endpoint. Same limitation that required the inbound claim to bypass the proxy with native Solana transactions. Rewrote wormhole_rome_to_sepolia.ts with 2-step hybrid approach: Step 1a: SPL Token transfer from PDA ATA → payer ATA (via Rome EVM CPI) Step 1b: Wormhole transferWrapped as native Solana tx (payer + message keypair) Added monti_spl_iterative network config for the -i endpoint. Note: Step 1a still fails due to the emulator limitation. This is a known platform constraint that requires Rome EVM proxy/emulator changes to resolve. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New RomeWormholeBridge at 0x2eb91e687247300853f392c4a903609df0cf8fcb deployed with invoke_signed support for outbound transfers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full end-to-end trace of both inbound (Sepolia -> Rome) and outbound (Rome -> Sepolia) flows with architecture diagrams, CPI chain traces, transaction hashes, balance flows, and test results. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Solidity's abi.encodeCall for _invoke and _invokeSigned with Yul assembly that constructs the delegatecall data directly from calldata. This avoids the expensive calldata→memory→ABI encode cycle for AccountMeta tuple arrays. Before: sendTransferWrapped consumed 1,399,644 / 1,400,000 CU (exceeded) After: sendTransferWrapped consumed 1,091,889 / 1,400,000 CU (308K saved) The Yul optimization uses calldatacopy to bulk-copy account arrays directly into the delegatecall buffer, skipping Solidity's per-element copy loop and ABI re-encoding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update: Yul-optimized CPI invoke for outbound flowProblem
Fix (commit
|
| Metric | Before | After | Savings |
|---|---|---|---|
| CU consumed | 1,399,644 | 1,091,889 | 308K (22%) |
| Status | EXCEEDED | SUCCESS |
E2E verified
- Outbound TX:
0x9e428426756f0276717ec3f3f2e0fa2f676df4e7d9a56170ff975983618d5474 - New contract:
0x31ab49a6035f6a17b22872aba223b747ed69d93aon montispl - All 41 contract tests pass
🤖 This response was generated by Claude Code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both _invoke and _invokeSigned read mload(0x40) but never advanced the free memory pointer after writing. Any subsequent Solidity memory allocation would overwrite the delegatecall buffer. Fix: add mstore(0x40, add(ptr, totalLen)) after computing totalLen in both assembly blocks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WrappedTokenView is a minimal ERC-20 view wrapper over a Wormhole-minted wrapped SPL mint so MetaMask (or any ERC-20 UI) can display the balance held in the user's Rome-PDA-owned ATA on Solana. Why not ERC20SPL from the SDK: the SDK's transparent-proxy wrapper calls SplToken.program_id() to derive the ATA, but that view precompile is unimplemented on the maximus SPL Token precompile (returns 0x). This contract sidesteps the issue by hardcoding the program ids (SPL_TOKEN_PROGRAM, ATA_PROGRAM, ROME_EVM_PROGRAM) and deriving the user PDA + ATA directly via SystemProgram.find_program_address. balanceOf reads the SPL Token Account's u64 amount field (offset 64, LE) via CpiProgram.account_info, returning 0 when the ATA doesn't exist. Transfer/approve revert — this is strictly a view shim, not a functional ERC-20. The canonical production path is the SDK's ERC20SPL once the SplToken.program_id() precompile lands on Rome. Deployed on maximus for Sepolia-WETH wrapped mint (6F5YWWrUMNpee8C6BDUc6DmRvYRMDDTgJHwKhbXuifWs): contract: 0x08a34004e48f800a8d865986c3d8e2e13511ca03 Users can Import as a custom token in MetaMask (symbol whWETH, decimals 8) to see their Wormhole-inbound ETH balance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add read-only ERC-20 shim for MetaMask balance display
Why not
Deployed on maximus for the Sepolia-WETH wrapped mint (
Production follow-ups (not in this PR):
🤖 This response was generated by Claude Code. |
Summary
wormhole-adapterbranch onto masterBridgeSend,BridgeClaim) for RomeScout indexingpause()/unpause()invoke()functionsEXTERNAL_AUTHORITYseed + actual program ID)E2E Evidence
0xef382c10a820c5dca4656561656ddfc4b91400ecb500166ba9467183625069bc3dtCW1H8rsQzFKha9GuTTjV9tDkUSnQTMUJjKsRLmiGqcQFqifuZeUhuGRPWcN61ovAurEYDQwVS6VUeDptqm8VxKnown Limitations
Test plan
npx hardhat test— 41/41 passing🤖 Generated with Claude Code