// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {BridgeEscrowVault} from "../../../contracts/bridge/interop/BridgeEscrowVault.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockERC20 is ERC20 { constructor() ERC20("Mock Token", "MOCK") { _mint(msg.sender, 1000000 * 10**18); } function mint(address to, uint256 amount) external { _mint(to, amount); } } contract BridgeEscrowVaultTest is Test { BridgeEscrowVault public vault; MockERC20 public token; address public admin = address(0x1); address public operator = address(0x2); address public refundOperator = address(0x3); address public hsmSigner = address(0x4); address public user = address(0x5); event Deposit( bytes32 indexed transferId, address indexed depositor, address indexed asset, uint256 amount, BridgeEscrowVault.DestinationType destinationType, bytes destinationData, uint256 timestamp ); function setUp() public { vm.startPrank(admin); vault = new BridgeEscrowVault(admin); vault.grantRole(vault.OPERATOR_ROLE(), operator); vault.grantRole(vault.REFUND_ROLE(), refundOperator); token = new MockERC20(); vm.stopPrank(); vm.deal(user, 100 ether); token.mint(user, 1000 * 10**18); } function test_DepositNative() public { vm.startPrank(user); bytes32 nonce = keccak256("test-nonce"); bytes memory destinationData = abi.encodePacked(address(0x100)); vm.expectEmit(false, true, true, true); // skip transferId (unknown until deposit) emit Deposit( bytes32(0), user, address(0), 1 ether, BridgeEscrowVault.DestinationType.EVM, destinationData, block.timestamp ); bytes32 transferId = vault.depositNative{value: 1 ether}( BridgeEscrowVault.DestinationType.EVM, destinationData, 3600, // 1 hour timeout nonce ); assertNotEq(transferId, bytes32(0)); assertEq(address(vault).balance, 1 ether); } function test_DepositERC20() public { vm.startPrank(user); token.approve(address(vault), 100 * 10**18); bytes32 nonce = keccak256("test-nonce-2"); bytes memory destinationData = abi.encodePacked(address(0x200)); bytes32 transferId = vault.depositERC20( address(token), 100 * 10**18, BridgeEscrowVault.DestinationType.XRPL, destinationData, 3600, nonce ); assertNotEq(transferId, bytes32(0)); assertEq(token.balanceOf(address(vault)), 100 * 10**18); assertEq(token.balanceOf(user), 900 * 10**18); } function test_UpdateTransferStatus() public { vm.startPrank(user); bytes32 transferId = vault.depositNative{value: 1 ether}( BridgeEscrowVault.DestinationType.EVM, abi.encodePacked(address(0x100)), 3600, keccak256("test") ); vm.stopPrank(); vm.startPrank(operator); vault.updateTransferStatus(transferId, BridgeEscrowVault.TransferStatus.DEPOSIT_CONFIRMED); BridgeEscrowVault.Transfer memory transfer = vault.getTransfer(transferId); assertEq(uint8(transfer.status), uint8(BridgeEscrowVault.TransferStatus.DEPOSIT_CONFIRMED)); } function test_RefundAfterTimeout() public { vm.startPrank(user); bytes32 transferId = vault.depositNative{value: 1 ether}( BridgeEscrowVault.DestinationType.EVM, abi.encodePacked(address(0x100)), 3600, // 1 hour timeout keccak256("refund-test") ); vm.stopPrank(); // Fast forward time vm.warp(block.timestamp + 3601); // Use a known test account for HSM signing (vm.sign requires private key, not address) uint256 hsmPk = 4; address signer = vm.addr(hsmPk); // Create refund request with HSM signature (EIP-712) bytes32 structHash = keccak256( abi.encode( keccak256("RefundRequest(bytes32 transferId,uint256 deadline)"), transferId, block.timestamp + 3600 ) ); bytes32 domainSeparator = keccak256( abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes("BridgeEscrowVault")), keccak256(bytes("1")), block.chainid, address(vault) ) ); bytes32 hash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(hsmPk, hash); bytes memory signature = abi.encodePacked(r, s, v); vm.startPrank(refundOperator); vault.initiateRefund( BridgeEscrowVault.RefundRequest({ transferId: transferId, deadline: block.timestamp + 3600, hsmSignature: signature }), signer ); vault.executeRefund(transferId); vm.stopPrank(); assertEq(address(user).balance, 100 ether); // Refunded } function test_Revert_DoubleDeposit() public { vm.startPrank(user); bytes32 nonce = keccak256("same-nonce"); bytes memory destinationData = abi.encodePacked(address(0x100)); vault.depositNative{value: 1 ether}( BridgeEscrowVault.DestinationType.EVM, destinationData, 3600, nonce ); // Try to deposit again with same nonce (should fail due to replay protection) vm.expectRevert(); vault.depositNative{value: 1 ether}( BridgeEscrowVault.DestinationType.EVM, destinationData, 3600, nonce ); } function test_Pause() public { vm.startPrank(admin); vault.pause(); vm.stopPrank(); vm.startPrank(user); vm.expectRevert(); vault.depositNative{value: 1 ether}( BridgeEscrowVault.DestinationType.EVM, abi.encodePacked(address(0x100)), 3600, keccak256("pause-test") ); } }