Bladeren bron

ethereum: migrate truffle tests to forge

derpy-duck 1 jaar geleden
bovenliggende
commit
2f4811c190

+ 1382 - 6
ethereum/forge-test/Bridge.t.sol

@@ -3,10 +3,21 @@
 pragma solidity ^0.8.0;
 
 import "../contracts/bridge/Bridge.sol";
+import "../contracts/bridge/BridgeSetup.sol";
+import "../contracts/bridge/BridgeImplementation.sol";
+import "../contracts/bridge/TokenBridge.sol";
+import "../contracts/interfaces/IWormhole.sol";
+import "../contracts/bridge/interfaces/ITokenBridge.sol";
+import "../contracts/bridge/token/TokenImplementation.sol";
+import "../contracts/bridge/mock/MockBridgeImplementation.sol";
+import "../contracts/bridge/mock/MockTokenBridgeIntegration.sol";
+import "../contracts/bridge/mock/MockFeeToken.sol";
 import "forge-std/Test.sol";
+import "./Implementation.t.sol";
+import "../contracts/bridge/mock/MockWETH9.sol";
 
 // @dev ensure some internal methods are public for testing
-contract ExportedBridge is Bridge {
+contract ExportedBridge is BridgeImplementation {
     function _truncateAddressPub(bytes32 b) public pure returns (address) {
         return super._truncateAddress(b);
     }
@@ -20,26 +31,105 @@ contract ExportedBridge is Bridge {
     }
 }
 
+interface ITokenBridgeTest is ITokenBridge {
+    function _truncateAddressPub(bytes32 b) external pure returns (address);
+
+    function setChainIdPub(uint16 chainId) external;
+
+    function setEvmChainIdPub(uint256 evmChainId) external;
+}
+
 contract TestBridge is Test {
-    ExportedBridge bridge;
+    BridgeSetup bridgeSetup;
+    ExportedBridge bridgeImpl;
+    ITokenBridgeTest bridge;
+    IWormhole wormhole;
+    TestImplementation implementationTest;
+    TokenImplementation tokenImpl;
+    IERC20 weth;
+    uint16 testChainId;
+    uint256 testEvmChainId;
+    uint16 governanceChainId;
+    bytes32 governanceContract;
+    uint8 constant finality = 15;
+
+    // "TokenBridge" (left padded)
+    bytes32 constant tokenBridgeModule =
+        0x000000000000000000000000000000000000000000546f6b656e427269646765;
+    uint8 actionRegisterChain = 1;
+    uint8 actionContractUpgrade = 2;
+    uint8 actionRecoverChainId = 3;
+
+    uint16 fakeChainId = 1337;
+    uint256 fakeEvmChainId = 10001;
+
+    uint16 testForeignChainId = 1;
+    bytes32 testForeignBridgeContract =
+        0x0000000000000000000000000000000000000000000000000000000000000004;
+    uint16 testBridgedAssetChain = 1;
+    bytes32 testBridgedAssetAddress =
+        0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e;
+
+    uint256 public constant testGuardian =
+        93941733246223705020089879371323733820373732307041878556247502674739205313440;
 
     function setUp() public {
-        bridge = new ExportedBridge();
+        // Setup wormhole
+        implementationTest = new TestImplementation();
+        implementationTest.setUp();
+
+        // Get wormhole from implementation tests
+        wormhole = IWormhole(address(implementationTest.proxied()));
+
+        // Deploy setup
+        bridgeSetup = new BridgeSetup();
+        // Deploy implementation contract
+        bridgeImpl = new ExportedBridge();
+        // Deploy token implementation
+        tokenImpl = new TokenImplementation();
+        // Deploy WETH
+        weth = IERC20(address(new MockWETH9()));
+
+        testChainId = implementationTest.testChainId();
+        testEvmChainId = implementationTest.testEvmChainId();
+        vm.chainId(testEvmChainId);
+        governanceChainId = implementationTest.governanceChainId();
+        governanceContract = implementationTest.governanceContract();
+
+        bytes memory setupAbi = abi.encodeWithSelector(
+            BridgeSetup.setup.selector,
+            address(bridgeImpl),
+            testChainId,
+            address(wormhole),
+            governanceChainId,
+            governanceContract,
+            address(tokenImpl),
+            address(weth),
+            finality,
+            testEvmChainId
+        );
+
+        // Deploy proxy
+        bridge = ITokenBridgeTest(
+            address(new TokenBridge(address(bridgeSetup), setupAbi))
+        );
     }
 
     function testTruncate(bytes32 b) public {
         bool invalidAddress = bytes12(b) != 0;
         if (invalidAddress) {
-            vm.expectRevert( "invalid EVM address");
+            vm.expectRevert("invalid EVM address");
         }
-        bytes32 converted = bytes32(uint256(uint160(bytes20(bridge._truncateAddressPub(b)))));
+        bytes32 converted = bytes32(
+            uint256(uint160(bytes20(bridge._truncateAddressPub(b))))
+        );
 
         if (!invalidAddress) {
             require(converted == b, "truncate does not roundrip");
         }
     }
 
-    function testEvmChainId() public {
+    function testSetEvmChainId() public {
         vm.chainId(1);
         bridge.setChainIdPub(1);
         bridge.setEvmChainIdPub(1);
@@ -56,6 +146,1292 @@ contract TestBridge is Test {
         // evmChainId must equal block.chainid
         vm.expectRevert("invalid evmChainId");
         bridge.setEvmChainIdPub(1337);
+    }
+
+    function testShouldBeInitializedWithTheCorrectSignersAndValues() public {
+        assertEq(address(bridge.WETH()), address(weth));
+        assertEq(bridge.tokenImplementation(), address(tokenImpl));
+        // test beacon functionality
+        assertEq(bridge.implementation(), address(tokenImpl));
+        assertEq(bridge.chainId(), testChainId);
+        assertEq(bridge.evmChainId(), testEvmChainId);
+        assertEq(bridge.finality(), finality);
+
+        // governance
+        uint16 readGovernanceChainId = bridge.governanceChainId();
+        bytes32 readGovernanceContract = bridge.governanceContract();
+        assertEq(
+            readGovernanceChainId,
+            governanceChainId,
+            "Wrong governance chain ID"
+        );
+        assertEq(
+            readGovernanceContract,
+            governanceContract,
+            "Wrong governance contract"
+        );
+    }
+
+    function testShouldRegisterAForeignBridgeImplementationCorrectly() public {
+        bytes memory data = abi.encodePacked(
+            tokenBridgeModule,
+            actionRegisterChain,
+            uint16(0),
+            testForeignChainId,
+            testForeignBridgeContract
+        );
+        bytes memory vaa = signAndEncodeVM(
+            1,
+            1,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        assertEq(
+            bridge.bridgeContracts(testForeignChainId),
+            bytes32(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            )
+        );
+        bridge.registerChain(vaa);
+        assertEq(
+            bridge.bridgeContracts(testForeignChainId),
+            testForeignBridgeContract
+        );
+    }
+
+    function testShouldAcceptAValidUpgrade() public {
+        MockBridgeImplementation mock = new MockBridgeImplementation();
+        bytes memory data = abi.encodePacked(
+            tokenBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            1,
+            1,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        assertEq(
+            vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT),
+            addressToBytes32(address(bridgeImpl))
+        );
+        bridge.upgrade(vaa);
+        assertEq(
+            vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT),
+            addressToBytes32(address(mock))
+        );
+
+        assertTrue(
+            MockBridgeImplementation(payable(address(bridge)))
+                .testNewImplementationActive(),
+            "implementation not active"
+        );
+    }
+
+    function testBridgedTokensShouldOnlyBeMintAndBurnableByOwner() public {
+        address owner = address(this);
+        address notOwner = address(0x1);
+        tokenImpl.initialize("TestToken", "TT", 18, 0, owner, 0, 0x0);
+        tokenImpl.mint(owner, 10);
+        tokenImpl.burn(owner, 5);
+
+        vm.expectRevert("caller is not the owner");
+        vm.prank(address(notOwner));
+        tokenImpl.mint(owner, 10);
+
+        vm.expectRevert("caller is not the owner");
+        vm.prank(address(notOwner));
+        tokenImpl.burn(owner, 5);
+    }
+
+    event LogMessagePublished(
+        address indexed sender,
+        uint64 sequence,
+        uint32 nonce,
+        bytes payload,
+        uint8 consistencyLevel
+    );
+
+    function testShouldAttestATokenCorrectly() public {
+        tokenImpl.initialize("TestToken", "TT", 18, 0, address(this), 0, 0x0);
+        bytes memory attestPayload = abi.encodePacked(
+            uint8(2),
+            addressToBytes32(address(tokenImpl)),
+            testChainId,
+            // decimals
+            uint8(18),
+            // symbol (TT)
+            bytes32(
+                0x5454000000000000000000000000000000000000000000000000000000000000
+            ),
+            // name (TestToken)
+            bytes32(
+                0x54657374546f6b656e0000000000000000000000000000000000000000000000
+            )
+        );
+        vm.expectEmit();
+        emit LogMessagePublished(
+            address(bridge),
+            uint64(0),
+            uint32(234),
+            attestPayload,
+            uint8(finality)
+        );
+        bridge.attestToken(address(tokenImpl), 234);
+    }
+
+    function testShouldCorrectlyDeployAWrappedAssetForATokenAttestation()
+        public
+    {
+        testShouldRegisterAForeignBridgeImplementationCorrectly();
+        testShouldAttestATokenCorrectly();
+        bytes memory data = abi.encodePacked(
+            uint8(2),
+            testBridgedAssetAddress,
+            testBridgedAssetChain,
+            uint8(18),
+            // symbol (TT)
+            bytes32(
+                0x5454000000000000000000000000000000000000000000000000000000000000
+            ),
+            // name (TestToken)
+            bytes32(
+                0x54657374546f6b656e0000000000000000000000000000000000000000000000
+            )
+        );
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        bridge.createWrapped(vaa);
+        address wrappedAddress = bridge.wrappedAsset(
+            testBridgedAssetChain,
+            testBridgedAssetAddress
+        );
+        assertTrue(
+            bridge.isWrappedAsset(wrappedAddress),
+            "Wrapped asset is not a wrapped asset"
+        );
+
+        TokenImplementation wrapped = TokenImplementation(wrappedAddress);
+
+        assertEq(wrapped.symbol(), "TT");
+        assertEq(wrapped.name(), "TestToken");
+        assertEq(wrapped.decimals(), 18);
+        assertEq(wrapped.chainId(), testBridgedAssetChain);
+        assertEq(wrapped.nativeContract(), testBridgedAssetAddress);
+    }
+
+    function testShouldCorrectlyUpdateAWrappedAssetForATokenAttestation()
+        public
+    {
+        testShouldCorrectlyDeployAWrappedAssetForATokenAttestation();
+        bytes memory data = abi.encodePacked(
+            uint8(2),
+            testBridgedAssetAddress,
+            testBridgedAssetChain,
+            uint8(18),
+            // symbol
+            bytes32(
+                0x5555000000000000000000000000000000000000000000000000000000000000
+            ),
+            // name
+            bytes32(
+                0x5472656500000000000000000000000000000000000000000000000000000000
+            )
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        vm.expectRevert("current metadata is up to date");
+        bridge.updateWrapped(vaa);
+
+        vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            1,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        bridge.updateWrapped(vaa);
+
+        address wrappedAddress = bridge.wrappedAsset(
+            testBridgedAssetChain,
+            testBridgedAssetAddress
+        );
+        assertTrue(
+            bridge.isWrappedAsset(wrappedAddress),
+            "Wrapped asset is not a wrapped asset"
+        );
+
+        TokenImplementation wrapped = TokenImplementation(wrappedAddress);
+
+        assertEq(wrapped.symbol(), "UU");
+        assertEq(wrapped.name(), "Tree");
+        assertEq(wrapped.decimals(), 18);
+        assertEq(wrapped.chainId(), testBridgedAssetChain);
+        assertEq(wrapped.nativeContract(), testBridgedAssetAddress);
+    }
+
+    function testShouldDepositAndLogTransfersCorrectly() public {
+        testShouldCorrectlyDeployAWrappedAssetForATokenAttestation();
+        uint256 amount = 1_000_000_000_000_000_000;
+        uint256 fee = 100_000_000_000_000_000;
+        tokenImpl.mint(address(this), amount);
+        tokenImpl.approve(address(bridge), amount);
+
+        uint256 accountBalanceBefore = tokenImpl.balanceOf(address(this));
+        uint256 bridgeBalanceBefore = tokenImpl.balanceOf(address(bridge));
+
+        assertEq(accountBalanceBefore, amount);
+        assertEq(bridgeBalanceBefore, 0);
+
+        uint16 toChain = testForeignChainId;
+        bytes32 toAddress = testForeignBridgeContract;
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            amount / 1e10,
+            addressToBytes32(address(tokenImpl)),
+            testChainId,
+            toAddress,
+            toChain,
+            fee / 1e10
+        );
+        vm.expectEmit();
+        emit LogMessagePublished(
+            address(bridge),
+            uint64(1),
+            uint32(234),
+            transferPayload,
+            uint8(finality)
+        );
+        bridge.transferTokens(
+            address(tokenImpl),
+            amount,
+            toChain,
+            toAddress,
+            fee,
+            234
+        );
+
+        uint256 accountBalanceAfter = tokenImpl.balanceOf(address(this));
+        uint256 bridgeBalanceAfter = tokenImpl.balanceOf(address(bridge));
+
+        assertEq(accountBalanceAfter, 0);
+        assertEq(bridgeBalanceAfter, amount);
+    }
+
+    function testShouldDepositAndLogFeeTokenTransfersCorrectly() public {
+        testShouldCorrectlyDeployAWrappedAssetForATokenAttestation();
+
+        uint256 mintAmount = 10_000_000_000_000_000_000;
+        uint256 amount = 1_000_000_000_000_000_000;
+        uint256 fee = 100_000_000_000_000_000;
+
+        uint16 toChain = testForeignChainId;
+        bytes32 toAddress = testForeignBridgeContract;
+
+        FeeToken feeToken = new FeeToken();
+        feeToken.initialize("Test", "TST", 18, 123, address(this), 0, 0x0);
+        feeToken.mint(address(this), mintAmount);
+        feeToken.approve(address(bridge), mintAmount);
+
+        uint256 bridgeBalanceBefore = feeToken.balanceOf(address(bridge));
+
+        uint256 feeAmount = (amount * 9) / 10;
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            feeAmount / 1e10,
+            addressToBytes32(address(feeToken)),
+            testChainId,
+            toAddress,
+            toChain,
+            fee / 1e10
+        );
+        vm.expectEmit();
+        emit LogMessagePublished(
+            address(bridge),
+            uint64(1),
+            uint32(234),
+            transferPayload,
+            uint8(finality)
+        );
+        bridge.transferTokens(
+            address(feeToken),
+            amount,
+            toChain,
+            toAddress,
+            fee,
+            234
+        );
+
+        uint256 bridgeBalanceAfter = feeToken.balanceOf(address(bridge));
+        assertEq(bridgeBalanceAfter, feeAmount, "Bridge balance is incorrect");
+    }
+
+    event TransferRedeemed(
+        uint16 indexed emitterChainId,
+        bytes32 indexed emitterAddress,
+        uint64 indexed sequence
+    );
+
+    function testShouldTransferOutLockedAssetsForAValidTransferVM() public {
+        testShouldDepositAndLogTransfersCorrectly();
+
+        uint256 amount = 1_000_000_000_000_000_000;
+        uint64 sequence = 1697;
+
+        uint256 accountBalanceBefore = tokenImpl.balanceOf(address(this));
+        uint256 bridgeBalanceBefore = tokenImpl.balanceOf(address(bridge));
+        assertEq(accountBalanceBefore, 0);
+        assertEq(bridgeBalanceBefore, amount);
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            amount / 1e10,
+            addressToBytes32(address(tokenImpl)),
+            testChainId,
+            addressToBytes32(address(this)),
+            testChainId,
+            uint256(0)
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            sequence,
+            transferPayload,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        vm.expectEmit();
+        emit TransferRedeemed(
+            testForeignChainId,
+            testForeignBridgeContract,
+            sequence
+        );
+        bridge.completeTransfer(vaa);
+
+        uint256 accountBalanceAfter = tokenImpl.balanceOf(address(this));
+        uint256 bridgeBalanceAfter = tokenImpl.balanceOf(address(bridge));
+        assertEq(accountBalanceAfter, amount);
+        assertEq(bridgeBalanceAfter, 0);
+    }
+
+    function testShouldDepositAndLogTransferWithPayloadCorrectly() public {
+        testShouldCorrectlyDeployAWrappedAssetForATokenAttestation();
+        uint256 amount = 1_000_000_000_000_000_000;
+        tokenImpl.mint(address(this), amount);
+        tokenImpl.approve(address(bridge), amount);
+
+        uint256 accountBalanceBefore = tokenImpl.balanceOf(address(this));
+        uint256 bridgeBalanceBefore = tokenImpl.balanceOf(address(bridge));
+
+        assertEq(accountBalanceBefore, amount);
+        assertEq(bridgeBalanceBefore, 0);
+
+        bytes memory additionalPayload = bytes("abc123");
+
+        uint16 toChain = testForeignChainId;
+        bytes32 toAddress = testForeignBridgeContract;
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(3),
+            amount / 1e10,
+            addressToBytes32(address(tokenImpl)),
+            testChainId,
+            toAddress,
+            toChain,
+            addressToBytes32(address(this)),
+            additionalPayload
+        );
+        vm.expectEmit();
+        emit LogMessagePublished(
+            address(bridge),
+            uint64(1),
+            uint32(234),
+            transferPayload,
+            uint8(finality)
+        );
+        bridge.transferTokensWithPayload(
+            address(tokenImpl),
+            amount,
+            toChain,
+            toAddress,
+            234,
+            additionalPayload
+        );
+
+        uint256 accountBalanceAfter = tokenImpl.balanceOf(address(this));
+        uint256 bridgeBalanceAfter = tokenImpl.balanceOf(address(bridge));
+
+        assertEq(accountBalanceAfter, 0);
+        assertEq(bridgeBalanceAfter, amount);
+    }
+
+    function testShouldTransferOutLockedAssetsForAValidTransferWithPayloadVM()
+        public
+    {
+        testShouldDepositAndLogTransfersCorrectly();
+
+        uint256 amount = 1_000_000_000_000_000_000;
+        uint64 sequence = 1111;
+
+        uint256 accountBalanceBefore = tokenImpl.balanceOf(address(this));
+        uint256 bridgeBalanceBefore = tokenImpl.balanceOf(address(bridge));
+        assertEq(accountBalanceBefore, 0);
+        assertEq(bridgeBalanceBefore, amount);
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(3),
+            amount / 1e10,
+            addressToBytes32(address(tokenImpl)),
+            testChainId,
+            addressToBytes32(address(this)),
+            testChainId,
+            uint256(0),
+            bytes("abc123")
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            sequence,
+            transferPayload,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        vm.expectEmit();
+        emit TransferRedeemed(
+            testForeignChainId,
+            testForeignBridgeContract,
+            sequence
+        );
+        bridge.completeTransferWithPayload(vaa);
+
+        uint256 accountBalanceAfter = tokenImpl.balanceOf(address(this));
+        uint256 bridgeBalanceAfter = tokenImpl.balanceOf(address(bridge));
+        assertEq(accountBalanceAfter, amount);
+        assertEq(bridgeBalanceAfter, 0);
+    }
+
+    function testShouldMintBridgedAssetWrappersOnTransferFromAnotherChainAndHandleFeesCorrectly()
+        public
+    {
+        testShouldTransferOutLockedAssetsForAValidTransferWithPayloadVM();
+
+        uint256 amount = 1_000_000_000_000_000_000;
+        uint256 fee = 100_000_000_000_000_000;
+
+        address wrappedAddress = bridge.wrappedAsset(
+            testBridgedAssetChain,
+            testBridgedAssetAddress
+        );
+        TokenImplementation wrapped = TokenImplementation(wrappedAddress);
+
+        assertEq(wrapped.totalSupply(), 0, "Wrong total supply");
+
+        bytes memory data = abi.encodePacked(
+            uint8(1),
+            amount / 1e10,
+            testBridgedAssetAddress,
+            testBridgedAssetChain,
+            addressToBytes32(address(this)),
+            testChainId,
+            fee / 1e10
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        address sender = address(0x1);
+        vm.prank(sender);
+        bridge.completeTransfer(vaa);
+
+        uint256 accountBalanceAfter = wrapped.balanceOf(address(this));
+        uint256 senderBalanceAfter = wrapped.balanceOf(address(sender));
+        assertEq(accountBalanceAfter, amount - fee);
+        assertEq(senderBalanceAfter, fee);
+        assertEq(wrapped.totalSupply(), amount);
+
+        vm.prank(sender);
+        wrapped.transfer(address(this), fee);
+    }
+
+    function testShouldNotAllowARedemptionFromMsgSenderOtherThanToOnTokenBridgeTransferWithPayload()
+        public
+    {
+        testShouldTransferOutLockedAssetsForAValidTransferWithPayloadVM();
+
+        uint256 amount = 1_000_000_000_000_000_000;
+
+        address wrappedAddress = bridge.wrappedAsset(
+            testBridgedAssetChain,
+            testBridgedAssetAddress
+        );
+        TokenImplementation wrapped = TokenImplementation(wrappedAddress);
+
+        assertEq(wrapped.totalSupply(), 0, "Wrong total supply");
+
+        address fromAddress = address(0x2);
+        bytes memory additionalPayload = bytes("abc123");
+
+        bytes memory data = abi.encodePacked(
+            uint8(3),
+            amount / 1e10,
+            testBridgedAssetAddress,
+            testBridgedAssetChain,
+            addressToBytes32(address(this)),
+            testChainId,
+            addressToBytes32(fromAddress),
+            additionalPayload
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        address sender = address(0x1);
+
+        vm.expectRevert("invalid sender");
+        vm.prank(sender);
+        bridge.completeTransferWithPayload(vaa);
+    }
 
+    function testShouldAllowARedemptionFromMsgSenderIsToOnTokenBridgeTransferWithPayloadAndCheckThatSenderReceivesFees()
+        public
+    {
+        testShouldTransferOutLockedAssetsForAValidTransferWithPayloadVM();
+
+        uint256 amount = 1_000_000_000_000_000_000;
+
+        address wrappedAddress = bridge.wrappedAsset(
+            testBridgedAssetChain,
+            testBridgedAssetAddress
+        );
+        TokenImplementation wrapped = TokenImplementation(wrappedAddress);
+
+        assertEq(wrapped.totalSupply(), 0, "Wrong total supply");
+
+        address fromAddress = address(0x2);
+        bytes memory additionalPayload = bytes("abc123");
+
+        bytes memory data = abi.encodePacked(
+            uint8(3),
+            amount / 1e10,
+            testBridgedAssetAddress,
+            testBridgedAssetChain,
+            addressToBytes32(address(this)),
+            testChainId,
+            addressToBytes32(fromAddress),
+            additionalPayload
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        bridge.completeTransferWithPayload(vaa);
+
+        uint256 accountBalanceAfter = wrapped.balanceOf(address(this));
+        assertEq(accountBalanceAfter, amount);
+        assertEq(wrapped.totalSupply(), amount);
+    }
+
+    function testShouldBurnBridgedAssetsWrappersOnTransferToAnotherChain()
+        public
+    {
+        testShouldMintBridgedAssetWrappersOnTransferFromAnotherChainAndHandleFeesCorrectly();
+
+        uint256 amount = 1_000_000_000_000_000_000;
+
+        address wrappedAddress = bridge.wrappedAsset(
+            testBridgedAssetChain,
+            testBridgedAssetAddress
+        );
+        TokenImplementation wrapped = TokenImplementation(wrappedAddress);
+        wrapped.approve(address(bridge), amount);
+
+        assertEq(wrapped.balanceOf(address(this)), amount);
+        assertEq(wrapped.totalSupply(), amount);
+
+        bridge.transferTokens(
+            wrappedAddress,
+            amount,
+            11,
+            testForeignBridgeContract,
+            0,
+            234
+        );
+
+        assertEq(wrapped.balanceOf(address(this)), 0);
+        assertEq(wrapped.balanceOf(address(bridge)), 0);
+        assertEq(wrapped.totalSupply(), 0);
+    }
+
+    function testShouldHandleETHDepositsCorrectly() public {
+        testShouldRegisterAForeignBridgeImplementationCorrectly();
+        uint256 amount = 1_000_000_000_000_000_000;
+        uint256 fee = 100_000_000_000_000_000;
+
+        vm.deal(address(this), amount);
+
+        uint256 totalWETHsupply = weth.totalSupply();
+        uint256 bridgeBalanceBefore = weth.balanceOf(address(bridge));
+
+        assertEq(totalWETHsupply, 0);
+        assertEq(bridgeBalanceBefore, 0);
+
+        uint16 toChain = testForeignChainId;
+        bytes32 toAddress = testForeignBridgeContract;
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            amount / 1e10,
+            addressToBytes32(address(weth)),
+            testChainId,
+            toAddress,
+            toChain,
+            fee / 1e10
+        );
+        vm.expectEmit();
+        emit LogMessagePublished(
+            address(bridge),
+            uint64(0),
+            uint32(234),
+            transferPayload,
+            uint8(finality)
+        );
+        bridge.wrapAndTransferETH{value: amount}(toChain, toAddress, fee, 234);
+
+        uint256 totalWETHSupplyAfter = weth.totalSupply();
+        uint256 bridgeBalanceAfter = weth.balanceOf(address(bridge));
+
+        assertEq(totalWETHSupplyAfter, amount);
+        assertEq(bridgeBalanceAfter, amount);
+    }
+
+    function testShouldHandleETHWithdrawalsAndFeesCorrectly() public {
+        testShouldHandleETHDepositsCorrectly();
+        uint256 amount = 1_000_000_000_000_000_000;
+        uint256 fee = 500_000_000_000_000_000;
+        uint64 sequence = 235;
+        address feeRecipient = address(
+            0x1234123412341234123412341234123412341234
+        );
+
+        uint256 accountBalanceBefore = weth.balanceOf(address(this));
+        uint256 feeRecipientBalanceBefore = weth.balanceOf(feeRecipient);
+        uint256 bridgeBalanceBefore = weth.balanceOf(address(bridge));
+        assertEq(accountBalanceBefore, 0);
+        assertEq(feeRecipientBalanceBefore, 0);
+        assertEq(bridgeBalanceBefore, amount);
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            amount / 1e10,
+            addressToBytes32(address(weth)),
+            testChainId,
+            addressToBytes32(address(this)),
+            testChainId,
+            fee / 1e10
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            sequence,
+            transferPayload,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        vm.expectEmit();
+        emit TransferRedeemed(
+            testForeignChainId,
+            testForeignBridgeContract,
+            sequence
+        );
+        vm.prank(feeRecipient);
+        bridge.completeTransferAndUnwrapETH(vaa);
+
+        uint256 totalSupplyAfter = weth.totalSupply();
+        assertEq(totalSupplyAfter, 0);
+
+        uint256 bridgeBalanceAfter = weth.balanceOf(address(bridge));
+        uint256 accountBalanceAfter = address(this).balance;
+        uint256 feeRecipientBalanceAfter = feeRecipient.balance;
+        assertEq(accountBalanceAfter, amount - fee);
+        assertEq(bridgeBalanceAfter, 0);
+        assertEq(feeRecipientBalanceAfter, fee);
+    }
+
+    function testShouldHandleETHDepositsWithPayloadCorrectly() public {
+        testShouldRegisterAForeignBridgeImplementationCorrectly();
+        uint256 amount = 1_000_000_000_000_000_000;
+
+        vm.deal(address(this), amount);
+
+        uint256 totalWETHsupply = weth.totalSupply();
+        uint256 bridgeBalanceBefore = weth.balanceOf(address(bridge));
+
+        assertEq(totalWETHsupply, 0);
+        assertEq(bridgeBalanceBefore, 0);
+
+        uint16 toChain = testForeignChainId;
+        bytes32 toAddress = testForeignBridgeContract;
+
+        bytes memory additionalPayload = bytes("abc123");
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(3),
+            amount / 1e10,
+            addressToBytes32(address(weth)),
+            testChainId,
+            toAddress,
+            toChain,
+            addressToBytes32(address(this)),
+            additionalPayload
+        );
+        vm.expectEmit();
+        emit LogMessagePublished(
+            address(bridge),
+            uint64(0),
+            uint32(234),
+            transferPayload,
+            uint8(finality)
+        );
+        bridge.wrapAndTransferETHWithPayload{value: amount}(
+            toChain,
+            toAddress,
+            234,
+            additionalPayload
+        );
+
+        uint256 totalWETHSupplyAfter = weth.totalSupply();
+        uint256 bridgeBalanceAfter = weth.balanceOf(address(bridge));
+
+        assertEq(totalWETHSupplyAfter, amount);
+        assertEq(bridgeBalanceAfter, amount);
+    }
+
+    function testShouldHandleETHWithdrawalsWithPayloadCorrectly() public {
+        testShouldHandleETHDepositsWithPayloadCorrectly();
+        uint256 amount = 1_000_000_000_000_000_000;
+        uint64 sequence = 235;
+
+        uint256 accountBalanceBefore = weth.balanceOf(address(this));
+        uint256 bridgeBalanceBefore = weth.balanceOf(address(bridge));
+        assertEq(accountBalanceBefore, 0);
+        assertEq(bridgeBalanceBefore, amount);
+        assertEq(weth.totalSupply(), amount);
+
+        address receiver = address(0x2);
+
+        bytes memory additionalPayload = bytes("abc123");
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(3),
+            amount / 1e10,
+            addressToBytes32(address(weth)),
+            testChainId,
+            addressToBytes32(receiver),
+            testChainId,
+            uint256(0),
+            additionalPayload
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            sequence,
+            transferPayload,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        vm.expectEmit();
+        emit TransferRedeemed(
+            testForeignChainId,
+            testForeignBridgeContract,
+            sequence
+        );
+        vm.prank(receiver);
+        bridge.completeTransferAndUnwrapETHWithPayload(vaa);
+
+        uint256 totalSupplyAfter = weth.totalSupply();
+        assertEq(totalSupplyAfter, 0);
+
+        uint256 bridgeBalanceAfter = weth.balanceOf(address(bridge));
+        uint256 receiverBalanceAfter = address(receiver).balance;
+        assertEq(receiverBalanceAfter, amount);
+        assertEq(bridgeBalanceAfter, 0);
+    }
+
+    function testShouldRevertOnTransferOutOfATotalOfMaxUint64Tokens() public {
+        uint256 amount = 184467440737095516160000000000;
+        uint256 firstTransfer = 1000000000000;
+
+        testShouldCorrectlyDeployAWrappedAssetForATokenAttestation();
+        tokenImpl.mint(address(this), amount);
+        tokenImpl.approve(address(bridge), amount);
+
+        uint16 toChain = testForeignChainId;
+        bytes32 toAddress = testForeignBridgeContract;
+
+        bridge.transferTokens(
+            address(tokenImpl),
+            firstTransfer,
+            toChain,
+            toAddress,
+            0,
+            234
+        );
+
+        vm.expectRevert(
+            "transfer exceeds max outstanding bridged token amount"
+        );
+        bridge.transferTokens(
+            address(tokenImpl),
+            amount - firstTransfer,
+            toChain,
+            toAddress,
+            0,
+            234
+        );
+    }
+
+    function addressToBytes32(address input) internal returns (bytes32 output) {
+        return bytes32(uint256(uint160(input)));
+    }
+
+    function uint256Array(
+        uint256 member
+    ) internal returns (uint256[] memory arr) {
+        arr = new uint256[](1);
+        arr[0] = member;
+    }
+
+    function signAndEncodeVM(
+        uint32 timestamp,
+        uint32 nonce,
+        uint16 emitterChainId,
+        bytes32 emitterAddress,
+        uint64 sequence,
+        bytes memory data,
+        uint256[] memory signers,
+        uint32 guardianSetIndex,
+        uint8 consistencyLevel
+    ) public returns (bytes memory signedMessage) {
+        bytes memory body = abi.encodePacked(
+            timestamp,
+            nonce,
+            emitterChainId,
+            emitterAddress,
+            sequence,
+            consistencyLevel,
+            data
+        );
+        bytes32 bodyHash = keccak256(abi.encodePacked(keccak256(body)));
+
+        // Sign the hash with the devnet guardian private key
+        IWormhole.Signature[] memory sigs = new IWormhole.Signature[](
+            signers.length
+        );
+        for (uint256 i = 0; i < signers.length; i++) {
+            (sigs[i].v, sigs[i].r, sigs[i].s) = vm.sign(signers[i], bodyHash);
+            sigs[i].guardianIndex = 0;
+        }
+
+        signedMessage = abi.encodePacked(
+            uint8(1),
+            guardianSetIndex,
+            uint8(sigs.length)
+        );
+
+        for (uint256 i = 0; i < signers.length; i++) {
+            signedMessage = abi.encodePacked(
+                signedMessage,
+                sigs[i].guardianIndex,
+                sigs[i].r,
+                sigs[i].s,
+                sigs[i].v - 27
+            );
+        }
+
+        signedMessage = abi.encodePacked(signedMessage, body);
+    }
+
+    function testShouldRejectSmartContractUpgradesOnForks() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        // Perform a successful upgrade
+        MockBridgeImplementation mock = new MockBridgeImplementation();
+
+        bytes memory data = abi.encodePacked(
+            tokenBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+
+        bridge.upgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(bridge),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, addressToBytes32(address(mock)));
+        assertEq(
+            MockBridgeImplementation(payable(address(bridge)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+
+        // Overwrite EVM Chain ID
+        MockBridgeImplementation(payable(address(bridge)))
+            .testOverwriteEVMChainId(fakeChainId, fakeEvmChainId);
+        assertEq(
+            bridge.chainId(),
+            fakeChainId,
+            "Overwrite didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            fakeEvmChainId,
+            "Overwrite didn't work for evm chain ID"
+        );
+
+        data = abi.encodePacked(
+            tokenBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        vm.expectRevert("invalid fork");
+        bridge.upgrade(vaa);
+    }
+
+    function testShouldAllowRecoverChainIDGovernancePacketsForks() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        // Perform a successful upgrade
+        MockBridgeImplementation mock = new MockBridgeImplementation();
+
+        bytes memory data = abi.encodePacked(
+            tokenBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+
+        bridge.upgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(bridge),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, bytes32(uint256(uint160(address(mock)))));
+        assertEq(
+            MockBridgeImplementation(payable(address(bridge)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+
+        // Overwrite EVM Chain ID
+        MockBridgeImplementation(payable(address(bridge)))
+            .testOverwriteEVMChainId(fakeChainId, fakeEvmChainId);
+        assertEq(
+            bridge.chainId(),
+            fakeChainId,
+            "Overwrite didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            fakeEvmChainId,
+            "Overwrite didn't work for evm chain ID"
+        );
+
+        // recover chain ID
+        data = abi.encodePacked(
+            tokenBridgeModule,
+            actionRecoverChainId,
+            testEvmChainId,
+            testChainId
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bridge.submitRecoverChainId(vaa);
+
+        assertEq(
+            bridge.chainId(),
+            testChainId,
+            "Recover didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            testEvmChainId,
+            "Recover didn't work for evm chain ID"
+        );
     }
+
+    function testShouldAcceptSmartContractUpgradesAfterChainIdHasBeenRecovered()
+        public
+    {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        // Perform a successful upgrade
+        MockBridgeImplementation mock = new MockBridgeImplementation();
+
+        bytes memory data = abi.encodePacked(
+            tokenBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+
+        bridge.upgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(bridge),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, bytes32(uint256(uint160(address(mock)))));
+        assertEq(
+            MockBridgeImplementation(payable(address(bridge)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+
+        // Overwrite EVM Chain ID
+        MockBridgeImplementation(payable(address(bridge)))
+            .testOverwriteEVMChainId(fakeChainId, fakeEvmChainId);
+        assertEq(
+            bridge.chainId(),
+            fakeChainId,
+            "Overwrite didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            fakeEvmChainId,
+            "Overwrite didn't work for evm chain ID"
+        );
+
+        // recover chain ID
+        data = abi.encodePacked(
+            tokenBridgeModule,
+            actionRecoverChainId,
+            testEvmChainId,
+            testChainId
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bridge.submitRecoverChainId(vaa);
+
+        assertEq(
+            bridge.chainId(),
+            testChainId,
+            "Recover didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            testEvmChainId,
+            "Recover didn't work for evm chain ID"
+        );
+
+        // Perform a successful upgrade
+        mock = new MockBridgeImplementation();
+
+        data = abi.encodePacked(
+            tokenBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        before = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+
+        bridge.upgrade(vaa);
+
+        afterUpgrade = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+        assertEq(afterUpgrade, bytes32(uint256(uint160(address(mock)))));
+        assertEq(
+            MockBridgeImplementation(payable(address(bridge)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+    }
+
+    fallback() external payable {}
 }

+ 1163 - 34
ethereum/forge-test/Implementation.t.sol

@@ -9,21 +9,55 @@ import "../contracts/Wormhole.sol";
 import "../contracts/interfaces/IWormhole.sol";
 import "forge-std/Test.sol";
 import "forge-test/rv-helpers/TestUtils.sol";
+import "../contracts/mock/MockImplementation.sol";
 
 contract TestImplementation is TestUtils {
-    event LogMessagePublished(address indexed sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel);
+    event LogMessagePublished(
+        address indexed sender,
+        uint64 sequence,
+        uint32 nonce,
+        bytes payload,
+        uint8 consistencyLevel
+    );
 
     Wormhole proxy;
     Implementation impl;
     Setup setup;
     Setup proxiedSetup;
-    IWormhole proxied;
+    IWormhole public proxied;
 
-    uint256 constant testGuardian = 93941733246223705020089879371323733820373732307041878556247502674739205313440;
-    bytes32 constant governanceContract = 0x0000000000000000000000000000000000000000000000000000000000000004;
+    uint256 public constant testGuardian =
+        93941733246223705020089879371323733820373732307041878556247502674739205313440;
+    uint16 public governanceChainId = 1;
+    bytes32 public constant governanceContract =
+        0x0000000000000000000000000000000000000000000000000000000000000004;
     bytes32 constant MESSAGEFEE_STORAGESLOT = bytes32(uint256(7));
     bytes32 constant SEQUENCES_SLOT = bytes32(uint256(4));
 
+    uint256 constant testBadSigner1PK =
+        61380885381456947260501717894649826485638944763666157704556612272461980735996;
+    uint256 constant testSigner1 =
+        93941733246223705020089879371323733820373732307041878556247502674739205313440;
+    uint256 constant testSigner2 =
+        62029033948131772461620424086954761227341731979036746506078649711513083917822;
+    uint256 constant testSigner3 =
+        61380885381456947260501717894649826485638944763666157704556612272461980735995;
+
+    // "Core" (left padded)
+    bytes32 constant core =
+        0x00000000000000000000000000000000000000000000000000000000436f7265;
+    uint8 actionContractUpgrade = 1;
+    uint8 actionGuardianSetUpgrade = 2;
+    uint8 actionMessageFee = 3;
+    uint8 actionTransferFee = 4;
+    uint8 actionRecoverChainId = 5;
+
+    uint16 public testChainId = 2;
+    uint256 public testEvmChainId = 1;
+
+    uint16 fakeChainId = 1337;
+    uint256 fakeEvmChainId = 10001;
+
     function setUp() public {
         // Deploy setup
         setup = new Setup();
@@ -39,19 +73,26 @@ contract TestImplementation is TestUtils {
         //proxied setup
         proxiedSetup = Setup(address(proxy));
 
-        vm.chainId(1);
+        vm.chainId(testEvmChainId);
         proxiedSetup.setup({
             implementation: address(impl),
             initialGuardians: keys,
-            chainId: 2,
+            chainId: testChainId,
             governanceChainId: 1,
             governanceContract: governanceContract,
-            evmChainId: 1
+            evmChainId: testEvmChainId
         });
 
         proxied = IWormhole(address(proxy));
     }
 
+    function uint256Array(
+        uint256 member
+    ) internal returns (uint256[] memory arr) {
+        arr = new uint256[](1);
+        arr[0] = member;
+    }
+
     function testPublishMessage(
         bytes32 storageSlot,
         uint256 messageFee,
@@ -59,22 +100,24 @@ contract TestImplementation is TestUtils {
         uint256 aliceBalance,
         uint32 nonce,
         bytes memory payload,
-        uint8 consistencyLevel)
-        public
-        unchangedStorage(address(proxied), storageSlot)
-    {
+        uint8 consistencyLevel
+    ) public unchangedStorage(address(proxied), storageSlot) {
         uint64 sequence = proxied.nextSequence(alice);
-        bytes32 storageLocation = hashedLocation(alice, SEQUENCES_SLOT); 
+        bytes32 storageLocation = hashedLocation(alice, SEQUENCES_SLOT);
 
         vm.assume(aliceBalance >= messageFee);
         vm.assume(storageSlot != storageLocation);
         vm.assume(storageSlot != MESSAGEFEE_STORAGESLOT);
 
         vm.store(address(proxied), MESSAGEFEE_STORAGESLOT, bytes32(messageFee));
-        vm.deal(address(alice),aliceBalance);
+        vm.deal(address(alice), aliceBalance);
 
         vm.prank(alice);
-        proxied.publishMessage{value: messageFee}(nonce, payload, consistencyLevel);
+        proxied.publishMessage{value: messageFee}(
+            nonce,
+            payload,
+            consistencyLevel
+        );
 
         assertEq(sequence + 1, proxied.nextSequence(alice));
     }
@@ -86,25 +129,33 @@ contract TestImplementation is TestUtils {
         uint256 aliceBalance,
         uint32 nonce,
         bytes memory payload,
-        uint8 consistencyLevel)
-        public
-        unchangedStorage(address(proxied), storageSlot)
-    {
+        uint8 consistencyLevel
+    ) public unchangedStorage(address(proxied), storageSlot) {
         uint64 sequence = proxied.nextSequence(alice);
-        bytes32 storageLocation = hashedLocation(alice, SEQUENCES_SLOT); 
+        bytes32 storageLocation = hashedLocation(alice, SEQUENCES_SLOT);
 
         vm.assume(aliceBalance >= messageFee);
         vm.assume(storageSlot != storageLocation);
         vm.assume(storageSlot != MESSAGEFEE_STORAGESLOT);
 
         vm.store(address(proxied), MESSAGEFEE_STORAGESLOT, bytes32(messageFee));
-        vm.deal(address(alice),aliceBalance);
+        vm.deal(address(alice), aliceBalance);
 
         vm.prank(alice);
         vm.expectEmit(true, true, true, true);
-        emit LogMessagePublished(alice, sequence, nonce, payload, consistencyLevel);
+        emit LogMessagePublished(
+            alice,
+            sequence,
+            nonce,
+            payload,
+            consistencyLevel
+        );
 
-        proxied.publishMessage{value: messageFee}(nonce, payload, consistencyLevel);
+        proxied.publishMessage{value: messageFee}(
+            nonce,
+            payload,
+            consistencyLevel
+        );
     }
 
     function testPublishMessage_Revert_InvalidFee(
@@ -115,20 +166,22 @@ contract TestImplementation is TestUtils {
         uint256 aliceFee,
         uint32 nonce,
         bytes memory payload,
-        uint8 consistencyLevel)
-        public
-        unchangedStorage(address(proxied), storageSlot)
-    {
+        uint8 consistencyLevel
+    ) public unchangedStorage(address(proxied), storageSlot) {
         vm.assume(aliceBalance >= aliceFee);
         vm.assume(aliceFee != messageFee);
         vm.assume(storageSlot != MESSAGEFEE_STORAGESLOT);
 
         vm.store(address(proxied), MESSAGEFEE_STORAGESLOT, bytes32(messageFee));
-        vm.deal(address(alice),aliceBalance);
+        vm.deal(address(alice), aliceBalance);
 
         vm.prank(alice);
         vm.expectRevert("invalid fee");
-        proxied.publishMessage{value: aliceFee}(nonce, payload, consistencyLevel);
+        proxied.publishMessage{value: aliceFee}(
+            nonce,
+            payload,
+            consistencyLevel
+        );
     }
 
     function testPublishMessage_Revert_OutOfFunds(
@@ -138,18 +191,1094 @@ contract TestImplementation is TestUtils {
         uint256 aliceBalance,
         uint32 nonce,
         bytes memory payload,
-        uint8 consistencyLevel)
-        public
-        unchangedStorage(address(proxied), storageSlot)
-    {
+        uint8 consistencyLevel
+    ) public unchangedStorage(address(proxied), storageSlot) {
         vm.assume(aliceBalance < messageFee);
         vm.assume(storageSlot != MESSAGEFEE_STORAGESLOT);
 
         vm.store(address(proxied), MESSAGEFEE_STORAGESLOT, bytes32(messageFee));
-        vm.deal(address(alice),aliceBalance);
+        vm.deal(address(alice), aliceBalance);
 
         vm.prank(alice);
         vm.expectRevert();
-        proxied.publishMessage{value: messageFee}(nonce, payload, consistencyLevel);
+        proxied.publishMessage{value: messageFee}(
+            nonce,
+            payload,
+            consistencyLevel
+        );
+    }
+
+    function testShouldBeInitializedWithCorrectSignersAndValues() public {
+        uint32 index = proxied.getCurrentGuardianSetIndex();
+        IWormhole.GuardianSet memory set = proxied.getGuardianSet(index);
+
+        // check set
+        assertEq(set.keys.length, 1, "Guardian set length wrong");
+        assertEq(set.keys[0], vm.addr(testGuardian), "Guardian wrong");
+
+        // check expiration
+        assertEq(set.expirationTime, 0);
+
+        // chain id
+        uint16 chainId = proxied.chainId();
+        assertEq(chainId, testChainId, "Wrong Chain ID");
+
+        // evm chain id
+        uint256 evmChainId = proxied.evmChainId();
+        assertEq(evmChainId, testEvmChainId, "Wrong EVM Chain ID");
+
+        // governance
+        uint16 readGovernanceChainId = proxied.governanceChainId();
+        bytes32 readGovernanceContract = proxied.governanceContract();
+        assertEq(
+            readGovernanceChainId,
+            governanceChainId,
+            "Wrong governance chain ID"
+        );
+        assertEq(
+            readGovernanceContract,
+            governanceContract,
+            "Wrong governance contract"
+        );
+    }
+
+    function testShouldLogAPublishedMessageCorrectly() public {
+        vm.expectEmit();
+        emit LogMessagePublished(
+            address(this),
+            uint64(0),
+            uint32(291),
+            bytes(hex"123321"),
+            uint8(32)
+        );
+        proxied.publishMessage(0x123, hex"123321", 32);
+    }
+
+    function testShouldIncreaseTheSequenceForAnAccount() public {
+        proxied.publishMessage(0x1, hex"01", 32);
+        uint64 sequence = proxied.publishMessage(0x1, hex"01", 32);
+        assertEq(sequence, 1, "Sequence number didn't increase");
+    }
+
+    function signAndEncodeVMFixedIndex(
+        uint32 timestamp,
+        uint32 nonce,
+        uint16 emitterChainId,
+        bytes32 emitterAddress,
+        uint64 sequence,
+        bytes memory data,
+        uint256[] memory signers,
+        uint32 guardianSetIndex,
+        uint8 consistencyLevel
+    ) public returns (bytes memory signedMessage) {
+        bytes memory body = abi.encodePacked(
+            timestamp,
+            nonce,
+            emitterChainId,
+            emitterAddress,
+            sequence,
+            consistencyLevel,
+            data
+        );
+        bytes32 bodyHash = keccak256(abi.encodePacked(keccak256(body)));
+
+        // Sign the hash with the devnet guardian private key
+        IWormhole.Signature[] memory sigs = new IWormhole.Signature[](
+            signers.length
+        );
+        for (uint256 i = 0; i < signers.length; i++) {
+            (sigs[i].v, sigs[i].r, sigs[i].s) = vm.sign(signers[i], bodyHash);
+            sigs[i].guardianIndex = 0;
+        }
+
+        signedMessage = abi.encodePacked(
+            uint8(1),
+            guardianSetIndex,
+            uint8(sigs.length)
+        );
+
+        for (uint256 i = 0; i < signers.length; i++) {
+            signedMessage = abi.encodePacked(
+                signedMessage,
+                uint8(0),
+                sigs[i].r,
+                sigs[i].s,
+                sigs[i].v - 27
+            );
+        }
+
+        signedMessage = abi.encodePacked(signedMessage, body);
+    }
+
+    function signAndEncodeVM(
+        uint32 timestamp,
+        uint32 nonce,
+        uint16 emitterChainId,
+        bytes32 emitterAddress,
+        uint64 sequence,
+        bytes memory data,
+        uint256[] memory signers,
+        uint32 guardianSetIndex,
+        uint8 consistencyLevel
+    ) public returns (bytes memory signedMessage) {
+        bytes memory body = abi.encodePacked(
+            timestamp,
+            nonce,
+            emitterChainId,
+            emitterAddress,
+            sequence,
+            consistencyLevel,
+            data
+        );
+        bytes32 bodyHash = keccak256(abi.encodePacked(keccak256(body)));
+
+        // Sign the hash with the devnet guardian private key
+        IWormhole.Signature[] memory sigs = new IWormhole.Signature[](
+            signers.length
+        );
+        for (uint256 i = 0; i < signers.length; i++) {
+            (sigs[i].v, sigs[i].r, sigs[i].s) = vm.sign(signers[i], bodyHash);
+            sigs[i].guardianIndex = 0;
+        }
+
+        signedMessage = abi.encodePacked(
+            uint8(1),
+            guardianSetIndex,
+            uint8(sigs.length)
+        );
+
+        for (uint256 i = 0; i < signers.length; i++) {
+            signedMessage = abi.encodePacked(
+                signedMessage,
+                sigs[i].guardianIndex,
+                sigs[i].r,
+                sigs[i].s,
+                sigs[i].v - 27
+            );
+        }
+
+        signedMessage = abi.encodePacked(signedMessage, body);
+    }
+
+    function testParseVMsCorrectly() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        uint16 emitterChainId = 11;
+        bytes32 emitterAddress = 0x0000000000000000000000000000000000000000000000000000000000000eee;
+        uint64 sequence = 0;
+        uint8 consistencyLevel = 2;
+        uint32 guardianSetIndex = 0;
+        bytes memory data = hex"aaaaaa";
+
+        bytes memory signedMessage = signAndEncodeVM(
+            timestamp,
+            nonce,
+            emitterChainId,
+            emitterAddress,
+            sequence,
+            data,
+            uint256Array(testGuardian),
+            guardianSetIndex,
+            consistencyLevel
+        );
+
+        (IWormhole.VM memory parsed, bool valid, string memory reason) = proxied
+            .parseAndVerifyVM(signedMessage);
+
+        assertEq(parsed.version, 1, "Wrong VM version");
+        assertEq(parsed.timestamp, timestamp, "Wrong VM timestamp");
+        assertEq(parsed.nonce, nonce, "Wrong VM nonce");
+        assertEq(
+            parsed.emitterChainId,
+            emitterChainId,
+            "Wrong emitter chain id"
+        );
+        assertEq(
+            parsed.emitterAddress,
+            emitterAddress,
+            "Wrong emitter address"
+        );
+        assertEq(parsed.payload, data, "Wrong VM payload");
+        assertEq(parsed.guardianSetIndex, 0, "Wrong VM guardian set index");
+        assertEq(parsed.sequence, sequence, "Wrong VM sequence");
+        assertEq(
+            parsed.consistencyLevel,
+            consistencyLevel,
+            "Wrong VM consistency level"
+        );
+        assertEq(valid, true, "Signed vaa not valid");
+        assertEq(reason, "", "Wrong reason");
+    }
+
+    function testShouldFailQuorumOnVMsWithNoSigners() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        uint16 emitterChainId = 11;
+        bytes32 emitterAddress = 0x0000000000000000000000000000000000000000000000000000000000000eee;
+        uint64 sequence = 0;
+        uint8 consistencyLevel = 2;
+        uint32 guardianSetIndex = 0;
+        bytes memory data = hex"aaaaaa";
+
+        bytes memory signedMessage = signAndEncodeVM(
+            timestamp,
+            nonce,
+            emitterChainId,
+            emitterAddress,
+            sequence,
+            data,
+            new uint256[](0),
+            guardianSetIndex,
+            consistencyLevel
+        );
+
+        (, bool valid, string memory reason) = proxied.parseAndVerifyVM(
+            signedMessage
+        );
+
+        assertEq(valid, false, "Signed vaa shouldn't be valid");
+        assertEq(reason, "no quorum", "Wrong reason");
+    }
+
+    function testShouldFailToVerifyOnVMsWithBadSigner() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        uint16 emitterChainId = 11;
+        bytes32 emitterAddress = 0x0000000000000000000000000000000000000000000000000000000000000eee;
+        uint64 sequence = 0;
+        uint8 consistencyLevel = 2;
+        uint32 guardianSetIndex = 0;
+        bytes memory data = hex"aaaaaa";
+
+        bytes memory signedMessage = signAndEncodeVM(
+            timestamp,
+            nonce,
+            emitterChainId,
+            emitterAddress,
+            sequence,
+            data,
+            uint256Array(testBadSigner1PK),
+            guardianSetIndex,
+            consistencyLevel
+        );
+
+        (, bool valid, string memory reason) = proxied.parseAndVerifyVM(
+            signedMessage
+        );
+
+        assertEq(valid, false, "Signed vaa shouldn't be valid");
+        assertEq(reason, "VM signature invalid", "Wrong reason");
+    }
+
+    function testShouldErrorOnVMsWithInvalidGuardianSetIndex() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        uint16 emitterChainId = 11;
+        bytes32 emitterAddress = 0x0000000000000000000000000000000000000000000000000000000000000eee;
+        uint64 sequence = 0;
+        uint8 consistencyLevel = 2;
+        uint32 guardianSetIndex = 200;
+        bytes memory data = hex"aaaaaa";
+
+        bytes memory signedMessage = signAndEncodeVM(
+            timestamp,
+            nonce,
+            emitterChainId,
+            emitterAddress,
+            sequence,
+            data,
+            uint256Array(testGuardian),
+            guardianSetIndex,
+            consistencyLevel
+        );
+
+        (IWormhole.VM memory parsed, bool valid, string memory reason) = proxied
+            .parseAndVerifyVM(signedMessage);
+
+        assertEq(valid, false, "Signed vaa shouldn't be valid");
+        assertEq(reason, "invalid guardian set", "Wrong reason");
+    }
+
+    function testShouldRevertOnVMsWithDuplicateNonMonotonicSignatureIndexes()
+        public
+    {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        uint16 emitterChainId = 11;
+        bytes32 emitterAddress = 0x0000000000000000000000000000000000000000000000000000000000000eee;
+        uint64 sequence = 0;
+        uint8 consistencyLevel = 2;
+        uint32 guardianSetIndex = 0;
+        bytes memory data = hex"aaaaaa";
+        uint256[] memory signers = new uint256[](3);
+        signers[0] = testSigner1;
+        signers[1] = testSigner2;
+        signers[2] = testSigner3;
+        bytes memory signedMessage = signAndEncodeVMFixedIndex(
+            timestamp,
+            nonce,
+            emitterChainId,
+            emitterAddress,
+            sequence,
+            data,
+            signers,
+            guardianSetIndex,
+            consistencyLevel
+        );
+
+        vm.expectRevert("signature indices must be ascending");
+        (IWormhole.VM memory parsed, bool valid, string memory reason) = proxied
+            .parseAndVerifyVM(signedMessage);
+    }
+
+    function testShouldSetAndEnforceFees() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        uint256 messageFee = 1111;
+        bytes memory data = abi.encodePacked(
+            core,
+            actionMessageFee,
+            testChainId,
+            messageFee
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        uint256 before = proxied.messageFee();
+        proxied.submitSetMessageFee(vaa);
+        uint256 afterSettingFee = proxied.messageFee();
+        assertTrue(before != afterSettingFee, "message fee did not update");
+        assertEq(afterSettingFee, messageFee, "wrong message fee");
+    }
+
+    function testShouldTransferOutCollectedFees() public {
+        address receiver = address(0x1234123412341234123412341234123412341234);
+
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        uint256 amount = 11;
+
+        vm.deal(address(proxied), amount);
+        bytes memory data = abi.encodePacked(
+            core,
+            actionTransferFee,
+            testChainId,
+            amount,
+            addressToBytes32(receiver)
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        uint256 receiverBefore = receiver.balance;
+        uint256 whBefore = address(proxied).balance;
+        proxied.submitTransferFees(vaa);
+        uint256 receiverAfter = receiver.balance;
+        uint256 whAfter = address(proxied).balance;
+        assertEq(
+            receiverAfter - receiverBefore,
+            amount,
+            "Receiver balance didn't change correctly"
+        );
+        assertEq(
+            whBefore - whAfter,
+            amount,
+            "WH Core balance didn't change correctly"
+        );
+    }
+
+    function testShouldRevertWhenSubmittingANewGuardianSetWithTheZeroAddress()
+        public
+    {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        address zeroAddress = address(0x0);
+
+        uint32 oldGuardianSetIndex = proxied.getCurrentGuardianSetIndex();
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionGuardianSetUpgrade,
+            testChainId,
+            oldGuardianSetIndex + 1,
+            uint8(3),
+            vm.addr(testSigner1),
+            vm.addr(testSigner2),
+            zeroAddress
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        vm.expectRevert("Invalid key");
+        proxied.submitNewGuardianSet(vaa);
+    }
+
+    function testShouldAcceptANewGuardianSet() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        address zeroAddress = address(0x0);
+
+        uint32 oldGuardianSetIndex = proxied.getCurrentGuardianSetIndex();
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionGuardianSetUpgrade,
+            testChainId,
+            oldGuardianSetIndex + 1,
+            uint8(3),
+            vm.addr(testSigner1),
+            vm.addr(testSigner2),
+            vm.addr(testSigner3)
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        proxied.submitNewGuardianSet(vaa);
+
+        uint32 newIndex = proxied.getCurrentGuardianSetIndex();
+        assertEq(
+            oldGuardianSetIndex + 1,
+            newIndex,
+            "New index is one more than old index"
+        );
+
+        IWormhole.GuardianSet memory guardianSet = proxied.getGuardianSet(
+            newIndex
+        );
+
+        assertEq(guardianSet.expirationTime, 0, "Wrong expiration time");
+        assertEq(guardianSet.keys[0], vm.addr(testSigner1), "Wrong guardian");
+        assertEq(guardianSet.keys[1], vm.addr(testSigner2), "Wrong guardian");
+        assertEq(guardianSet.keys[2], vm.addr(testSigner3), "Wrong guardian");
+
+        IWormhole.GuardianSet memory oldGuardianSet = proxied.getGuardianSet(
+            oldGuardianSetIndex
+        );
+
+        assertTrue(
+            (oldGuardianSet.expirationTime > block.timestamp + 86000) &&
+                (oldGuardianSet.expirationTime < block.timestamp + 88000),
+            "Wrong expiration time"
+        );
+        assertEq(
+            oldGuardianSet.keys[0],
+            vm.addr(testGuardian),
+            "Wrong guardian"
+        );
+    }
+
+    function testShouldAcceptSmartContractUpgrades() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        MockImplementation mock = new MockImplementation();
+
+        uint256 oldGuardianSetIndex = proxied.getCurrentGuardianSetIndex();
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(proxied), IMPLEMENTATION_STORAGE_SLOT);
+
+        proxied.submitContractUpgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(proxied),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, addressToBytes32(address(mock)));
+        assertEq(
+            MockImplementation(payable(address(proxied)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+    }
+
+    function testShouldRevertRecoverChainIDGovernancePacketsOnCanonicalChainsNonFork()
+        public
+    {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        address zeroAddress = address(0x0);
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionRecoverChainId,
+            testEvmChainId,
+            testChainId
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        vm.expectRevert("not a fork");
+        proxied.submitRecoverChainId(vaa);
+    }
+
+    function testShouldRevertGovernancePacketsFromOldGuardianSet() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        address zeroAddress = address(0x0);
+
+        // upgrade guardian set
+        bytes memory data = abi.encodePacked(
+            core,
+            actionGuardianSetUpgrade,
+            testChainId,
+            uint32(1),
+            uint8(3),
+            vm.addr(testSigner1),
+            vm.addr(testSigner2),
+            vm.addr(testSigner3)
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        proxied.submitNewGuardianSet(vaa);
+
+        data = abi.encodePacked(
+            core,
+            actionTransferFee,
+            testChainId,
+            uint256(1),
+            address(0)
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        vm.expectRevert("not signed by current guardian set");
+        proxied.submitTransferFees(vaa);
+    }
+
+    function testShouldTimeOutOldGuardians() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        address zeroAddress = address(0x0);
+        uint16 emitterChainId = 11;
+        bytes32 emitterAddress = 0x0000000000000000000000000000000000000000000000000000000000000eee;
+
+        // upgrade guardian set
+        bytes memory data = abi.encodePacked(
+            core,
+            actionGuardianSetUpgrade,
+            testChainId,
+            uint32(1),
+            uint8(3),
+            vm.addr(testSigner1),
+            vm.addr(testSigner2),
+            vm.addr(testSigner3)
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        proxied.submitNewGuardianSet(vaa);
+
+        data = hex"aaaaaa";
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        (, bool valid, ) = proxied.parseAndVerifyVM(vaa);
+
+        assertEq(valid, true, "Vaa should be valid");
+
+        skip(100000);
+
+        (, valid, ) = proxied.parseAndVerifyVM(vaa);
+
+        assertEq(valid, false, "Vaa should be expired");
+    }
+
+    function testShouldRevertGovernancePacketsFromWrongGovernanceChain()
+        public
+    {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        address zeroAddress = address(0x0);
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionTransferFee,
+            testChainId,
+            uint256(1),
+            address(0)
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            999,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        vm.expectRevert("wrong governance chain");
+        proxied.submitTransferFees(vaa);
+    }
+
+    function testShouldRevertGovernancePacketsFromWrongGovernanceContract()
+        public
+    {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        address zeroAddress = address(0x0);
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionTransferFee,
+            testChainId,
+            uint256(1),
+            address(0)
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            core,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        vm.expectRevert("wrong governance contract");
+        proxied.submitTransferFees(vaa);
+    }
+
+    function testShouldRevertGovernancePacketsThatAlreadyHaveBeenApplied()
+        public
+    {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+        address zeroAddress = address(0x0);
+
+        uint256 amount = 1;
+        vm.deal(address(proxied), amount);
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionTransferFee,
+            testChainId,
+            amount,
+            addressToBytes32(address(0))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        proxied.submitTransferFees(vaa);
+
+        vm.expectRevert("governance action already consumed");
+        proxied.submitTransferFees(vaa);
+    }
+
+    function addressToBytes32(address input) internal returns (bytes32 output) {
+        return bytes32(uint256(uint160(input)));
+    }
+
+    function testShouldRejectSmartContractUpgradesOnForks() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        // Perform a successful upgrade
+        MockImplementation mock = new MockImplementation();
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(proxied), IMPLEMENTATION_STORAGE_SLOT);
+
+        proxied.submitContractUpgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(proxied),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, addressToBytes32(address(mock)));
+        assertEq(
+            MockImplementation(payable(address(proxied)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+
+        // Overwrite EVM Chain ID
+        MockImplementation(payable(address(proxied))).testOverwriteEVMChainId(
+            fakeChainId,
+            fakeEvmChainId
+        );
+        assertEq(
+            proxied.chainId(),
+            fakeChainId,
+            "Overwrite didn't work for chain ID"
+        );
+        assertEq(
+            proxied.evmChainId(),
+            fakeEvmChainId,
+            "Overwrite didn't work for evm chain ID"
+        );
+
+        data = abi.encodePacked(
+            core,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        vm.expectRevert("invalid fork");
+        proxied.submitContractUpgrade(vaa);
+    }
+
+    function testShouldAllowRecoverChainIDGovernancePacketsForks() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        // Perform a successful upgrade
+        MockImplementation mock = new MockImplementation();
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(proxied), IMPLEMENTATION_STORAGE_SLOT);
+
+        proxied.submitContractUpgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(proxied),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, addressToBytes32(address(mock)));
+        assertEq(
+            MockImplementation(payable(address(proxied)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+
+        // Overwrite EVM Chain ID
+        MockImplementation(payable(address(proxied))).testOverwriteEVMChainId(
+            fakeChainId,
+            fakeEvmChainId
+        );
+        assertEq(
+            proxied.chainId(),
+            fakeChainId,
+            "Overwrite didn't work for chain ID"
+        );
+        assertEq(
+            proxied.evmChainId(),
+            fakeEvmChainId,
+            "Overwrite didn't work for evm chain ID"
+        );
+
+        // recover chain ID
+        data = abi.encodePacked(
+            core,
+            actionRecoverChainId,
+            testEvmChainId,
+            testChainId
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        proxied.submitRecoverChainId(vaa);
+
+        assertEq(
+            proxied.chainId(),
+            testChainId,
+            "Recover didn't work for chain ID"
+        );
+        assertEq(
+            proxied.evmChainId(),
+            testEvmChainId,
+            "Recover didn't work for evm chain ID"
+        );
+    }
+
+    function testShouldAcceptSmartContractUpgradesAfterChainIdHasBeenRecovered()
+        public
+    {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        // Perform a successful upgrade
+        MockImplementation mock = new MockImplementation();
+
+        bytes memory data = abi.encodePacked(
+            core,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(proxied), IMPLEMENTATION_STORAGE_SLOT);
+
+        proxied.submitContractUpgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(proxied),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, addressToBytes32(address(mock)));
+        assertEq(
+            MockImplementation(payable(address(proxied)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+
+        // Overwrite EVM Chain ID
+        MockImplementation(payable(address(proxied))).testOverwriteEVMChainId(
+            fakeChainId,
+            fakeEvmChainId
+        );
+        assertEq(
+            proxied.chainId(),
+            fakeChainId,
+            "Overwrite didn't work for chain ID"
+        );
+        assertEq(
+            proxied.evmChainId(),
+            fakeEvmChainId,
+            "Overwrite didn't work for evm chain ID"
+        );
+
+        // recover chain ID
+        data = abi.encodePacked(
+            core,
+            actionRecoverChainId,
+            testEvmChainId,
+            testChainId
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        proxied.submitRecoverChainId(vaa);
+
+        assertEq(
+            proxied.chainId(),
+            testChainId,
+            "Recover didn't work for chain ID"
+        );
+        assertEq(
+            proxied.evmChainId(),
+            testEvmChainId,
+            "Recover didn't work for evm chain ID"
+        );
+
+        // Perform a successful upgrade
+        mock = new MockImplementation();
+
+        data = abi.encodePacked(
+            core,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        before = vm.load(address(proxied), IMPLEMENTATION_STORAGE_SLOT);
+
+        proxied.submitContractUpgrade(vaa);
+
+        afterUpgrade = vm.load(address(proxied), IMPLEMENTATION_STORAGE_SLOT);
+        assertEq(afterUpgrade, addressToBytes32(address(mock)));
+        assertEq(
+            MockImplementation(payable(address(proxied)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
     }
 }

+ 976 - 0
ethereum/forge-test/NFT.t.sol

@@ -0,0 +1,976 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "../contracts/nft/NFTBridge.sol";
+import "../contracts/nft/NFTBridgeSetup.sol";
+import "../contracts/nft/NFTBridgeImplementation.sol";
+import "../contracts/nft/NFTBridgeEntrypoint.sol";
+import "../contracts/interfaces/IWormhole.sol";
+import "../contracts/nft/interfaces/INFTBridge.sol";
+
+import "../contracts/nft/interfaces/INFTBridge.sol";
+import "../contracts/nft/token/NFTImplementation.sol";
+import "../contracts/nft/mock/MockNFTImplementation.sol";
+import "../contracts/nft/mock/MockNFTBridgeImplementation.sol";
+import "forge-std/Test.sol";
+import "./Implementation.t.sol";
+import "../contracts/bridge/mock/MockWETH9.sol";
+
+contract TestNFTBridge is Test {
+    NFTBridgeSetup bridgeSetup;
+    NFTBridgeImplementation bridgeImpl;
+    NFTImplementation tokenImpl;
+    INFTBridge bridge;
+    IWormhole wormhole;
+
+    TestImplementation implementationTest;
+
+    uint16 testChainId;
+    uint256 testEvmChainId;
+    uint16 governanceChainId;
+    bytes32 governanceContract;
+    uint8 constant finality = 15;
+
+    // "NFTBridge" (left padded)
+    bytes32 constant NFTBridgeModule =
+        0x00000000000000000000000000000000000000000000004e4654427269646765;
+    uint8 actionRegisterChain = 1;
+    uint8 actionContractUpgrade = 2;
+    uint8 actionRecoverChainId = 3;
+
+    uint16 fakeChainId = 1337;
+    uint256 fakeEvmChainId = 10001;
+
+    uint16 testForeignChainId = 1;
+    bytes32 testForeignBridgeContract =
+        0x000000000000000000000000000000000000000000000000000000000000ffff;
+    uint16 testBridgedAssetChain = 3;
+    bytes32 testBridgedAssetAddress =
+        0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e;
+
+    uint256 public constant testGuardian =
+        93941733246223705020089879371323733820373732307041878556247502674739205313440;
+
+    function setUp() public {
+        // Setup wormhole
+        implementationTest = new TestImplementation();
+        implementationTest.setUp();
+
+        // Get wormhole from implementation tests
+        wormhole = IWormhole(address(implementationTest.proxied()));
+
+        // Deploy setup
+        bridgeSetup = new NFTBridgeSetup();
+        // Deploy implementation contract
+        bridgeImpl = new NFTBridgeImplementation();
+        // Deploy token implementation
+        tokenImpl = new NFTImplementation();
+
+        testChainId = implementationTest.testChainId();
+        testEvmChainId = implementationTest.testEvmChainId();
+        vm.chainId(testEvmChainId);
+        governanceChainId = implementationTest.governanceChainId();
+        governanceContract = implementationTest.governanceContract();
+
+        bytes memory setupAbi = abi.encodeWithSelector(
+            NFTBridgeSetup.setup.selector,
+            address(bridgeImpl),
+            testChainId,
+            address(wormhole),
+            governanceChainId,
+            governanceContract,
+            address(tokenImpl),
+            finality,
+            testEvmChainId
+        );
+
+        // Deploy proxy
+        bridge = INFTBridge(
+            address(new NFTBridgeEntrypoint(address(bridgeSetup), setupAbi))
+        );
+    }
+
+    function testShouldBeInitializedWithTheCorrectSignersAndValues() public {
+        assertEq(bridge.tokenImplementation(), address(tokenImpl));
+        // test beacon functionality
+        assertEq(bridge.implementation(), address(tokenImpl));
+        assertEq(bridge.chainId(), testChainId);
+        assertEq(bridge.evmChainId(), testEvmChainId);
+        assertEq(bridge.finality(), finality);
+
+        // governance
+        uint16 readGovernanceChainId = bridge.governanceChainId();
+        bytes32 readGovernanceContract = bridge.governanceContract();
+        assertEq(
+            readGovernanceChainId,
+            governanceChainId,
+            "Wrong governance chain ID"
+        );
+        assertEq(
+            readGovernanceContract,
+            governanceContract,
+            "Wrong governance contract"
+        );
+    }
+
+    function testShouldRegisterAForeignBridgeImplementationCorrectly() public {
+        bytes memory data = abi.encodePacked(
+            NFTBridgeModule,
+            actionRegisterChain,
+            uint16(0),
+            testForeignChainId,
+            testForeignBridgeContract
+        );
+        bytes memory vaa = signAndEncodeVM(
+            1,
+            1,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        assertEq(
+            bridge.bridgeContracts(testForeignChainId),
+            bytes32(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            )
+        );
+        bridge.registerChain(vaa);
+        assertEq(
+            bridge.bridgeContracts(testForeignChainId),
+            testForeignBridgeContract
+        );
+    }
+
+    function testShouldAcceptAValidUpgrade() public {
+        MockNFTBridgeImplementation mock = new MockNFTBridgeImplementation();
+        bytes memory data = abi.encodePacked(
+            NFTBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            1,
+            1,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        assertEq(
+            vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT),
+            addressToBytes32(address(bridgeImpl))
+        );
+        bridge.upgrade(vaa);
+        assertEq(
+            vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT),
+            addressToBytes32(address(mock))
+        );
+
+        assertTrue(
+            MockNFTBridgeImplementation(address(bridge))
+                .testNewImplementationActive(),
+            "implementation not active"
+        );
+    }
+
+    function testBridgedTokensShouldOnlyBeMintAndBurnableByOwner() public {
+        address owner = address(this);
+        address notOwner = address(0x1);
+        tokenImpl.initialize("TestToken", "TT", owner, 0, 0x0);
+        tokenImpl.mint(owner, 10, "");
+
+        vm.expectRevert("caller is not the owner");
+        vm.prank(address(notOwner));
+        tokenImpl.mint(owner, 11, "");
+
+        vm.expectRevert("caller is not the owner");
+        vm.prank(address(notOwner));
+        tokenImpl.burn(10);
+
+        tokenImpl.burn(10);
+    }
+
+    event LogMessagePublished(
+        address indexed sender,
+        uint64 sequence,
+        uint32 nonce,
+        bytes payload,
+        uint8 consistencyLevel
+    );
+
+    function testShouldDepositAndLogTransfersCorrectly() public {
+        testShouldRegisterAForeignBridgeImplementationCorrectly();
+        testBridgedTokensShouldOnlyBeMintAndBurnableByOwner();
+
+        uint256 tokenId = 1000000000000000000;
+
+        tokenImpl.mint(address(this), tokenId, "abcd");
+        tokenImpl.approve(address(bridge), tokenId);
+
+        address ownerBefore = tokenImpl.ownerOf(tokenId);
+        assertEq(ownerBefore, address(this));
+
+        uint16 toChain = testForeignChainId;
+        bytes32 toAddress = testForeignBridgeContract;
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            addressToBytes32(address(tokenImpl)),
+            testChainId,
+            bytes32("TT"),
+            bytes32("TestToken"),
+            tokenId,
+            uint8(4),
+            hex"61626364",
+            toAddress,
+            toChain
+        );
+        vm.expectEmit();
+        emit LogMessagePublished(
+            address(bridge),
+            uint64(0),
+            uint32(234),
+            transferPayload,
+            uint8(finality)
+        );
+        bridge.transferNFT(
+            address(tokenImpl),
+            tokenId,
+            toChain,
+            toAddress,
+            234
+        );
+
+        address ownerAfter = tokenImpl.ownerOf(tokenId);
+        assertEq(ownerAfter, address(bridge));
+    }
+
+    function testShouldTransferOutLockedAssetsForAValidTransferVM() public {
+        testShouldDepositAndLogTransfersCorrectly();
+
+        uint256 tokenId = 1000000000000000000;
+
+        uint16 toChain = testChainId;
+        bytes32 toAddress = addressToBytes32(address(this));
+
+        address ownerBefore = tokenImpl.ownerOf(tokenId);
+        assertEq(ownerBefore, address(bridge));
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            addressToBytes32(address(tokenImpl)),
+            testChainId,
+            bytes32(0x0),
+            bytes32(0x0),
+            tokenId,
+            uint8(0),
+            hex"",
+            toAddress,
+            toChain
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            294, // sequence
+            transferPayload,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        bridge.completeTransfer(vaa);
+
+        address ownerAfter = tokenImpl.ownerOf(tokenId);
+        assertEq(ownerAfter, address(this));
+    }
+
+    function testShouldMintBridgedAssetWrappersOnTransferFromAnotherChainAndHandleFeesCorrectly()
+        public
+    {
+        testShouldTransferOutLockedAssetsForAValidTransferVM();
+
+        uint256 tokenId = 1000000000000000001;
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            testBridgedAssetAddress,
+            testBridgedAssetChain,
+            // symbol
+            bytes32(
+                0x464f520000000000000000000000000000000000000000000000000000000000
+            ),
+            // name
+            bytes32(
+                0x466f726569676e20436861696e204e4654000000000000000000000000000000
+            ),
+            tokenId,
+            // no URL
+            uint8(0),
+            hex"",
+            addressToBytes32(address(this)),
+            testChainId
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            0,
+            transferPayload,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        address sender = address(0x1);
+        vm.prank(sender);
+        bridge.completeTransfer(vaa);
+
+        address wrappedAddress = bridge.wrappedAsset(
+            testBridgedAssetChain,
+            testBridgedAssetAddress
+        );
+        NFTImplementation wrapped = NFTImplementation(wrappedAddress);
+
+        assertTrue(
+            bridge.isWrappedAsset(address(wrapped)),
+            "not wrapped asset"
+        );
+
+        address ownerAfter = wrapped.ownerOf(tokenId);
+        assertEq(ownerAfter, address(this));
+
+        assertEq(wrapped.symbol(), "FOR");
+        assertEq(wrapped.name(), "Foreign Chain NFT");
+        assertEq(wrapped.chainId(), testBridgedAssetChain);
+        assertEq(wrapped.nativeContract(), testBridgedAssetAddress);
+
+        tokenId = 1000000000000000002;
+        transferPayload = abi.encodePacked(
+            uint8(1),
+            testBridgedAssetAddress,
+            testBridgedAssetChain,
+            // symbol
+            bytes32(
+                0x464f520000000000000000000000000000000000000000000000000000000000
+            ),
+            // name
+            bytes32(
+                0x466f726569676e20436861696e204e4654000000000000000000000000000000
+            ),
+            tokenId,
+            // no URL
+            uint8(0),
+            hex"",
+            addressToBytes32(address(this)),
+            testChainId
+        );
+
+        vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            0,
+            transferPayload,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        sender = address(0x1);
+        vm.prank(sender);
+        bridge.completeTransfer(vaa);
+
+        ownerAfter = wrapped.ownerOf(tokenId);
+        assertEq(ownerAfter, address(this));
+    }
+
+    function testShouldMintBridgedAssetsFromSolanaUnderUnifiedNameCachingTheOriginal()
+        public
+    {
+        testShouldTransferOutLockedAssetsForAValidTransferVM();
+
+        uint256 tokenId = 1000000000000000001;
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            testBridgedAssetAddress,
+            uint16(1), // solana
+            // symbol
+            bytes32(
+                0x464f520000000000000000000000000000000000000000000000000000000000
+            ),
+            // name
+            bytes32(
+                0x466f726569676e20436861696e204e4654000000000000000000000000000000
+            ),
+            tokenId,
+            // no URL
+            uint8(0),
+            hex"",
+            addressToBytes32(address(this)),
+            testChainId
+        );
+
+        bytes memory vaa = signAndEncodeVM(
+            0,
+            0,
+            testForeignChainId,
+            testForeignBridgeContract,
+            0,
+            transferPayload,
+            uint256Array(testGuardian),
+            0,
+            0
+        );
+
+        address sender = address(0x1);
+        vm.prank(sender);
+        bridge.completeTransfer(vaa);
+
+        address wrappedAddress = bridge.wrappedAsset(
+            1,
+            testBridgedAssetAddress
+        );
+        NFTImplementation wrapped = NFTImplementation(wrappedAddress);
+
+        INFTBridge.SPLCache memory cache = bridge.splCache(tokenId);
+        assertEq(
+            cache.symbol,
+            bytes32(
+                0x464f520000000000000000000000000000000000000000000000000000000000
+            )
+        );
+        assertEq(
+            cache.name,
+            bytes32(
+                0x466f726569676e20436861696e204e4654000000000000000000000000000000
+            )
+        );
+
+        address ownerAfter = wrapped.ownerOf(tokenId);
+        assertEq(ownerAfter, address(this));
+
+        assertEq(wrapped.symbol(), "WORMSPLNFT");
+        assertEq(wrapped.name(), "Wormhole Bridged Solana-NFT");
+        assertEq(wrapped.chainId(), 1);
+        assertEq(wrapped.nativeContract(), testBridgedAssetAddress);
+    }
+
+    function testCachedSPLNamesAreLoadedWhenTransferringOutCacheIsCleared()
+        public
+    {
+        testShouldMintBridgedAssetsFromSolanaUnderUnifiedNameCachingTheOriginal();
+        address wrappedAddress = bridge.wrappedAsset(
+            1,
+            testBridgedAssetAddress
+        );
+        NFTImplementation wrapped = NFTImplementation(wrappedAddress);
+
+        uint256 tokenId = 1000000000000000001;
+
+        bytes memory transferPayload = abi.encodePacked(
+            uint8(1),
+            testBridgedAssetAddress,
+            uint16(1),
+            bytes32(
+                0x464f520000000000000000000000000000000000000000000000000000000000
+            ),
+            bytes32(
+                0x466f726569676e20436861696e204e4654000000000000000000000000000000
+            ),
+            tokenId,
+            uint8(0),
+            hex"",
+            testBridgedAssetAddress, // to address
+            testBridgedAssetChain // to chain
+        );
+
+        wrapped.approve(address(bridge), tokenId);
+
+        vm.expectEmit();
+        emit LogMessagePublished(
+            address(bridge),
+            uint64(1),
+            uint32(2345),
+            transferPayload,
+            uint8(finality)
+        );
+        bridge.transferNFT(
+            wrappedAddress,
+            tokenId,
+            testBridgedAssetChain, // to chain
+            testBridgedAssetAddress, // to address
+            2345
+        );
+
+        INFTBridge.SPLCache memory cache = bridge.splCache(tokenId);
+        assertEq(
+            cache.symbol,
+            bytes32(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            )
+        );
+        assertEq(
+            cache.name,
+            bytes32(
+                0x0000000000000000000000000000000000000000000000000000000000000000
+            )
+        );
+    }
+
+    function testShouldFailDepositUnapprovedNFTs() public {
+        testShouldRegisterAForeignBridgeImplementationCorrectly();
+        testBridgedTokensShouldOnlyBeMintAndBurnableByOwner();
+
+        uint256 tokenId = 1000000000000000000;
+
+        tokenImpl.mint(address(this), tokenId, "abcd");
+        // tokenImpl.approve(address(bridge), tokenId);
+
+        address ownerBefore = tokenImpl.ownerOf(tokenId);
+        assertEq(ownerBefore, address(this));
+
+        uint16 toChain = testForeignChainId;
+        bytes32 toAddress = testForeignBridgeContract;
+
+        vm.expectRevert("ERC721: transfer caller is not owner nor approved");
+        bridge.transferNFT(
+            address(tokenImpl),
+            tokenId,
+            toChain,
+            toAddress,
+            234
+        );
+    }
+
+    function testShouldRefuseToBurnWrappersNotHeldByMsgSender() public {
+        testShouldMintBridgedAssetWrappersOnTransferFromAnotherChainAndHandleFeesCorrectly();
+        address wrappedAddress = bridge.wrappedAsset(
+            testBridgedAssetChain,
+            testBridgedAssetAddress
+        );
+        NFTImplementation wrapped = NFTImplementation(wrappedAddress);
+
+        uint256 tokenId = 1000000000000000001;
+
+        wrapped.approve(address(bridge), tokenId);
+
+        vm.expectRevert("ERC721: transfer of token that is not own");
+        vm.prank(address(0x1));
+        bridge.transferNFT(
+            wrappedAddress,
+            tokenId,
+            testBridgedAssetChain, // to chain
+            testBridgedAssetAddress, // to address
+            2345
+        );
+    }
+
+    function testShouldDepositAndBurnApprovedBridgedAssetWrapperOnTransferToAnotherChain()
+        public
+    {
+        testShouldMintBridgedAssetWrappersOnTransferFromAnotherChainAndHandleFeesCorrectly();
+        address wrappedAddress = bridge.wrappedAsset(
+            testBridgedAssetChain,
+            testBridgedAssetAddress
+        );
+        NFTImplementation wrapped = NFTImplementation(wrappedAddress);
+
+        uint256 tokenId = 1000000000000000001;
+
+        wrapped.approve(address(bridge), tokenId);
+
+        bridge.transferNFT(
+            wrappedAddress,
+            tokenId,
+            testBridgedAssetChain, // to chain
+            testBridgedAssetAddress, // to address
+            2345
+        );
+
+        vm.expectRevert("ERC721: owner query for nonexistent token");
+        wrapped.ownerOf(tokenId);
+    }
+
+    function addressToBytes32(address input) internal returns (bytes32 output) {
+        return bytes32(uint256(uint160(input)));
+    }
+
+    function uint256Array(
+        uint256 member
+    ) internal returns (uint256[] memory arr) {
+        arr = new uint256[](1);
+        arr[0] = member;
+    }
+
+    function signAndEncodeVM(
+        uint32 timestamp,
+        uint32 nonce,
+        uint16 emitterChainId,
+        bytes32 emitterAddress,
+        uint64 sequence,
+        bytes memory data,
+        uint256[] memory signers,
+        uint32 guardianSetIndex,
+        uint8 consistencyLevel
+    ) public returns (bytes memory signedMessage) {
+        bytes memory body = abi.encodePacked(
+            timestamp,
+            nonce,
+            emitterChainId,
+            emitterAddress,
+            sequence,
+            consistencyLevel,
+            data
+        );
+        bytes32 bodyHash = keccak256(abi.encodePacked(keccak256(body)));
+
+        // Sign the hash with the devnet guardian private key
+        IWormhole.Signature[] memory sigs = new IWormhole.Signature[](
+            signers.length
+        );
+        for (uint256 i = 0; i < signers.length; i++) {
+            (sigs[i].v, sigs[i].r, sigs[i].s) = vm.sign(signers[i], bodyHash);
+            sigs[i].guardianIndex = 0;
+        }
+
+        signedMessage = abi.encodePacked(
+            uint8(1),
+            guardianSetIndex,
+            uint8(sigs.length)
+        );
+
+        for (uint256 i = 0; i < signers.length; i++) {
+            signedMessage = abi.encodePacked(
+                signedMessage,
+                sigs[i].guardianIndex,
+                sigs[i].r,
+                sigs[i].s,
+                sigs[i].v - 27
+            );
+        }
+
+        signedMessage = abi.encodePacked(signedMessage, body);
+    }
+
+    function testShouldRejectSmartContractUpgradesOnForks() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        // Perform a successful upgrade
+        MockNFTBridgeImplementation mock = new MockNFTBridgeImplementation();
+
+        bytes memory data = abi.encodePacked(
+            NFTBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+
+        bridge.upgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(bridge),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, addressToBytes32(address(mock)));
+        assertEq(
+            MockNFTBridgeImplementation(payable(address(bridge)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+
+        // Overwrite EVM Chain ID
+        MockNFTBridgeImplementation(payable(address(bridge)))
+            .testOverwriteEVMChainId(fakeChainId, fakeEvmChainId);
+        assertEq(
+            bridge.chainId(),
+            fakeChainId,
+            "Overwrite didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            fakeEvmChainId,
+            "Overwrite didn't work for evm chain ID"
+        );
+
+        data = abi.encodePacked(
+            NFTBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        vm.expectRevert("invalid fork");
+        bridge.upgrade(vaa);
+    }
+
+    function testShouldAllowRecoverChainIDGovernancePacketsForks() public {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        // Perform a successful upgrade
+        MockNFTBridgeImplementation mock = new MockNFTBridgeImplementation();
+
+        bytes memory data = abi.encodePacked(
+            NFTBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+
+        bridge.upgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(bridge),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, bytes32(uint256(uint160(address(mock)))));
+        assertEq(
+            MockNFTBridgeImplementation(payable(address(bridge)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+
+        // Overwrite EVM Chain ID
+        MockNFTBridgeImplementation(payable(address(bridge)))
+            .testOverwriteEVMChainId(fakeChainId, fakeEvmChainId);
+        assertEq(
+            bridge.chainId(),
+            fakeChainId,
+            "Overwrite didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            fakeEvmChainId,
+            "Overwrite didn't work for evm chain ID"
+        );
+
+        // recover chain ID
+        data = abi.encodePacked(
+            NFTBridgeModule,
+            actionRecoverChainId,
+            testEvmChainId,
+            testChainId
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bridge.submitRecoverChainId(vaa);
+
+        assertEq(
+            bridge.chainId(),
+            testChainId,
+            "Recover didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            testEvmChainId,
+            "Recover didn't work for evm chain ID"
+        );
+    }
+
+    function testShouldAcceptSmartContractUpgradesAfterChainIdHasBeenRecovered()
+        public
+    {
+        uint32 timestamp = 1000;
+        uint32 nonce = 1001;
+
+        // Perform a successful upgrade
+        MockNFTBridgeImplementation mock = new MockNFTBridgeImplementation();
+
+        bytes memory data = abi.encodePacked(
+            NFTBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        bytes memory vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bytes32 IMPLEMENTATION_STORAGE_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
+        bytes32 before = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+
+        bridge.upgrade(vaa);
+
+        bytes32 afterUpgrade = vm.load(
+            address(bridge),
+            IMPLEMENTATION_STORAGE_SLOT
+        );
+        assertEq(afterUpgrade, bytes32(uint256(uint160(address(mock)))));
+        assertEq(
+            MockNFTBridgeImplementation(payable(address(bridge)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+
+        // Overwrite EVM Chain ID
+        MockNFTBridgeImplementation(payable(address(bridge)))
+            .testOverwriteEVMChainId(fakeChainId, fakeEvmChainId);
+        assertEq(
+            bridge.chainId(),
+            fakeChainId,
+            "Overwrite didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            fakeEvmChainId,
+            "Overwrite didn't work for evm chain ID"
+        );
+
+        // recover chain ID
+        data = abi.encodePacked(
+            NFTBridgeModule,
+            actionRecoverChainId,
+            testEvmChainId,
+            testChainId
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        bridge.submitRecoverChainId(vaa);
+
+        assertEq(
+            bridge.chainId(),
+            testChainId,
+            "Recover didn't work for chain ID"
+        );
+        assertEq(
+            bridge.evmChainId(),
+            testEvmChainId,
+            "Recover didn't work for evm chain ID"
+        );
+
+        // Perform a successful upgrade
+        mock = new MockNFTBridgeImplementation();
+
+        data = abi.encodePacked(
+            NFTBridgeModule,
+            actionContractUpgrade,
+            testChainId,
+            addressToBytes32(address(mock))
+        );
+        vaa = signAndEncodeVM(
+            timestamp,
+            nonce,
+            governanceChainId,
+            governanceContract,
+            0,
+            data,
+            uint256Array(testGuardian),
+            0,
+            2
+        );
+
+        before = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+
+        bridge.upgrade(vaa);
+
+        afterUpgrade = vm.load(address(bridge), IMPLEMENTATION_STORAGE_SLOT);
+        assertEq(afterUpgrade, bytes32(uint256(uint160(address(mock)))));
+        assertEq(
+            MockNFTBridgeImplementation(payable(address(bridge)))
+                .testNewImplementationActive(),
+            true,
+            "New implementation not active"
+        );
+    }
+
+    function onERC721Received(
+        address,
+        address,
+        uint256,
+        bytes calldata
+    ) external returns (bytes4) {
+        return 0x150b7a02;
+    }
+
+    fallback() external payable {}
+}

+ 91 - 0
ethereum/forge-test/TokenMigrator.t.sol

@@ -0,0 +1,91 @@
+// test/Messages.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "../contracts/bridge/token/TokenImplementation.sol";
+import "../contracts/Setters.sol";
+import "../contracts/bridge/utils/Migrator.sol";
+import "forge-std/Test.sol";
+
+contract TestTokenMigrator is Test {
+    TokenImplementation fromToken;
+    TokenImplementation toToken;
+    uint8 fromDecimals = 8;
+    uint8 toDecimals = 18;
+    Migrator migrator;
+
+    function setUp() public {
+        fromToken = new TokenImplementation();
+        fromToken.initialize(
+            "TestFrom",
+            "FROM",
+            fromDecimals,
+            0,
+            address(this),
+            0,
+            bytes32(0x0)
+        );
+
+        toToken = new TokenImplementation();
+        toToken.initialize(
+            "TestTo",
+            "TO",
+            toDecimals,
+            0,
+            address(this),
+            0,
+            bytes32(0x0)
+        );
+        migrator = new Migrator(address(fromToken), address(toToken));
+
+        assertEq(address(migrator.fromAsset()), address(fromToken));
+        assertEq(address(migrator.toAsset()), address(toToken));
+        assertEq(migrator.fromDecimals(), fromDecimals);
+        assertEq(migrator.toDecimals(), toDecimals);
+    }
+
+    function testShouldGiveOutLPTokens1to1ForAToTokenDeposit() public {
+        uint256 amount = 1000000000000000000;
+        toToken.mint(address(this), amount);
+        toToken.approve(address(migrator), amount);
+        migrator.add(amount);
+        assertEq(migrator.balanceOf(address(this)), amount);
+        assertEq(toToken.balanceOf(address(migrator)), amount);
+    }
+
+    function testShouldRefundToTokenForLPTokens() public {
+        testShouldGiveOutLPTokens1to1ForAToTokenDeposit();
+        uint256 amount = 500000000000000000;
+        migrator.remove(amount);
+        assertEq(migrator.balanceOf(address(this)), amount);
+        assertEq(toToken.balanceOf(address(migrator)), amount);
+        assertEq(toToken.balanceOf(address(this)), amount);
+    }
+
+    function testShouldRedeemFromTokenToToTokenAdjustingForDecimals() public {
+        testShouldRefundToTokenForLPTokens();
+        address newAddr = address(0x1);
+        fromToken.mint(newAddr, 50000000);
+        vm.prank(newAddr);
+        fromToken.approve(address(migrator), 50000000);
+        vm.prank(newAddr);
+        migrator.migrate(50000000);
+
+        assertEq(toToken.balanceOf(newAddr), 500000000000000000);
+        assertEq(toToken.balanceOf(address(migrator)), 0);
+        assertEq(fromToken.balanceOf(newAddr), 0);
+        assertEq(fromToken.balanceOf(address(migrator)), 50000000);
+    }
+
+    function testFromTokenShouldBeClaimableForLPTokensAdjustingForDecimals()
+        public
+    {
+        testShouldRedeemFromTokenToToTokenAdjustingForDecimals();
+        migrator.claim(500000000000000000);
+
+        assertEq(fromToken.balanceOf(address(this)), 50000000);
+        assertEq(fromToken.balanceOf(address(migrator)), 0);
+        assertEq(migrator.balanceOf(address(this)), 0);
+    }
+}