Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(node): support l2 plus value transfer #1240

Merged
merged 11 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion contracts/contracts/errors/IPCErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
16 changes: 10 additions & 6 deletions contracts/contracts/gateway/GatewayMessengerFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ 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";
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";

Expand All @@ -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.
Expand All @@ -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));

Expand All @@ -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) {
Expand All @@ -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);

Expand Down
9 changes: 9 additions & 0 deletions contracts/contracts/interfaces/ISubnetActor.sol
Original file line number Diff line number Diff line change
@@ -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);
}
11 changes: 9 additions & 2 deletions contracts/contracts/lib/AssetHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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});
}
}
18 changes: 8 additions & 10 deletions contracts/contracts/lib/CrossMsgHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -182,20 +181,19 @@ library CrossMsgHelper {
}

address recipient = crossMsg.to.rawAddress.extractEvmAddress().normalize();
if (crossMsg.kind == IpcMsgKind.Transfer) {
// If the cross msg kind is Result, create result message should have handled the value correctly.
// If the execution is ok, value should be 0, else one should perform refund.
if (crossMsg.kind == IpcMsgKind.Transfer || crossMsg.kind == IpcMsgKind.Result) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct; the version in main is more correct. For a Result message, you will want to perform a call as this returns control back to the caller. Rationale:

  • 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, you will want to give it control so it can handle the result

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;

} else if (crossMsg.kind == IpcMsgKind.Call) {
// send the envelope directly to the entrypoint
// use supplySource so the tokens in the message are handled successfully
// and by the right supply source
return
supplySource.performCall(
payable(recipient),
abi.encodeCall(IIpcHandler.handleIpcMessage, (crossMsg)),
value
crossMsg.value
);
}
return (false, EMPTY_BYTES);
Expand Down Expand Up @@ -224,7 +222,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);
}
}
113 changes: 90 additions & 23 deletions contracts/contracts/lib/LibGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ 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
enum CrossMessageValidationOutcome {
Valid,
InvalidDstSubnet,
CannotSendToItself,
CommonParentNotExist
CommonParentNotExist,
IncompatibleSupplySource
}

library LibGateway {
Expand Down Expand Up @@ -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()});
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -567,54 +567,120 @@ 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) {
Comment on lines 611 to +617
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would keep these as a single function returning a tuple. Would call this single method inspect().

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;
} else if (outcome == CrossMessageValidationOutcome.CannotSendToItself) {
return InvalidXnetMessageReason.CannotSendToItself;
} else if (outcome == CrossMessageValidationOutcome.CommonParentNotExist) {
return InvalidXnetMessageReason.CommonParentNotExist;
} else if (outcome == CrossMessageValidationOutcome.IncompatibleSupplySource) {
return InvalidXnetMessageReason.IncompatibleSupplySource;
}

revert("Unhandled validation outcome");
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion contracts/test/integration/GatewayDiamondToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading