Skip to content

Commit

Permalink
feat(node): support l2 plus value transfer (#1240)
Browse files Browse the repository at this point in the history
Co-authored-by: cryptoAtwill <[email protected]>
  • Loading branch information
cryptoAtwill and cryptoAtwill authored Dec 31, 2024
1 parent 17acf21 commit 8780344
Show file tree
Hide file tree
Showing 11 changed files with 763 additions and 103 deletions.
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});
}
}
14 changes: 7 additions & 7 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 @@ -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
Expand All @@ -195,7 +195,7 @@ library CrossMsgHelper {
supplySource.performCall(
payable(recipient),
abi.encodeCall(IIpcHandler.handleIpcMessage, (crossMsg)),
value
crossMsg.value
);
}
return (false, EMPTY_BYTES);
Expand Down Expand Up @@ -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);
}
}
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) {
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

0 comments on commit 8780344

Please sign in to comment.