diff --git a/contracts/bridge/integration/VaultBridgeIntegration.sol b/contracts/bridge/integration/VaultBridgeIntegration.sol index e5b9bb6..ae4970c 100644 --- a/contracts/bridge/integration/VaultBridgeIntegration.sol +++ b/contracts/bridge/integration/VaultBridgeIntegration.sol @@ -53,7 +53,7 @@ contract VaultBridgeIntegration is AccessControl { defaultDestinations.push(42161); // Arbitrum defaultDestinations.push(43114); // Avalanche defaultDestinations.push(56); // BNB Chain - defaultDestinations.push(10143); // Monad (example chain ID) + defaultDestinations.push(143); // Monad mainnet defaultDestinations.push(42793); // Etherlink (Tezos EVM L2) } diff --git a/contracts/bridge/integration/WTokenBridgeIntegration.sol b/contracts/bridge/integration/WTokenBridgeIntegration.sol index f98df6a..2b4806d 100644 --- a/contracts/bridge/integration/WTokenBridgeIntegration.sol +++ b/contracts/bridge/integration/WTokenBridgeIntegration.sol @@ -58,7 +58,7 @@ contract WTokenBridgeIntegration is AccessControl { defaultEvmDestinations.push(42161); // Arbitrum defaultEvmDestinations.push(43114); // Avalanche defaultEvmDestinations.push(56); // BNB Chain - defaultEvmDestinations.push(10143); // Monad + defaultEvmDestinations.push(143); // Monad mainnet defaultEvmDestinations.push(42793); // Etherlink (Tezos EVM L2) // Set default non-EVM destinations diff --git a/contracts/ccip/CCIPWETH10Bridge.sol b/contracts/ccip/CCIPWETH10Bridge.sol index 6c68f31..d63fa65 100644 --- a/contracts/ccip/CCIPWETH10Bridge.sol +++ b/contracts/ccip/CCIPWETH10Bridge.sol @@ -63,7 +63,7 @@ contract CCIPWETH10Bridge { constructor(address _ccipRouter, address _weth10, address _feeToken) { require(_ccipRouter != address(0), "CCIPWETH10Bridge: zero router"); require(_weth10 != address(0), "CCIPWETH10Bridge: zero WETH10"); - require(_feeToken != address(0), "CCIPWETH10Bridge: zero fee token"); + // feeToken may be address(0) to pay fees in native ETH/MON (msg.value) ccipRouter = IRouterClient(_ccipRouter); weth10 = _weth10; @@ -82,7 +82,7 @@ contract CCIPWETH10Bridge { uint64 destinationChainSelector, address recipient, uint256 amount - ) external returns (bytes32 messageId) { + ) external payable returns (bytes32 messageId) { require(amount > 0, "CCIPWETH10Bridge: invalid amount"); require(recipient != address(0), "CCIPWETH10Bridge: zero recipient"); @@ -123,14 +123,23 @@ contract CCIPWETH10Bridge { // Calculate fee uint256 fee = ccipRouter.getFee(destinationChainSelector, message); - // Approve and pay fee + // Pay fee: native ETH/MON if feeToken is address(0), else LINK/ERC20 if (fee > 0) { - require(IERC20(feeToken).transferFrom(msg.sender, address(this), fee), "CCIPWETH10Bridge: fee transfer failed"); - require(IERC20(feeToken).approve(address(ccipRouter), fee), "CCIPWETH10Bridge: fee approval failed"); + if (feeToken == address(0)) { + require(msg.value >= fee, "CCIPWETH10Bridge: insufficient native fee"); + } else { + require(msg.value == 0, "CCIPWETH10Bridge: use native or token fee, not both"); + require(IERC20(feeToken).transferFrom(msg.sender, address(this), fee), "CCIPWETH10Bridge: fee transfer failed"); + require(IERC20(feeToken).approve(address(ccipRouter), fee), "CCIPWETH10Bridge: fee approval failed"); + } } - // Send via CCIP - (messageId, ) = ccipRouter.ccipSend(destinationChainSelector, message); + // Send via CCIP (pass value when paying in native) + if (feeToken == address(0) && fee > 0) { + (messageId, ) = ccipRouter.ccipSend{value: fee}(destinationChainSelector, message); + } else { + (messageId, ) = ccipRouter.ccipSend(destinationChainSelector, message); + } emit CrossChainTransferInitiated( messageId, @@ -272,7 +281,6 @@ contract CCIPWETH10Bridge { * @notice Update fee token */ function updateFeeToken(address newFeeToken) external onlyAdmin { - require(newFeeToken != address(0), "CCIPWETH10Bridge: zero address"); feeToken = newFeeToken; } diff --git a/docs/deployment/ENV_MONAD_CHAIN138.example b/docs/deployment/ENV_MONAD_CHAIN138.example new file mode 100644 index 0000000..f05c1c5 --- /dev/null +++ b/docs/deployment/ENV_MONAD_CHAIN138.example @@ -0,0 +1,22 @@ +# Monad mainnet (chain 143) — CCIP stack for Chain 138 mesh +# Copy into smom-dbis-138/.env after deploy (or before if pre-seeding WMON addresses). +# See docs/03-deployment/MONAD_CHAIN138_DEPLOYMENT_RUNBOOK.md + +MONAD_MAINNET_RPC=https://rpc.monad.xyz +# Or Infura: https://monad-mainnet.infura.io/v3/ + +CCIP_MONAD_ROUTER=0x33566fE5976AAa420F3d5C64996641Fc3858CaDB +MONAD_SELECTOR=8481857512324358265 +# Native MON CCIP fees until LINK ERC-20 exists on Monad: +CCIP_MONAD_LINK_TOKEN=0x0000000000000000000000000000000000000000 + +# Canonical wrapped native (use for both WETH9/WETH10 slots on Monad) +WMON_MAINNET=0x754704bc059f8c67012fed69bc8a327a5aafb603 +WETH9_MONAD=0x754704bc059f8c67012fed69bc8a327a5aafb603 +WETH10_MONAD=0x754704bc059f8c67012fed69bc8a327a5aafb603 + +# Fill after deploy-monad-ccip-stack.sh --execute +CCIPWETH9_BRIDGE_MONAD= +CCIPWETH10_BRIDGE_MONAD= + +DEST_FUND_WALLET_MONAD=0x4A666F96fC8764181194447A7dFdb7d471b301C8 diff --git a/frontend-dapp/src/__tests__/utils/ens.test.ts b/frontend-dapp/src/__tests__/utils/ens.test.ts new file mode 100644 index 0000000..c157865 --- /dev/null +++ b/frontend-dapp/src/__tests__/utils/ens.test.ts @@ -0,0 +1,39 @@ +/** + * ENS utilities — forward resolve for impersonation / admin flows. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const DEPLOYER = '0x4A666F96fC8764181194447A7dFdb7d471b301C8' + +vi.mock('@wagmi/core', () => ({ + getPublicClient: vi.fn(() => ({ + getEnsAddress: vi.fn(async ({ name }: { name: string }) => + name.toLowerCase() === 'defi-oracle.eth' ? DEPLOYER : null, + ), + getEnsName: vi.fn(async () => null), + })), +})) + +vi.mock('../../config/wagmi', () => ({ config: {} })) + +describe('ENS utilities', () => { + beforeEach(async () => { + const { clearENSCache } = await import('../../utils/ens') + clearENSCache() + }) + + it('resolveAddressOrHex resolves defi-oracle.eth to deployer', async () => { + const { resolveAddressOrHex } = await import('../../utils/ens') + const result = await resolveAddressOrHex('defi-oracle.eth') + expect(result.valid).toBe(true) + expect(result.checksummed?.toLowerCase()).toBe(DEPLOYER.toLowerCase()) + }) + + it('resolveAddressOrHex rejects unknown ENS names', async () => { + const { resolveAddressOrHex } = await import('../../utils/ens') + const result = await resolveAddressOrHex('unknown-name.eth') + expect(result.valid).toBe(false) + expect(result.error).toMatch(/Could not resolve ENS/) + }) +}) diff --git a/frontend-dapp/src/components/admin/ImpersonationMode.tsx b/frontend-dapp/src/components/admin/ImpersonationMode.tsx index eb40272..3089a92 100644 --- a/frontend-dapp/src/components/admin/ImpersonationMode.tsx +++ b/frontend-dapp/src/components/admin/ImpersonationMode.tsx @@ -2,15 +2,31 @@ * ImpersonationMode Component - Wallet impersonation UI */ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useAdmin } from '../../contexts/AdminContext' -import { validateAddress } from '../../utils/security' +import { isEnsName, resolveAddressOrHex, resolveENS } from '../../utils/ens' import toast from 'react-hot-toast' export default function ImpersonationMode() { const { impersonationAddress, setImpersonationAddress, isImpersonating } = useAdmin() const [inputAddress, setInputAddress] = useState('') const [isValidating, setIsValidating] = useState(false) + const [resolvedEnsName, setResolvedEnsName] = useState(null) + const [inputEnsLabel, setInputEnsLabel] = useState(null) + + useEffect(() => { + if (!impersonationAddress) { + setResolvedEnsName(null) + return + } + let active = true + void resolveENS(impersonationAddress).then((name) => { + if (active) setResolvedEnsName(name) + }) + return () => { + active = false + } + }, [impersonationAddress]) const handleEnableImpersonation = async () => { if (!inputAddress.trim()) { @@ -19,7 +35,8 @@ export default function ImpersonationMode() { } setIsValidating(true) - const validation = validateAddress(inputAddress.trim()) + const trimmed = inputAddress.trim() + const validation = await resolveAddressOrHex(trimmed) if (!validation.valid) { toast.error(validation.error || 'Invalid address') @@ -29,7 +46,12 @@ export default function ImpersonationMode() { if (validation.checksummed) { setImpersonationAddress(validation.checksummed) - toast.success(`Impersonating address: ${validation.checksummed.slice(0, 10)}...`) + setInputEnsLabel(isEnsName(trimmed) ? trimmed : null) + toast.success( + isEnsName(trimmed) + ? `Impersonating ${trimmed}` + : `Impersonating address: ${validation.checksummed.slice(0, 10)}...`, + ) setInputAddress('') } setIsValidating(false) @@ -37,9 +59,12 @@ export default function ImpersonationMode() { const handleDisableImpersonation = () => { setImpersonationAddress(null) + setInputEnsLabel(null) toast.success('Impersonation disabled') } + const displayLabel = inputEnsLabel || resolvedEnsName + return (

Wallet Impersonation

@@ -53,6 +78,9 @@ export default function ImpersonationMode() {

Currently Impersonating

+ {displayLabel ? ( +

{displayLabel}

+ ) : null}

{impersonationAddress}