Skip to content

Commit

Permalink
Linked Token Upgrades using Transparent Proxy (#837)
Browse files Browse the repository at this point in the history
Co-authored-by: raulk <[email protected]>
Co-authored-by: raulk <[email protected]>
  • Loading branch information
3 people authored Apr 4, 2024
1 parent 2d3c76d commit 6dedd7b
Show file tree
Hide file tree
Showing 29 changed files with 863 additions and 189 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@
[submodule "extras/linked-token/lib/openzeppelin-contracts"]
path = extras/linked-token/lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "extras/linked-token/lib/openzeppelin-contracts-upgradeable"]
path = extras/linked-token/lib/openzeppelin-contracts-upgradeable
url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable
2 changes: 1 addition & 1 deletion contracts/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ lint: fmt

fmt:
npm install --silent --no-save
npx prettier --check -w 'src/**/**/*.sol' 'test/**/**/*.sol' 'test/**/**/*.t.sol' '**/*.{js,jsx,ts,tsx,json,css,md}'
npx prettier --check -w 'src/**/**/*.sol' 'sdk/**/**/*.sol' 'test/**/**/*.sol' 'test/**/**/*.t.sol' '**/*.{js,jsx,ts,tsx,json,css,md}'

build: | forge
forge build
Expand Down
20 changes: 4 additions & 16 deletions contracts/sdk/IpcContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,9 @@ import {IGateway} from "../src/interfaces/IGateway.sol";
import {ReentrancyGuard} from "openzeppelin-contracts/utils/ReentrancyGuard.sol";
import {Ownable} from "openzeppelin-contracts/access/Ownable.sol";
import {CrossMsgHelper} from "../src/lib/CrossMsgHelper.sol";
import {IIpcHandler} from "./interfaces/IIpcHandler.sol";

// Interface that needs to be implemented by IPC-aware contracts.
//
// TODO: extract to a shared module, so that both the IPC contracts and the SDK can depend on this.
// (IPC contracts reaching into the SDK is wrong).
interface IpcHandler {
error CallerIsNotGateway();
error UnsupportedMsgKind();
error UnrecognizedResult();

/// @notice Entrypoint for handling xnet messages in IPC-aware contracts.
function handleIpcMessage(IpcEnvelope calldata envelope) external payable returns (bytes memory ret);
}

