diff --git a/contracts/contracts/errors/IPCErrors.sol b/contracts/contracts/errors/IPCErrors.sol index 9b9ec83a6..98696bb43 100644 --- a/contracts/contracts/errors/IPCErrors.sol +++ b/contracts/contracts/errors/IPCErrors.sol @@ -94,7 +94,8 @@ enum InvalidXnetMessageReason { Value, Kind, CannotSendToItself, - CommonParentNotExist + CommonParentNotExist, + IncompatibleSupplySource } string constant ERR_PERMISSIONED_AND_BOOTSTRAPPED = "Method not allowed if permissioned is enabled and subnet bootstrapped"; diff --git a/contracts/contracts/gateway/GatewayMessengerFacet.sol b/contracts/contracts/gateway/GatewayMessengerFacet.sol index 6fb80dd9a..3afca4700 100644 --- a/contracts/contracts/gateway/GatewayMessengerFacet.sol +++ b/contracts/contracts/gateway/GatewayMessengerFacet.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.23; import {GatewayActorModifiers} from "../lib/LibGatewayActorStorage.sol"; import {IpcEnvelope, CallMsg, IpcMsgKind} from "../structs/CrossNet.sol"; import {IPCMsgType} from "../enums/IPCMsgType.sol"; -import {Subnet, SubnetID, AssetKind, IPCAddress} from "../structs/Subnet.sol"; +import {Subnet, SubnetID, AssetKind, IPCAddress, Asset} from "../structs/Subnet.sol"; import {InvalidXnetMessage, InvalidXnetMessageReason, CannotSendCrossMsgToItself, MethodNotAllowed, UnroutableMessage} from "../errors/IPCErrors.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {LibGateway, CrossMessageValidationOutcome} from "../lib/LibGateway.sol"; @@ -12,6 +12,7 @@ import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {AssetHelper} from "../lib/AssetHelper.sol"; import {CrossMsgHelper} from "../lib/CrossMsgHelper.sol"; import {FvmAddressHelper} from "../lib/FvmAddressHelper.sol"; +import {ISubnetActor} from "../interfaces/ISubnetActor.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -23,6 +24,7 @@ contract GatewayMessengerFacet is GatewayActorModifiers { using SubnetIDHelper for SubnetID; using EnumerableSet for EnumerableSet.Bytes32Set; using CrossMsgHelper for IpcEnvelope; + using AssetHelper for Asset; /** * @dev Sends a general-purpose cross-message from the local subnet to the destination subnet. @@ -47,10 +49,6 @@ contract GatewayMessengerFacet is GatewayActorModifiers { revert InvalidXnetMessage(InvalidXnetMessageReason.Sender); } - if (envelope.kind != IpcMsgKind.Call) { - revert InvalidXnetMessage(InvalidXnetMessageReason.Kind); - } - // Will revert if the message won't deserialize into a CallMsg. abi.decode(envelope.message, (CallMsg)); @@ -63,7 +61,7 @@ contract GatewayMessengerFacet is GatewayActorModifiers { nonce: 0 // nonce will be updated by LibGateway.commitValidatedCrossMessage }); - CrossMessageValidationOutcome outcome = committed.validateCrossMessage(); + (CrossMessageValidationOutcome outcome, IPCMsgType applyType) = committed.validateCrossMessage(); if (outcome != CrossMessageValidationOutcome.Valid) { if (outcome == CrossMessageValidationOutcome.InvalidDstSubnet) { @@ -75,6 +73,12 @@ contract GatewayMessengerFacet is GatewayActorModifiers { } } + if (applyType == IPCMsgType.TopDown) { + (, SubnetID memory nextHop) = committed.to.subnetId.down(s.networkName); + // lock funds on the current subnet gateway for the next hop + ISubnetActor(nextHop.getActor()).supplySource().lock(envelope.value); + } + // Commit xnet message for dispatch. bool shouldBurn = LibGateway.commitValidatedCrossMessage(committed); diff --git a/contracts/contracts/interfaces/ISubnetActor.sol b/contracts/contracts/interfaces/ISubnetActor.sol new file mode 100644 index 000000000..5582b4b8f --- /dev/null +++ b/contracts/contracts/interfaces/ISubnetActor.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import {Asset} from "../structs/Subnet.sol"; + +/// @title Subnet actor interface +interface ISubnetActor { + function supplySource() external view returns (Asset memory); +} diff --git a/contracts/contracts/lib/AssetHelper.sol b/contracts/contracts/lib/AssetHelper.sol index d3a299e55..b7d80e245 100644 --- a/contracts/contracts/lib/AssetHelper.sol +++ b/contracts/contracts/lib/AssetHelper.sol @@ -6,7 +6,7 @@ import {Asset, AssetKind} from "../structs/Subnet.sol"; import {EMPTY_BYTES} from "../constants/Constants.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {SubnetActorGetterFacet} from "../subnet/SubnetActorGetterFacet.sol"; +import {ISubnetActor} from "../interfaces/ISubnetActor.sol"; /// @notice Helpers to deal with a supply source. library AssetHelper { @@ -16,7 +16,7 @@ library AssetHelper { /// and checks if its supply kind matches the provided one. /// It reverts if the address does not correspond to a subnet actor. function hasSupplyOfKind(address subnetActor, AssetKind compare) internal view returns (bool) { - return SubnetActorGetterFacet(subnetActor).supplySource().kind == compare; + return ISubnetActor(subnetActor).supplySource().kind == compare; } /// @notice Checks that a given supply strategy is correctly formed and its preconditions are met. @@ -37,6 +37,10 @@ library AssetHelper { require(asset.kind == kind, "Unexpected asset"); } + function equals(Asset memory asset, Asset memory asset2) internal pure returns (bool) { + return asset.tokenAddress == asset2.tokenAddress && asset.kind == asset2.kind; + } + /// @notice Locks the specified amount from msg.sender into custody. /// Reverts with NoBalanceIncrease if the token balance does not increase. /// May return more than requested for inflationary tokens due to balance rise. @@ -236,4 +240,7 @@ library AssetHelper { return Asset({kind: AssetKind.Native, tokenAddress: address(0)}); } + function erc20(address token) internal pure returns (Asset memory) { + return Asset({kind: AssetKind.ERC20, tokenAddress: token}); + } } \ No newline at end of file diff --git a/contracts/contracts/lib/CrossMsgHelper.sol b/contracts/contracts/lib/CrossMsgHelper.sol index 2bab81830..144907add 100644 --- a/contracts/contracts/lib/CrossMsgHelper.sol +++ b/contracts/contracts/lib/CrossMsgHelper.sol @@ -72,8 +72,7 @@ library CrossMsgHelper { uint256 value = crossMsg.value; // if the message was executed successfully, the value stayed // in the subnet and there's no need to return it. - // or if the message is a call, the value is always 0 because transfers for calls are not allowed - if (outcome == OutcomeType.Ok || crossMsg.kind == IpcMsgKind.Call) { + if (outcome == OutcomeType.Ok) { value = 0; } return @@ -185,8 +184,9 @@ library CrossMsgHelper { if (crossMsg.kind == IpcMsgKind.Transfer) { return supplySource.transferFunds({recipient: payable(recipient), value: crossMsg.value}); } else if (crossMsg.kind == IpcMsgKind.Call || crossMsg.kind == IpcMsgKind.Result) { - // transferring funds is not allowed for Call messages - uint256 value = crossMsg.kind == IpcMsgKind.Call ? 0 : crossMsg.value; + // For a Result message, the idea is to perform a call as this returns control back to the caller. + // If it's an account, there will be no code to invoke, so this will be have like a bare transfer. + // But if the original caller was a contract, this give it control so it can handle the result // send the envelope directly to the entrypoint // use supplySource so the tokens in the message are handled successfully @@ -195,7 +195,7 @@ library CrossMsgHelper { supplySource.performCall( payable(recipient), abi.encodeCall(IIpcHandler.handleIpcMessage, (crossMsg)), - value + crossMsg.value ); } return (false, EMPTY_BYTES); @@ -224,7 +224,7 @@ library CrossMsgHelper { return true; } - function validateCrossMessage(IpcEnvelope memory crossMsg) internal view returns (CrossMessageValidationOutcome) { - return LibGateway.validateCrossMessage(crossMsg); + function validateCrossMessage(IpcEnvelope memory crossMsg) internal view returns (CrossMessageValidationOutcome, IPCMsgType) { + return LibGateway.checkCrossMessage(crossMsg); } } diff --git a/contracts/contracts/lib/LibGateway.sol b/contracts/contracts/lib/LibGateway.sol index cc4eb4cab..6105ff88d 100644 --- a/contracts/contracts/lib/LibGateway.sol +++ b/contracts/contracts/lib/LibGateway.sol @@ -13,6 +13,7 @@ import {CrossMsgHelper} from "../lib/CrossMsgHelper.sol"; import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol"; import {AssetHelper} from "../lib/AssetHelper.sol"; +import {ISubnetActor} from "../interfaces/ISubnetActor.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; // Validation outcomes for cross messages @@ -20,7 +21,8 @@ enum CrossMessageValidationOutcome { Valid, InvalidDstSubnet, CannotSendToItself, - CommonParentNotExist + CommonParentNotExist, + IncompatibleSupplySource } library LibGateway { @@ -251,9 +253,7 @@ library LibGateway { crossMessage.nonce = topDownNonce; subnet.topDownNonce = topDownNonce + 1; - if (crossMessage.kind != IpcMsgKind.Call) { - subnet.circSupply += crossMessage.value; - } + subnet.circSupply += crossMessage.value; emit NewTopDownMessage({subnet: subnet.id.getAddress(), message: crossMessage, id: crossMessage.toDeterministicHash()}); } @@ -464,7 +464,7 @@ library LibGateway { emit MessageStoredInPostbox({id: crossMsg.toDeterministicHash()}); return; } - + // execute the message and get the receipt. (bool success, bytes memory ret) = executeCrossMsg(crossMsg, supplySource); if (success) { @@ -567,47 +567,111 @@ library LibGateway { } } + /// Checks if the incoming and outgoing subnet supply sources can be mapped. + /// Caller should make sure the incoming/outgoing subnets and current subnet are immediate parent/child subnets. + function checkSubnetsSupplyCompatible( + bool isLCA, + IPCMsgType applyType, + SubnetID memory incoming, + SubnetID memory outgoing, + SubnetID memory current + ) internal view returns(bool) { + if (isLCA) { + // now, it's pivoting @ LCA (i.e. upwards => downwards) + // if incoming bottom up subnet and outgoing target subnet have the same + // asset, we will allow it. This is because if they are using the + // same asset, then the asset can be mapped in both subnets. + + (, SubnetID memory incDown) = incoming.down(current); + (, SubnetID memory outDown) = outgoing.down(current); + + Asset memory incAsset = ISubnetActor(incDown.getActor()).supplySource(); + Asset memory outAsset = ISubnetActor(outDown.getActor()).supplySource(); + + return incAsset.equals(outAsset); + } + + if (applyType == IPCMsgType.BottomUp) { + // The child subnet has supply source native, this is the same as + // the current subnet's native source, the mapping makes sense, propagate up. + (, SubnetID memory incDown) = incoming.down(current); + return incDown.getActor().hasSupplyOfKind(AssetKind.Native); + } + + // Topdown handling + + // The incoming subnet's supply source will be mapped to native coin in the + // next child subnet. If the down subnet has native, then the mapping makes + // sense. + (, SubnetID memory down) = outgoing.down(current); + return down.getActor().hasSupplyOfKind(AssetKind.Native); + } + /// @notice Validates a cross message before committing it. function validateCrossMessage(IpcEnvelope memory envelope) internal view returns (CrossMessageValidationOutcome) { - GatewayActorStorage storage s = LibGatewayActorStorage.appStorage(); - SubnetID memory toSubnetId = envelope.to.subnetId; + (CrossMessageValidationOutcome outcome, ) = checkCrossMessage(envelope); + return outcome; + } + /// @notice Validates a cross message and returns the applyType if the message is valid + function checkCrossMessage(IpcEnvelope memory envelope) internal view returns (CrossMessageValidationOutcome, IPCMsgType applyType) { + SubnetID memory toSubnetId = envelope.to.subnetId; if (toSubnetId.isEmpty()) { - return CrossMessageValidationOutcome.InvalidDstSubnet; + return (CrossMessageValidationOutcome.InvalidDstSubnet, applyType); } + GatewayActorStorage storage s = LibGatewayActorStorage.appStorage(); + SubnetID memory currentNetwork = s.networkName; + // We cannot send a cross message to the same subnet. - if (toSubnetId.equals(s.networkName)) { - return CrossMessageValidationOutcome.CannotSendToItself; + if (toSubnetId.equals(currentNetwork)) { + return (CrossMessageValidationOutcome.CannotSendToItself, applyType); } // Lowest common ancestor subnet - bool isLCA = toSubnetId.commonParent(envelope.from.subnetId).equals(s.networkName); - IPCMsgType applyType = envelope.applyType(s.networkName); - + bool isLCA = toSubnetId.commonParent(envelope.from.subnetId).equals(currentNetwork); + applyType = envelope.applyType(currentNetwork); + // If the directionality is top-down, or if we're inverting the direction // else we need to check if the common parent exists. if (applyType == IPCMsgType.TopDown || isLCA) { - (bool foundChildSubnetId, SubnetID memory childSubnetId) = toSubnetId.down(s.networkName); + (bool foundChildSubnetId, SubnetID memory childSubnetId) = toSubnetId.down(currentNetwork); if (!foundChildSubnetId) { - return CrossMessageValidationOutcome.InvalidDstSubnet; + return (CrossMessageValidationOutcome.InvalidDstSubnet, applyType); } (bool foundSubnet,) = LibGateway.getSubnet(childSubnetId); if (!foundSubnet) { - return CrossMessageValidationOutcome.InvalidDstSubnet; + return (CrossMessageValidationOutcome.InvalidDstSubnet, applyType); } } else { - SubnetID memory commonParent = toSubnetId.commonParent(s.networkName); + SubnetID memory commonParent = toSubnetId.commonParent(currentNetwork); if (commonParent.isEmpty()) { - return CrossMessageValidationOutcome.CommonParentNotExist; + return (CrossMessageValidationOutcome.CommonParentNotExist, applyType); } } - return CrossMessageValidationOutcome.Valid; + // starting/ending subnet, no need check supply sources + if (envelope.from.subnetId.equals(currentNetwork) || envelope.to.subnetId.equals(currentNetwork)) { + return (CrossMessageValidationOutcome.Valid, applyType); + } + + bool supplySourcesCompatible = checkSubnetsSupplyCompatible({ + isLCA: isLCA, + applyType: applyType, + incoming: envelope.from.subnetId, + outgoing: envelope.to.subnetId, + current: currentNetwork + }); + + if (!supplySourcesCompatible) { + return (CrossMessageValidationOutcome.IncompatibleSupplySource, applyType); + } + + return (CrossMessageValidationOutcome.Valid, applyType); } - // Function to map CrossMessageValidationOutcome to InvalidXnetMessageReason + // Function to map CrossMessageValidationOutcome to InvalidXnetMessageReason function validationOutcomeToInvalidXnetMsgReason(CrossMessageValidationOutcome outcome) internal pure returns (InvalidXnetMessageReason) { if (outcome == CrossMessageValidationOutcome.InvalidDstSubnet) { return InvalidXnetMessageReason.DstSubnet; @@ -615,6 +679,8 @@ library LibGateway { return InvalidXnetMessageReason.CannotSendToItself; } else if (outcome == CrossMessageValidationOutcome.CommonParentNotExist) { return InvalidXnetMessageReason.CommonParentNotExist; + } else if (outcome == CrossMessageValidationOutcome.IncompatibleSupplySource) { + return InvalidXnetMessageReason.IncompatibleSupplySource; } revert("Unhandled validation outcome"); @@ -627,10 +693,11 @@ library LibGateway { GatewayActorStorage storage s = LibGatewayActorStorage.appStorage(); uint256 keysLength = s.postboxKeys.length(); - bytes32[] memory ids = new bytes32[](keysLength); + + bytes32[] memory values = s.postboxKeys.values(); + for (uint256 i = 0; i < keysLength; ) { - bytes32 msgCid = s.postboxKeys.at(i); - ids[i] = msgCid; + bytes32 msgCid = values[i]; LibGateway.propagatePostboxMessage(msgCid); unchecked { diff --git a/contracts/test/integration/GatewayDiamondToken.t.sol b/contracts/test/integration/GatewayDiamondToken.t.sol index 67b53ad5a..7adf75eed 100644 --- a/contracts/test/integration/GatewayDiamondToken.t.sol +++ b/contracts/test/integration/GatewayDiamondToken.t.sol @@ -232,7 +232,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase { vm.prank(address(saDiamond)); vm.expectCall(recipient, abi.encodeCall(IIpcHandler.handleIpcMessage, (msgs[0])), 1); gatewayDiamond.checkpointer().commitCheckpoint(batch); - assertEq(token.balanceOf(recipient), 0); + assertEq(token.balanceOf(recipient), 8); } function test_propagation() public { diff --git a/contracts/test/integration/L2PlusSubnet.t.sol b/contracts/test/integration/L2PlusSubnet.t.sol index e36b3ac6a..d3da360e1 100644 --- a/contracts/test/integration/L2PlusSubnet.t.sol +++ b/contracts/test/integration/L2PlusSubnet.t.sol @@ -219,25 +219,6 @@ contract L2PlusSubnetTest is Test, IntegrationTestBase { sendCrossMessageFromChildToParentWithResult(params); } - function testL2PlusSubnet_TokenMixed_SendCrossMessageFromChildToParentWithOkResult() public { - MockIpcContractResult caller = new MockIpcContractResult(); - Params memory params = Params({ - root: rootNetwork, - subnet: tokenL2Subnet, - subnetL3: tokenL3SubnetsWithTokenParent[0], - caller: caller, - callerAddr: address(caller), - recipientAddr: address(new MockIpcContractPayable()), - amount: 3, - expectedOutcome: OutcomeType.Ok, - expectedRet: abi.encode(EMPTY_BYTES), - callerAmount: 1 ether, - fundAmount: 100000 - }); - - sendCrossMessageFromChildToParentWithResult(params); - } - function testL2PlusSubnet_Token_SendCrossMessageFromParentToChildWithOkResult() public { MockIpcContractResult caller = new MockIpcContractResult(); Params memory params = Params({ @@ -257,25 +238,6 @@ contract L2PlusSubnetTest is Test, IntegrationTestBase { sendCrossMessageFromParentToChildWithResult(params); } - function testL2PlusSubnet_TokenMixed_SendCrossMessageFromParentToChildWithOkResult() public { - MockIpcContractResult caller = new MockIpcContractResult(); - Params memory params = Params({ - root: rootNetwork, - subnet: tokenL2Subnet, - subnetL3: tokenL3SubnetsWithTokenParent[0], - caller: caller, - callerAddr: address(caller), - recipientAddr: address(new MockIpcContractPayable()), - amount: 3, - expectedOutcome: OutcomeType.Ok, - expectedRet: abi.encode(EMPTY_BYTES), - callerAmount: 1 ether, - fundAmount: 100000 - }); - - sendCrossMessageFromParentToChildWithResult(params); - } - function testL2PlusSubnet_Token_SendCrossMessageFromSiblingToSiblingWithOkResult() public { sendCrossMessageFromSiblingToSiblingWithOkResult(rootNetwork, tokenL2Subnet, nativeL3SubnetsWithTokenParent); } @@ -599,6 +561,16 @@ contract L2PlusSubnetTest is Test, IntegrationTestBase { 0 ); + Asset memory subnetSupply = params.subnet.subnetActor.getter().supplySource(); + + if (subnetSupply.kind == AssetKind.ERC20) { + // increase callerAddr's token balance + IERC20(subnetSupply.tokenAddress).transfer(params.callerAddr, params.amount); + // increase allowance so that send xnet msg will make it + vm.prank(params.callerAddr); + IERC20(subnetSupply.tokenAddress).approve(address(params.root.gatewayAddr), params.amount); + } + crossMessage.nonce = 1; // send the cross message from the root network to the L3 subnet vm.prank(params.callerAddr); @@ -610,7 +582,11 @@ contract L2PlusSubnetTest is Test, IntegrationTestBase { }); crossMessage.nonce = 0; - params.root.gateway.messenger().sendContractXnetMessage{value: params.amount}(crossMessage); + if (subnetSupply.kind == AssetKind.ERC20) { + params.root.gateway.messenger().sendContractXnetMessage(crossMessage); + } else { + params.root.gateway.messenger().sendContractXnetMessage{value: params.amount}(crossMessage); + } IpcEnvelope[] memory msgs = new IpcEnvelope[](1); msgs[0] = crossMessage; @@ -623,22 +599,18 @@ contract L2PlusSubnetTest is Test, IntegrationTestBase { params.subnetL3.subnetActorAddr, params.subnet.gatewayAddr ); - // apply the cross message in the L3 subnet executeTopDownMsgs(msgs, params.subnetL3.gateway); - // submit checkoint so the result message can be propagated to L2 submitBottomUpCheckpoint( callCreateBottomUpCheckpointFromChildSubnet(params.subnetL3.id, params.subnetL3.gateway), params.subnetL3.subnetActor ); - // submit checkoint so the result message can be propagated to root network submitBottomUpCheckpoint( callCreateBottomUpCheckpointFromChildSubnet(params.subnet.id, params.subnet.gateway), params.subnet.subnetActor ); - assertTrue(params.caller.hasResult(), "missing result"); assertTrue(params.caller.result().outcome == params.expectedOutcome, "wrong result outcome"); assertTrue(keccak256(params.caller.result().ret) == keccak256(params.expectedRet), "wrong result outcome"); diff --git a/contracts/test/integration/L2PlusXNet.t.sol b/contracts/test/integration/L2PlusXNet.t.sol new file mode 100644 index 000000000..8f485287a --- /dev/null +++ b/contracts/test/integration/L2PlusXNet.t.sol @@ -0,0 +1,602 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.23; + +import "forge-std/Test.sol"; +import "../../contracts/errors/IPCErrors.sol"; +import {IpcEnvelope, BottomUpMsgBatch, BottomUpCheckpoint, ParentFinality, IpcMsgKind, ResultMsg} from "../../contracts/structs/CrossNet.sol"; +import {SubnetID, Subnet, IPCAddress, Validator, FvmAddress} from "../../contracts/structs/Subnet.sol"; +import {SubnetIDHelper} from "../../contracts/lib/SubnetIDHelper.sol"; +import {AssetHelper} from "../../contracts/lib/AssetHelper.sol"; +import {Asset, AssetKind} from "../../contracts/structs/Subnet.sol"; +import {FvmAddressHelper} from "../../contracts/lib/FvmAddressHelper.sol"; +import {CrossMsgHelper} from "../../contracts/lib/CrossMsgHelper.sol"; +import {GatewayDiamond} from "../../contracts/GatewayDiamond.sol"; +import {SubnetActorDiamond} from "../../contracts/SubnetActorDiamond.sol"; +import {SubnetActorManagerFacet} from "../../contracts/subnet/SubnetActorManagerFacet.sol"; +import {SubnetActorCheckpointingFacet} from "../../contracts/subnet/SubnetActorCheckpointingFacet.sol"; +import {GatewayGetterFacet} from "../../contracts/gateway/GatewayGetterFacet.sol"; +import {LibGateway} from "../../contracts/lib/LibGateway.sol"; +import {TopDownFinalityFacet} from "../../contracts/gateway/router/TopDownFinalityFacet.sol"; +import {CheckpointingFacet} from "../../contracts/gateway/router/CheckpointingFacet.sol"; +import {XnetMessagingFacet} from "../../contracts/gateway/router/XnetMessagingFacet.sol"; +import {DiamondCutFacet} from "../../contracts/diamond/DiamondCutFacet.sol"; +import {GatewayMessengerFacet} from "../../contracts/gateway/GatewayMessengerFacet.sol"; +import {IntegrationTestBase, RootSubnetDefinition, TestSubnetDefinition} from "../IntegrationTestBase.sol"; +import {TestUtils, MockIpcContract, MockIpcContractPayable, MockIpcContractResult} from "../helpers/TestUtils.sol"; +import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol"; +import {MerkleTreeHelper} from "../helpers/MerkleTreeHelper.sol"; +import {GatewayFacetsHelper} from "../helpers/GatewayFacetsHelper.sol"; +import {SubnetActorFacetsHelper} from "../helpers/SubnetActorFacetsHelper.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ERC20PresetFixedSupply} from "../helpers/ERC20PresetFixedSupply.sol"; + +import {ActivityHelper} from "../helpers/ActivityHelper.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {ISubnetActor} from "../../contracts/interfaces/ISubnetActor.sol"; +import {IPCMsgType} from "../../contracts/enums/IPCMsgType.sol"; +import {IIpcHandler} from "../../sdk/interfaces/IIpcHandler.sol"; +import {IGateway} from "../../contracts/interfaces/IGateway.sol"; + +import "forge-std/console.sol"; + +struct SubnetsHierarchy { + /// @dev The lookup key to l1 SubnetNode + bytes32 l1NodeLookupKey; + /// @dev The lookup keys to the children and beyond + EnumerableSet.Bytes32Set subnetKeys; + mapping(address => bytes32) actorToLookupKeys; + mapping(bytes32 => SubnetNode) nodes; + mapping(bytes32 => SubnetDefinition) definitions; +} + +struct SubnetDefinition { + address gatewayAddr; + SubnetID id; +} + +struct SubnetNode { + /// @dev The list of child subnet actor address + EnumerableSet.AddressSet childActors; +} + +struct SubnetCreationParams { + SubnetID parent; + Asset supplySource; +} + +library LibSubnetsHierarchy { + using EnumerableSet for EnumerableSet.AddressSet; + using SubnetIDHelper for SubnetID; + + function initL1(SubnetsHierarchy storage self, SubnetDefinition memory l1) internal { + bytes32 lookupId = l1.id.toHash(); + + self.l1NodeLookupKey = lookupId; + storeNewNode(self, lookupId, l1.id, l1.gatewayAddr); + } + + function getSubnetGateway(SubnetsHierarchy storage self, SubnetID memory id) internal view returns (address) { + bytes32 lookupId = id.toHash(); + return getSubnetGateway(self, lookupId); + } + + function getSubnetGateway(SubnetsHierarchy storage self, bytes32 lookupId) internal view returns (address) { + require(self.definitions[lookupId].gatewayAddr != address(0), "subnet not found"); + return self.definitions[lookupId].gatewayAddr; + } + + function storeNewNode( + SubnetsHierarchy storage self, + bytes32 lookupId, + SubnetID memory id, + address gateway + ) internal { + self.definitions[lookupId].gatewayAddr = gateway; + self.definitions[lookupId].id = id; + } + + function linkNewChild(SubnetsHierarchy storage self, bytes32 lookupId, address subnetActor) internal { + self.nodes[lookupId].childActors.add(subnetActor); + } + + function registerNewSubnet( + SubnetsHierarchy storage self, + SubnetID memory parent, + address subnetActor, + address gateway + ) internal returns (SubnetID memory) { + // ensure parent exists + getSubnetGateway(self, parent); + + SubnetID memory child = SubnetIDHelper.createSubnetId(parent, subnetActor); + + bytes32 lookupId = child.toHash(); + + self.actorToLookupKeys[subnetActor] = lookupId; + storeNewNode(self, lookupId, child, gateway); + linkNewChild(self, lookupId, subnetActor); + + return child; + } + + function getGatewayBySubnetActor( + SubnetsHierarchy storage self, + address subnetActor + ) internal view returns (address) { + bytes32 lookupId = self.actorToLookupKeys[subnetActor]; + return getSubnetGateway(self, lookupId); + } +} + +contract L2PlusSubnetTest is Test, IntegrationTestBase, IIpcHandler { + using SubnetIDHelper for SubnetID; + using CrossMsgHelper for IpcEnvelope; + using GatewayFacetsHelper for GatewayDiamond; + using SubnetActorFacetsHelper for SubnetActorDiamond; + using AssetHelper for Asset; + using FvmAddressHelper for FvmAddress; + + using LibSubnetsHierarchy for SubnetsHierarchy; + + SubnetsHierarchy private subnets; + bytes32 private new_topdown_message_topic; + /// @dev The latest result message received from sending a cross network message + bytes private latestResultMessage; + + function setUp() public override { + // there seems no auto way to derive the abi in string, check this before running tests, make sure + // it's updated with the implementation + new_topdown_message_topic = keccak256( + "NewTopDownMessage(address,(uint8,((uint64,address[]),(uint8,bytes)),((uint64,address[]),(uint8,bytes)),uint64,uint256,bytes),bytes32)" + ); + + // get some free money. + vm.deal(address(this), 10 ether); + } + + // ======= implements ipc handler ======= + + /* solhint-disable-next-line unused-vars */ + function handleIpcMessage(IpcEnvelope calldata envelope) external payable returns (bytes memory ret) { + if (envelope.kind == IpcMsgKind.Result) { + latestResultMessage = envelope.message; + } + ret = bytes(""); + } + + receive() external payable {} + + // ======= internal util methods ======== + + function executeTopdownMessages(IpcEnvelope[] memory msgs, GatewayDiamond gw) internal { + uint256 mintedTokens; + + for (uint256 i; i < msgs.length; i++) { + mintedTokens += msgs[i].value; + } + console.log("minted tokens in executed top-downs: %d", mintedTokens); + + // The implementation of the function in fendermint is in + // https://github.com/consensus-shipyard/ipc/blob/main/fendermint/vm/interpreter/contracts/fvm/topdown.rs#L43 + + // This emulates minting tokens. + vm.deal(address(gw), mintedTokens); + + XnetMessagingFacet xnetMessenger = gw.xnetMessenger(); + + vm.prank(FilAddress.SYSTEM_ACTOR); + xnetMessenger.applyCrossMessages(msgs); + } + + function createBottomUpCheckpoint( + SubnetID memory subnet, + GatewayDiamond gw + ) internal returns (BottomUpCheckpoint memory checkpoint) { + uint256 e = getNextEpoch(block.number, DEFAULT_CHECKPOINT_PERIOD); + + GatewayGetterFacet getter = gw.getter(); + CheckpointingFacet checkpointer = gw.checkpointer(); + + BottomUpMsgBatch memory batch = getter.bottomUpMsgBatch(e); + + (, address[] memory addrs, uint256[] memory weights) = TestUtils.getFourValidators(vm); + + (bytes32 membershipRoot, ) = MerkleTreeHelper.createMerkleProofsForValidators(addrs, weights); + + checkpoint = BottomUpCheckpoint({ + subnetID: subnet, + blockHeight: batch.blockHeight, + blockHash: keccak256("block1"), + nextConfigurationNumber: 0, + msgs: batch.msgs, + activity: ActivityHelper.newCompressedActivityRollup(1, 3, bytes32(uint256(0))) + }); + + vm.prank(FilAddress.SYSTEM_ACTOR); + checkpointer.createBottomUpCheckpoint( + checkpoint, + membershipRoot, + weights[0] + weights[1] + weights[2], + ActivityHelper.dummyActivityRollup() + ); + + return checkpoint; + } + + function prepareValidatorsSignatures( + BottomUpCheckpoint memory checkpoint, + SubnetActorDiamond sa + ) internal returns (address[] memory, bytes[] memory) { + (uint256[] memory parentKeys, address[] memory parentValidators, ) = TestUtils.getThreeValidators(vm); + bytes[] memory parentPubKeys = new bytes[](3); + bytes[] memory parentSignatures = new bytes[](3); + + SubnetActorManagerFacet manager = sa.manager(); + + for (uint256 i = 0; i < 3; i++) { + vm.deal(parentValidators[i], 10 gwei); + parentPubKeys[i] = TestUtils.deriveValidatorPubKeyBytes(parentKeys[i]); + vm.prank(parentValidators[i]); + manager.join{value: 10}(parentPubKeys[i], 10); + } + + bytes32 hash = keccak256(abi.encode(checkpoint)); + + for (uint256 i = 0; i < 3; i++) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(parentKeys[i], hash); + parentSignatures[i] = abi.encodePacked(r, s, v); + } + + return (parentValidators, parentSignatures); + } + + function submitBottomUpCheckpoint(BottomUpCheckpoint memory checkpoint, SubnetActorDiamond sa) internal { + (address[] memory parentValidators, bytes[] memory parentSignatures) = prepareValidatorsSignatures( + checkpoint, + sa + ); + + SubnetActorCheckpointingFacet checkpointer = sa.checkpointer(); + + vm.deal(address(1), 1 ether); + vm.prank(address(1)); + checkpointer.submitCheckpoint(checkpoint, parentValidators, parentSignatures); + } + + function getNextEpoch(uint256 blockNumber, uint256 checkPeriod) internal pure returns (uint256) { + return ((uint64(blockNumber) / checkPeriod) + 1) * checkPeriod; + } + + function createNativeSubnet( + address parentGatewayAddress, + SubnetID memory parentNetworkName + ) internal returns (TestSubnetDefinition memory) { + SubnetActorDiamond subnetActor = createSubnetActor( + defaultSubnetActorParamsWith(parentGatewayAddress, parentNetworkName) + ); + + return createSubnet(parentNetworkName.route, subnetActor); + } + + function createTokenSubnet( + address tokenAddress, + address parentGatewayAddress, + SubnetID memory parentNetworkName + ) internal returns (TestSubnetDefinition memory) { + SubnetActorDiamond subnetActor = createSubnetActor( + defaultSubnetActorParamsWith(parentGatewayAddress, parentNetworkName, tokenAddress) + ); + + return createSubnet(parentNetworkName.route, subnetActor); + } + + function createSubnet( + address[] memory subnetPath, + SubnetActorDiamond subnetActor + ) internal returns (TestSubnetDefinition memory) { + address[] memory newPath = new address[](subnetPath.length + 1); + for (uint i = 0; i < subnetPath.length; i++) { + newPath[i] = subnetPath[i]; + } + + newPath[subnetPath.length] = address(subnetActor); + + SubnetID memory subnetName = SubnetID({root: ROOTNET_CHAINID, route: newPath}); + GatewayDiamond subnetGateway = createGatewayDiamond(gatewayParams(subnetName)); + + return + TestSubnetDefinition({ + gateway: subnetGateway, + gatewayAddr: address(subnetGateway), + id: subnetName, + subnetActor: subnetActor, + subnetActorAddr: address(subnetActor), + path: newPath + }); + } + + function call(IpcEnvelope memory xnetMsg) internal { + IPCMsgType msgType = xnetMsg.applyType(xnetMsg.from.subnetId); + + address gateway = subnets.getSubnetGateway(xnetMsg.from.subnetId); + vm.prank(xnetMsg.from.rawAddress.extractEvmAddress()); + + if (msgType == IPCMsgType.BottomUp) { + IGateway(gateway).sendContractXnetMessage{value: xnetMsg.value}(xnetMsg); + } else { + IGateway(gateway).sendContractXnetMessage(xnetMsg); + } + } + + function fundContract( + SubnetID memory originSubnet, + SubnetID memory targetSubnet, + address targetRecipient, + uint256 amount + ) internal { + (bool sameChain, SubnetID memory nextHop) = targetSubnet.down(originSubnet); + require(sameChain, "not in the same hierachy"); + + Asset memory supplySource = ISubnetActor(nextHop.getActor()).supplySource(); + address gateway = subnets.getSubnetGateway(originSubnet); + + vm.prank(address(this)); + supplySource.makeAvailable(gateway, amount); + + // sadly fund/fundWithToken does not support multi hierachy call, need to use xnet msg + IpcEnvelope memory crossMessage = TestUtils.newXnetCallMsg( + IPCAddress({subnetId: originSubnet, rawAddress: FvmAddressHelper.from(address(this))}), + IPCAddress({subnetId: targetSubnet, rawAddress: FvmAddressHelper.from(address(targetRecipient))}), + amount, + 0 // the nonce, does not matter, should be handled by contract calls + ); + + call(crossMessage); + } + + function initL1() internal returns (SubnetID memory) { + SubnetID memory rootNetworkName = SubnetID({root: ROOTNET_CHAINID, route: new address[](0)}); + GatewayDiamond rootGateway = createGatewayDiamond(gatewayParams(rootNetworkName)); + + SubnetDefinition memory l1Defi = SubnetDefinition({gatewayAddr: address(rootGateway), id: rootNetworkName}); + subnets.initL1(l1Defi); + + return l1Defi.id; + } + + function newSubnets(SubnetCreationParams[] memory params) internal returns (SubnetID[] memory subnetIds) { + uint256 length = params.length; + + subnetIds = new SubnetID[](length); + + for (uint256 i = 0; i < length; i++) { + address parentGateway = subnets.getSubnetGateway(params[i].parent); + + TestSubnetDefinition memory t; + if (params[i].supplySource.kind == AssetKind.Native) { + t = createNativeSubnet(parentGateway, params[i].parent); + } else { + t = createTokenSubnet(params[i].supplySource.tokenAddress, parentGateway, params[i].parent); + } + + // register child subnet to parent gateway + vm.prank(t.subnetActorAddr); + registerSubnetGW(0, t.subnetActorAddr, GatewayDiamond(payable(parentGateway))); + + subnetIds[i] = subnets.registerNewSubnet(params[i].parent, t.subnetActorAddr, t.gatewayAddr); + } + } + + function propagateUp(SubnetID memory from, SubnetID memory to) internal { + require(from.commonParent(to).equals(to), "not related subnets"); + + bool finished = false; + while (!finished) { + SubnetID memory parent = from.getParentSubnet(); + + address gateway = subnets.getSubnetGateway(from); + // this would normally submitted by releayer. It call the subnet actor on the L2 network. + submitBottomUpCheckpoint( + createBottomUpCheckpoint(from, GatewayDiamond(payable(gateway))), + SubnetActorDiamond(payable(from.getActor())) + ); + + from = parent; + finished = parent.equals(to); + } + } + + /// @dev Extracts all the NewTopdownMessage emitted events, down side is that all other events are consumed too. + function propagateDown() internal { + while (true) { + Vm.Log[] memory entries = vm.getRecordedLogs(); + + uint256 num = 0; + for (uint256 i = 0; i < entries.length; i++) { + if (entries[i].topics[0] == new_topdown_message_topic) { + IpcEnvelope[] memory msgs = new IpcEnvelope[](1); + + (IpcEnvelope memory xnetMsg, ) = abi.decode(entries[i].data, (IpcEnvelope, bytes32)); + msgs[0] = xnetMsg; + + address gateway = subnets.getGatewayBySubnetActor(address(uint160(uint256(entries[i].topics[1])))); + executeTopdownMessages(msgs, GatewayDiamond(payable(gateway))); + + num++; + } + } + + if (num == 0) { + break; + } + } + } + + /// @dev checks and ensures the latestResultMessage matches with expected bytes + function checkResultMessageBytes(bytes memory expected, string memory errorMessage) internal view { + ResultMsg memory message = abi.decode(latestResultMessage, (ResultMsg)); + require(keccak256(expected) == keccak256(message.ret), errorMessage); + } + + //-------------------- + // Call flow tests. + //--------------------- + + // testing Native L1 => ERC20 L2 => ERC20 L3, this supply source is not allowed + function test_N1E2E3_rejects() public { + SubnetID memory l1SubnetID = initL1(); + + address erc20_1 = address(new ERC20PresetFixedSupply("TestToken1", "TT", 21_000_000 ether, address(this))); + address erc20_2 = address(new ERC20PresetFixedSupply("TestToken2", "TT", 21_000_000 ether, address(this))); + + // define L2s + SubnetCreationParams[] memory l2Params = new SubnetCreationParams[](1); + l2Params[0] = SubnetCreationParams({parent: l1SubnetID, supplySource: AssetHelper.erc20(erc20_1)}); + SubnetID[] memory l2SubnetIDs = newSubnets(l2Params); + + // define L3s + SubnetCreationParams[] memory l3Params = new SubnetCreationParams[](1); + l3Params[0] = SubnetCreationParams({parent: l2SubnetIDs[0], supplySource: AssetHelper.erc20(erc20_2)}); + SubnetID[] memory l3SubnetIDs = newSubnets(l3Params); + + // create the recipients + MockIpcContractResult recipientContract = new MockIpcContractResult(); + + // initial conditions + require(address(recipientContract).balance == 0); + + // start recording events, if not called, propagate down will not work + vm.recordLogs(); + + uint256 originalBalance = IERC20(erc20_1).balanceOf(address(this)); + uint256 amount = 0.01 ether; + + // fund the L3 address should throw an error and triggers an result xnet message + fundContract({ + originSubnet: l1SubnetID, + targetSubnet: l3SubnetIDs[0], + targetRecipient: address(recipientContract), + amount: amount + }); + propagateDown(); + + // funds should now be locked + require(IERC20(erc20_1).balanceOf(address(this)) == originalBalance - amount, "balance should have droppped"); + + // relayer carries the bottom up checkpoint + propagateUp(l2SubnetIDs[0], l1SubnetID); + + // post xnet message conditions + uint256 postBalance = IERC20(erc20_1).balanceOf(address(this)); + require(postBalance == originalBalance, "token should be refunded"); + checkResultMessageBytes( + abi.encodeWithSelector(InvalidXnetMessage.selector, InvalidXnetMessageReason.IncompatibleSupplySource), + "result should be incompatible supply source" + ); + } + + // testing Native L1 => ERC20 L2 => Native L3 + function test_N1E2N3_works() public { + SubnetID memory l1SubnetID = initL1(); + + address erc20 = address(new ERC20PresetFixedSupply("TestToken", "TT", 21_000_000 ether, address(this))); + + // define L2s + SubnetCreationParams[] memory l2Params = new SubnetCreationParams[](1); + l2Params[0] = SubnetCreationParams({parent: l1SubnetID, supplySource: AssetHelper.erc20(erc20)}); + SubnetID[] memory l2SubnetIDs = newSubnets(l2Params); + + // define L3s + SubnetCreationParams[] memory l3Params = new SubnetCreationParams[](1); + l3Params[0] = SubnetCreationParams({parent: l2SubnetIDs[0], supplySource: AssetHelper.native()}); + SubnetID[] memory l3SubnetIDs = newSubnets(l3Params); + + // create the recipients + MockIpcContractResult recipientContract = new MockIpcContractResult(); + + // initial conditions + require(address(recipientContract).balance == 0); + + // start recording events, if not called, propagate down will not work + vm.recordLogs(); + + // fund the L3 address first + fundContract({ + originSubnet: l1SubnetID, + targetSubnet: l3SubnetIDs[0], + targetRecipient: address(recipientContract), + amount: 0.01 ether + }); + propagateDown(); + + // post xnet message conditions + require(address(recipientContract).balance == 0.01 ether); + } + + // testing Native L3 => ERC20 L2 => Native L1 => ERC20 L2' => Native L3' + function test_N3E2N1E2N3_works() public { + SubnetID memory l1SubnetID = initL1(); + + address erc20 = address(new ERC20PresetFixedSupply("TestToken", "TT", 21_000_000 ether, address(this))); + + // define L2s + SubnetCreationParams[] memory l2Params = new SubnetCreationParams[](2); + l2Params[0] = SubnetCreationParams({parent: l1SubnetID, supplySource: AssetHelper.erc20(erc20)}); + l2Params[1] = SubnetCreationParams({parent: l1SubnetID, supplySource: AssetHelper.erc20(erc20)}); + SubnetID[] memory l2SubnetIDs = newSubnets(l2Params); + + // define L3s + SubnetCreationParams[] memory l3Params = new SubnetCreationParams[](2); + l3Params[0] = SubnetCreationParams({parent: l2SubnetIDs[0], supplySource: AssetHelper.native()}); + l3Params[1] = SubnetCreationParams({parent: l2SubnetIDs[1], supplySource: AssetHelper.native()}); + SubnetID[] memory l3SubnetIDs = newSubnets(l3Params); + + // create the sender and recipients + MockIpcContractResult callerContract = new MockIpcContractResult(); + MockIpcContractResult recipientContract = new MockIpcContractResult(); + + // start recording events, if not called, propagate down will not work + vm.recordLogs(); + + uint256 amount = 0.01 ether; + + // fund the L3 address first + fundContract({ + originSubnet: l1SubnetID, + targetSubnet: l3SubnetIDs[0], + targetRecipient: address(callerContract), + amount: amount + }); + propagateDown(); + + // initial conditions + require(address(callerContract).balance == amount, "sender initial balance should be 0.01 ether"); + require(address(recipientContract).balance == 0, "recipient initial balance should be 0"); + + // the funds now arrives at L3, trigger cross network call + IpcEnvelope memory crossMessage = TestUtils.newXnetCallMsg( + IPCAddress({subnetId: l3SubnetIDs[0], rawAddress: FvmAddressHelper.from(address(callerContract))}), + IPCAddress({subnetId: l3SubnetIDs[1], rawAddress: FvmAddressHelper.from(address(recipientContract))}), + amount, + 0 // the nonce, does not matter, should be handled by contract calls + ); + + call(crossMessage); + + propagateUp(l3SubnetIDs[0], l1SubnetID); + propagateDown(); + + // post xnet message conditions + require(address(callerContract).balance == 0, "sender final balance should be 0"); + require(address(recipientContract).balance == amount, "recipient final balance should be 0.01 ether"); + require( + address(subnets.getSubnetGateway(l3SubnetIDs[1])).balance == 0, + "L3 gateway final balance should be 0 ether" + ); + require( + address(subnets.getSubnetGateway(l3SubnetIDs[0])).balance == 0, + "L3 gateway final balance should be 0 ether" + ); + } +} diff --git a/contracts/test/unit/CrossMsgHelper.t.sol b/contracts/test/unit/CrossMsgHelper.t.sol index 1eeda8283..bd1a556a0 100644 --- a/contracts/test/unit/CrossMsgHelper.t.sol +++ b/contracts/test/unit/CrossMsgHelper.t.sol @@ -175,9 +175,10 @@ contract CrossMsgHelperTest is Test { crossMsg = crossMsg.setCallMsg(message); vm.deal(sender, 1 ether); - vm.expectCall(recipient, 0, new bytes(0), 1); + vm.expectCall(recipient, crossMsg.value, new bytes(0), 1); - (, bytes memory result) = crossMsg.execute(AssetHelper.native()); + (bool ok, bytes memory result) = crossMsg.execute(AssetHelper.native()); + console.log(ok); require(keccak256(result) == keccak256(EMPTY_BYTES)); } diff --git a/contracts/test/unit/LibGateway.t.sol b/contracts/test/unit/LibGateway.t.sol index 527026340..9a6ba9f93 100644 --- a/contracts/test/unit/LibGateway.t.sol +++ b/contracts/test/unit/LibGateway.t.sol @@ -198,13 +198,12 @@ contract LibGatewayTest is Test { address fromRaw = address(1000); address toRaw = address(1001); - IPCAddress memory from = IPCAddress({subnetId: parentSubnet, rawAddress: FvmAddressHelper.from(fromRaw)}); - IPCAddress memory to = IPCAddress({subnetId: childSubnet, rawAddress: FvmAddressHelper.from(toRaw)}); + uint256 value = 1000; IpcEnvelope memory crossMsg = CrossMsgHelper.createCallMsg({ - from: from, - to: to, - value: 1000, + from: IPCAddress({subnetId: parentSubnet, rawAddress: FvmAddressHelper.from(fromRaw)}), + to: IPCAddress({subnetId: childSubnet, rawAddress: FvmAddressHelper.from(toRaw)}), + value: value, method: bytes4(0), params: new bytes(0) }); @@ -219,7 +218,7 @@ contract LibGatewayTest is Test { kind: IpcMsgKind.Result, from: crossMsg.to, to: crossMsg.from, - value: 0, + value: value, message: abi.encode(message), nonce: 0 }); @@ -311,13 +310,12 @@ contract LibGatewayTest is Test { address fromRaw = address(1000); address toRaw = callingContract; - IPCAddress memory from = IPCAddress({subnetId: childSubnet, rawAddress: FvmAddressHelper.from(fromRaw)}); - IPCAddress memory to = IPCAddress({subnetId: parentSubnet, rawAddress: FvmAddressHelper.from(toRaw)}); + uint256 value = 1000; IpcEnvelope memory crossMsg = CrossMsgHelper.createCallMsg({ - from: from, - to: to, - value: 1000, + from: IPCAddress({subnetId: childSubnet, rawAddress: FvmAddressHelper.from(fromRaw)}), + to: IPCAddress({subnetId: parentSubnet, rawAddress: FvmAddressHelper.from(toRaw)}), + value: value, method: GatewayDummyContract.reverts.selector, params: new bytes(0) }); @@ -332,7 +330,7 @@ contract LibGatewayTest is Test { kind: IpcMsgKind.Result, from: crossMsg.to, to: crossMsg.from, - value: 0, + value: value, message: abi.encode(message), nonce: 0 }); @@ -365,13 +363,12 @@ contract LibGatewayTest is Test { address fromRaw = address(1000); address toRaw = callingContract; - IPCAddress memory from = IPCAddress({subnetId: childSubnet, rawAddress: FvmAddressHelper.from(fromRaw)}); - IPCAddress memory to = IPCAddress({subnetId: parentSubnet, rawAddress: FvmAddressHelper.from(toRaw)}); + uint256 value = 1000; IpcEnvelope memory crossMsg = CrossMsgHelper.createCallMsg({ - from: from, - to: to, - value: 1000, + from: IPCAddress({subnetId: childSubnet, rawAddress: FvmAddressHelper.from(fromRaw)}), + to: IPCAddress({subnetId: parentSubnet, rawAddress: FvmAddressHelper.from(toRaw)}), + value: value, method: GatewayDummyContract.reverts.selector, params: new bytes(0) }); @@ -386,7 +383,7 @@ contract LibGatewayTest is Test { kind: IpcMsgKind.Result, from: crossMsg.to, to: crossMsg.from, - value: 0, + value: value, message: abi.encode(message), nonce: 0 });