abstract contract IpcExchange is IpcHandler, Ownable, ReentrancyGuard {
abstract contract IpcExchange is IIpcHandler, Ownable, ReentrancyGuard {
using CrossMsgHelper for IpcEnvelope;

// The address of the gateway in the network.
Expand Down Expand Up @@ -50,7 +38,7 @@ abstract contract IpcExchange is IpcHandler, Ownable, ReentrancyGuard {
// If we were not tracking it, or if some details don't match, refuse to handle the receipt.
IpcEnvelope storage orig = inflightMsgs[result.id];
if (orig.message.length == 0 || keccak256(abi.encode(envelope.from)) != keccak256(abi.encode(orig.to))) {
revert IpcHandler.UnrecognizedResult();
revert IIpcHandler.UnrecognizedResult();
}

/// Note: if the result handler reverts, we will
Expand Down Expand Up @@ -114,7 +102,7 @@ abstract contract IpcExchange is IpcHandler, Ownable, ReentrancyGuard {
function _onlyGateway() private view {
// only the gateway address is allowed to deliver xnet messages.
if (msg.sender != gatewayAddr) {
revert IpcHandler.CallerIsNotGateway();
revert IIpcHandler.CallerIsNotGateway();
}
}

Expand Down
123 changes: 123 additions & 0 deletions contracts/sdk/IpcContractUpgradeable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.23;

import {IpcEnvelope, ResultMsg, CallMsg, IpcMsgKind} from "../src/structs/CrossNet.sol";
import {IPCAddress} from "../src/structs/Subnet.sol";
import {EMPTY_BYTES} from "../src/constants/Constants.sol";
import {IGateway} from "../src/interfaces/IGateway.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

import {CrossMsgHelper} from "../src/lib/CrossMsgHelper.sol";

import {IIpcHandler} from "./interfaces/IIpcHandler.sol";

abstract contract IpcExchangeUpgradeable is Initializable, IIpcHandler, OwnableUpgradeable, ReentrancyGuardUpgradeable {
using CrossMsgHelper for IpcEnvelope;

// The address of the gateway in the network.
address public gatewayAddr;

// List of messages in-flight for which the contract hasn't received a receipt yet.
mapping(bytes32 => IpcEnvelope) public inflightMsgs;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

function __IpcExchangeUpgradeable_init(address gatewayAddr_) public onlyInitializing {
gatewayAddr = gatewayAddr_;
__Ownable_init();
__ReentrancyGuard_init();
}

/// @notice Entrypoint for IPC-enabled contracts. This function is always called by
/// the gateway when a `Call` or `Receipt` cross-net messages is targeted to
/// a specific address in the subnet.
function handleIpcMessage(IpcEnvelope calldata envelope) external payable onlyGateway returns (bytes memory) {
// internal dispatch of the cross-net message to the right method.
if (envelope.kind == IpcMsgKind.Call) {
CallMsg memory call = abi.decode(envelope.message, (CallMsg));
return _handleIpcCall(envelope, call);
} else if (envelope.kind == IpcMsgKind.Result) {
ResultMsg memory result = abi.decode(envelope.message, (ResultMsg));

// Recover the original message.
// If we were not tracking it, or if some details don't match, refuse to handle the receipt.
IpcEnvelope storage orig = inflightMsgs[result.id];
if (orig.message.length == 0 || keccak256(abi.encode(envelope.from)) != keccak256(abi.encode(orig.to))) {
revert IIpcHandler.UnrecognizedResult();
}

/// Note: if the result handler reverts, we will
_handleIpcResult(orig, envelope, result);
delete inflightMsgs[result.id];
return EMPTY_BYTES;
}
revert UnsupportedMsgKind();
}

/// @notice Function to be overridden by the child contract to handle incoming IPC calls.
///
/// NOTE: It's fine for this method to revert. If that happens, IPC will carry the error to the caller.
function _handleIpcCall(
IpcEnvelope memory envelope,
CallMsg memory callMsg
) internal virtual returns (bytes memory);

/// @notice Function to be overridden by the child contract to handle results from previously performed IPC calls.
///
/// NOTE: This must not revert as doing so will leave the correlation map in an inconsistent state.
/// (IPC will consider the result delivery attempted, and will not repeat it again).
function _handleIpcResult(
IpcEnvelope storage original,
IpcEnvelope memory result,
ResultMsg memory resultMsg
) internal virtual;

/// @notice Method the implementation of this contract can invoke to perform an IPC call.
function performIpcCall(
IPCAddress memory to,
CallMsg memory callMsg,
uint256 value
) internal nonReentrant returns (IpcEnvelope memory envelope) {
// Queue the cross-net message for dispatch.
envelope = IGateway(gatewayAddr).sendContractXnetMessage{value: value}(
IpcEnvelope({
kind: IpcMsgKind.Call,
from: to, // TODO: will anyway be replaced by sendContractXnetMessage.
to: to,
nonce: 0, // TODO: will be replaced.
value: value,
message: abi.encode(callMsg)
})
);
// Add the message to the list of inflights
bytes32 id = envelope.toHash();
inflightMsgs[id] = envelope;
}

function dropMessages(bytes32[] calldata ids) public onlyOwner {
uint256 length = ids.length;
for (uint256 i; i < length; ) {
delete inflightMsgs[ids[i]];
unchecked {
++i;
}
}
}

function _onlyGateway() private view {
// only the gateway address is allowed to deliver xnet messages.
if (msg.sender != gatewayAddr) {
revert IIpcHandler.CallerIsNotGateway();
}
}

modifier onlyGateway() {
_onlyGateway();
_;
}
}
14 changes: 14 additions & 0 deletions contracts/sdk/interfaces/IIpcHandler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.23;

import {IpcEnvelope, ResultMsg, CallMsg, IpcMsgKind} from "../../src/structs/CrossNet.sol";

// Interface that needs to be implemented by IPC-aware contracts.
interface IIpcHandler {
error CallerIsNotGateway();
error UnsupportedMsgKind();
error UnrecognizedResult();

/// @notice Entrypoint for handling xnet messages in IPC-aware contracts.
function handleIpcMessage(IpcEnvelope calldata envelope) external payable returns (bytes memory ret);
}
4 changes: 2 additions & 2 deletions contracts/src/lib/CrossMsgHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {FilAddress} from "fevmate/utils/FilAddress.sol";
import {Address} from "openzeppelin-contracts/utils/Address.sol";
import {SupplySource} from "../structs/Subnet.sol";
import {SupplySourceHelper} from "./SupplySourceHelper.sol";
import {IpcHandler} from "../../sdk/IpcContract.sol";
import {IIpcHandler} from "../../sdk/interfaces/IIpcHandler.sol";

/// @title Helper library for manipulating IpcEnvelope-related structs
library CrossMsgHelper {
Expand Down Expand Up @@ -170,7 +170,7 @@ library CrossMsgHelper {
return
supplySource.performCall(
payable(recipient),
abi.encodeCall(IpcHandler.handleIpcMessage, (crossMsg)),
abi.encodeCall(IIpcHandler.handleIpcMessage, (crossMsg)),
crossMsg.value
);
}
Expand Down
10 changes: 5 additions & 5 deletions contracts/test/helpers/TestUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "forge-std/Test.sol";
import "elliptic-curve-solidity/contracts/EllipticCurve.sol";
import {IPCAddress} from "../../src/structs/Subnet.sol";
import {CallMsg, IpcMsgKind, IpcEnvelope} from "../../src/structs/CrossNet.sol";
import {IpcHandler} from "../../sdk/IpcContract.sol";
import {IIpcHandler} from "../../sdk/interfaces/IIpcHandler.sol";
import {METHOD_SEND, EMPTY_BYTES} from "../../src/constants/Constants.sol";

library TestUtils {
Expand Down Expand Up @@ -172,14 +172,14 @@ library TestUtils {
}
}

contract MockIpcContract is IpcHandler {
contract MockIpcContract is IIpcHandler {
/* solhint-disable-next-line unused-vars */
function handleIpcMessage(IpcEnvelope calldata) external payable returns (bytes memory ret) {
return EMPTY_BYTES;
}
}

contract MockIpcContractFallback is IpcHandler {
contract MockIpcContractFallback is IIpcHandler {
/* solhint-disable-next-line unused-vars */
function handleIpcMessage(IpcEnvelope calldata) external payable returns (bytes memory ret) {
return EMPTY_BYTES;
Expand All @@ -190,7 +190,7 @@ contract MockIpcContractFallback is IpcHandler {
}
}

contract MockIpcContractRevert is IpcHandler {
contract MockIpcContractRevert is IIpcHandler {
bool public reverted = true;

/* solhint-disable-next-line unused-vars */
Expand All @@ -208,7 +208,7 @@ contract MockIpcContractRevert is IpcHandler {
}
}

contract MockIpcContractPayable is IpcHandler {
contract MockIpcContractPayable is IIpcHandler {
/* solhint-disable-next-line unused-vars */
function handleIpcMessage(IpcEnvelope calldata) external payable returns (bytes memory ret) {
return EMPTY_BYTES;
Expand Down
4 changes: 2 additions & 2 deletions contracts/test/integration/GatewayDiamondToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {IPCAddress, SubnetID, Subnet, SupplySource, SupplyKind, Validator} from
import {SubnetIDHelper} from "../../src/lib/SubnetIDHelper.sol";
import {FvmAddressHelper} from "../../src/lib/FvmAddressHelper.sol";
import {CrossMsgHelper} from "../../src/lib/CrossMsgHelper.sol";
import {IpcHandler} from "../../sdk/IpcContract.sol";
import {IIpcHandler} from "../../sdk/interfaces/IIpcHandler.sol";
import {SupplySourceHelper} from "../../src/lib/SupplySourceHelper.sol";
import {FilAddress} from "fevmate/utils/FilAddress.sol";
import {GatewayDiamond} from "../../src/GatewayDiamond.sol";
Expand Down Expand Up @@ -226,7 +226,7 @@ contract GatewayDiamondTokenTest is Test, IntegrationTestBase {

// Verify that we received the call and that the recipient has the tokens.
vm.prank(address(saDiamond));
vm.expectCall(recipient, abi.encodeCall(IpcHandler.handleIpcMessage, (msgs[0])), 1);
vm.expectCall(recipient, abi.encodeCall(IIpcHandler.handleIpcMessage, (msgs[0])), 1);
gatewayDiamond.checkpointer().commitCheckpoint(batch);
assertEq(token.balanceOf(recipient), 8);
}
Expand Down
11 changes: 6 additions & 5 deletions contracts/test/sdk/IpcContract.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {SubnetIDHelper} from "../../src/lib/SubnetIDHelper.sol";
import {FvmAddressHelper} from "../../src/lib/FvmAddressHelper.sol";
import {CrossMsgHelper} from "../../src/lib/CrossMsgHelper.sol";
import {FilAddress} from "fevmate/utils/FilAddress.sol";
import {IpcHandler, IpcExchange} from "../../sdk/IpcContract.sol";
import {IpcExchange} from "../../sdk/IpcContract.sol";
import {IIpcHandler} from "../../sdk/interfaces/IIpcHandler.sol";
import {IGateway} from "../../src/interfaces/IGateway.sol";
import {CrossMsgHelper} from "../../src/lib/CrossMsgHelper.sol";

Expand Down Expand Up @@ -138,14 +139,14 @@ contract IpcExchangeTest is Test {
callEnvelope.kind = IpcMsgKind.Transfer;

// a transfer; fails because cannot handle.
vm.expectRevert(IpcHandler.UnsupportedMsgKind.selector);
vm.expectRevert(IIpcHandler.UnsupportedMsgKind.selector);
vm.prank(gateway);
exch.handleIpcMessage(callEnvelope);
}

function test_IpcExchange_testGatewayOnlyFails() public {
// a call; fails when the caller is not the gateway.
vm.expectRevert(IpcHandler.CallerIsNotGateway.selector);
vm.expectRevert(IIpcHandler.CallerIsNotGateway.selector);
exch.handleIpcMessage(callEnvelope);
}

Expand Down Expand Up @@ -179,7 +180,7 @@ contract IpcExchangeTest is Test {
callEnvelope.from = callEnvelope.to;
callEnvelope.to = from;

vm.expectRevert(IpcHandler.UnrecognizedResult.selector);
vm.expectRevert(IIpcHandler.UnrecognizedResult.selector);
exch.handleIpcMessage(callEnvelope);
}

Expand Down Expand Up @@ -243,7 +244,7 @@ contract IpcExchangeTest is Test {
vm.startPrank(gateway);

// unrecognized correlation id
vm.expectRevert(IpcHandler.UnrecognizedResult.selector);
vm.expectRevert(IIpcHandler.UnrecognizedResult.selector);
exch.handleIpcMessage(resultEnvelope);

// only remaining one
Expand Down
8 changes: 4 additions & 4 deletions extras/axelar-token/src/IpcTokenHandler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IERC20 } from "openzeppelin-contracts/interfaces/IERC20.sol";
import { Ownable } from "openzeppelin-contracts/access/Ownable.sol";
import { SubnetID, SupplySource, SupplyKind } from "@ipc/src/structs/Subnet.sol";
import { FvmAddress } from "@ipc/src/structs/FvmAddress.sol";
import { IpcHandler } from "@ipc/sdk/IpcContract.sol";
import { IIpcHandler } from "@ipc/sdk/interfaces/IIpcHandler.sol";
import { IpcMsgKind, ResultMsg, OutcomeType, IpcEnvelope } from "@ipc/src/structs/CrossNet.sol";
import { FvmAddressHelper } from "@ipc/src/lib/FvmAddressHelper.sol";
import { SubnetIDHelper } from "@ipc/src/lib/SubnetIDHelper.sol";
Expand All @@ -24,7 +24,7 @@ interface SubnetActor {
// IpcTokenSender via the Axelar ITS, receiving some token value to deposit into an IPC subnet (specified in the
// incoming message). The IpcTokenHandler handles deposit failures by crediting the value back to the original
// beneficiary, and making it available from them to withdraw() on the rootnet.
contract IpcTokenHandler is InterchainTokenExecutable, IpcHandler, Ownable {
contract IpcTokenHandler is InterchainTokenExecutable, IIpcHandler, Ownable {
using FvmAddressHelper for address;
using FvmAddressHelper for FvmAddress;
using SubnetIDHelper for SubnetID;
Expand Down Expand Up @@ -85,10 +85,10 @@ contract IpcTokenHandler is InterchainTokenExecutable, IpcHandler, Ownable {
// @notice Handles result messages for funding operations.
function handleIpcMessage(IpcEnvelope calldata envelope) external payable returns (bytes memory ret) {
if (msg.sender != address(_ipcGateway)) {
revert IpcHandler.CallerIsNotGateway();
revert IIpcHandler.CallerIsNotGateway();
}
if (envelope.kind != IpcMsgKind.Result) {
revert IpcHandler.UnsupportedMsgKind();
revert IIpcHandler.UnsupportedMsgKind();
}

ResultMsg memory result = abi.decode(envelope.message, (ResultMsg));
Expand Down
11 changes: 8 additions & 3 deletions extras/linked-token/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ export PRIVATE_KEY=

export AMOUNT=1000

export CALIBNET_RPC_URL=https://api.calibration.node.glif.io/rpc/v1
export CALIBNET_CHAIN_ID=314159
export CALIBNET_GATEWAY=
export ORIGIN_NET_RPC_URL=https://api.calibration.node.glif.io/rpc/v1
export ORIGIN_NET_CHAIN_ID=314159
export ORIGIN_NET_GATEWAY=

export SUBNET_RPC_URL=http://0.0.0.0:8545
export SUBNET_GATEWAY=0x77aa40b105843728088c0132e43fc44348881da8
export SUBNET_ROUTE_IN_ETH_FORMAT=


export REPLICA_TOKEN_NAME=OriginalTokenReplica
export REPLICA_TOKEN_SYMBOL=OTR
export REPLICA_TOKEN_DECIMALS=6
Loading

0 comments on commit 6dedd7b

Please sign in to comment.