Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
65 changes: 65 additions & 0 deletions contracts/crosschain/CrosschainRemoteController.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {CrosschainLinked} from "./CrosschainLinked.sol";

import {IERC7786GatewaySource} from "../interfaces/draft-IERC7786.sol";
import {InteroperableAddress} from "../utils/draft-InteroperableAddress.sol";
import {Bytes} from "../utils/Bytes.sol";
import {ERC7786Recipient} from "./ERC7786Recipient.sol";

import {Mode} from "../account/utils/draft-ERC7579Utils.sol";

abstract contract CrosschainRemoteController {
using Bytes for bytes;
using InteroperableAddress for bytes;

mapping(bytes chain => CrosschainLinked.Link) private _links;

constructor(CrosschainLinked.Link[] memory links) {
for (uint256 i = 0; i < links.length; ++i) {
_setLink(links[i].gateway, links[i].counterpart, false);
}
}

function _crosschainExecute(bytes memory chain, Mode mode, bytes memory executionCalldata) internal virtual {
(address gateway, bytes memory counterpart) = getLink(chain);
IERC7786GatewaySource(gateway).sendMessage(
counterpart,
abi.encodePacked(mode, executionCalldata),
new bytes[](0)
);
}

/**
* @dev Returns the ERC-7786 gateway used for sending and receiving cross-chain messages to a given chain.
*
* Note: The `chain` parameter is a "chain-only" InteroperableAddress (empty address) and the `counterpart` returns
* the full InteroperableAddress (chain ref + address) that is on `chain`.
*/
function getLink(bytes memory chain) public view virtual returns (address gateway, bytes memory counterpart) {
CrosschainLinked.Link storage self = _links[chain];
return (self.gateway, self.counterpart);
}

/**
* @dev Internal setter to change the ERC-7786 gateway and counterpart for a given chain. Called at construction.
*
* Note: The `counterpart` parameter is the full InteroperableAddress (chain ref + address).
*/
function _setLink(address gateway, bytes memory counterpart, bool allowOverride) internal virtual {
// Sanity check, this should revert if gateway is not an ERC-7786 implementation. Note that since
// supportsAttribute returns data, an EOA would fail that test (nothing returned).
IERC7786GatewaySource(gateway).supportsAttribute(bytes4(0));

(bytes2 chainType, bytes memory chainReference, ) = counterpart.parseV1();
bytes memory chain = InteroperableAddress.formatV1(chainType, chainReference, hex"");
if (allowOverride || _links[chain].gateway == address(0)) {
_links[chain] = CrosschainLinked.Link(gateway, counterpart);
emit CrosschainLinked.LinkRegistered(gateway, counterpart);
} else {
revert CrosschainLinked.LinkAlreadyRegistered(chain);
}
}
}
75 changes: 75 additions & 0 deletions contracts/crosschain/CrosschainRemoteExecutor.sol
Copy link
Member

Choose a reason for hiding this comment

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

As a side note, I think this remote executor could allow multichain accounts similar to Hyperlane's Interchain Accounts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Executor is indeed designed to be the "remote identity" of any "account". Can be used by a governor, but also by a timelock, a multisig, or any account

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {IERC7786GatewaySource} from "../interfaces/draft-IERC7786.sol";
import {ERC7786Recipient} from "./ERC7786Recipient.sol";
import {ERC7579Utils, Mode, CallType, ExecType} from "../account/utils/draft-ERC7579Utils.sol";
import {Bytes} from "../utils/Bytes.sol";

contract CrosschainRemoteExecutor is ERC7786Recipient {
using Bytes for bytes;
using ERC7579Utils for *;

address private _gateway;
bytes private _controller;

event CrosschainControllerSet(address gateway, bytes controller);
error AccessRestricted();

constructor(address initialGateway, bytes memory initialController) {
_setup(initialGateway, initialController);
}

function gateway() public view virtual returns (address) {
return _gateway;
}

function controller() public view virtual returns (bytes memory) {
return _controller;
}

function reconfigure(address newGateway, bytes memory newController) public virtual {
require(msg.sender == address(this), AccessRestricted());
_setup(newGateway, newController);
}

function _setup(address gateway_, bytes memory controller_) internal virtual {
// Sanity check, this should revert if gateway is not an ERC-7786 implementation. Note that since
// supportsAttribute returns data, an EOA would fail that test (nothing returned).
IERC7786GatewaySource(gateway_).supportsAttribute(bytes4(0));

_gateway = gateway_;
_controller = controller_;

emit CrosschainControllerSet(gateway_, controller_);
}

/// @inheritdoc ERC7786Recipient
function _isAuthorizedGateway(
address instance,
bytes calldata sender
) internal view virtual override returns (bool) {
return gateway() == instance && controller().equal(sender);
}

/// @inheritdoc ERC7786Recipient
function _processMessage(
address /*gateway*/,
bytes32 /*receiveId*/,
bytes calldata /*sender*/,
bytes calldata payload
) internal virtual override {
// split payload
(CallType callType, ExecType execType, , ) = Mode.wrap(bytes32(payload[0x00:0x20])).decodeMode();
bytes calldata executionCalldata = payload[0x20:];

if (callType == ERC7579Utils.CALLTYPE_SINGLE) {
executionCalldata.execSingle(execType);
} else if (callType == ERC7579Utils.CALLTYPE_BATCH) {
executionCalldata.execBatch(execType);
} else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) {
executionCalldata.execDelegateCall(execType);
} else revert ERC7579Utils.ERC7579UnsupportedCallType(callType);
Comment on lines +90 to +96
Copy link
Member

Choose a reason for hiding this comment

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

This pattern is shared with AccountERC7579. Maybe we should add a function to the ERC7579Utils:

function exec(bytes32 mode, bytes calldata executionCalldata) internal {
       if (callType == CALLTYPE_SINGLE) {
            executionCalldata.execSingle(execType);
        } else if (callType == CALLTYPE_BATCH) {
            executionCalldata.execBatch(execType);
        } else if (callType == CALLTYPE_DELEGATECALL) {
            executionCalldata.execDelegateCall(execType);
        } else revert ERC7579UnsupportedCallType(callType);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No strong opinion here. I'm not sure we really need to have this helper, but I also don't see any issues in adding it.

}
}
10 changes: 10 additions & 0 deletions contracts/mocks/crosschain/CrosschainRemoteControllerMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {CrosschainRemoteController} from "../../crosschain/CrosschainRemoteController.sol";
import {CrosschainLinked} from "../../crosschain/CrosschainLinked.sol";

contract CrosschainRemoteControllerMock is CrosschainRemoteController {
constructor(CrosschainLinked.Link[] memory links) CrosschainRemoteController(links) {}
}
158 changes: 158 additions & 0 deletions test/crosschain/CrosschainExecutor.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const { getLocalChain } = require('../helpers/chains');
const {
CALL_TYPE_SINGLE,
CALL_TYPE_BATCH,
CALL_TYPE_DELEGATE,
encodeMode,
encodeSingle,
encodeBatch,
encodeDelegate,
} = require('../helpers/erc7579');

async function fixture() {
const chain = await getLocalChain();
const [eoa] = await ethers.getSigners();

const gateway = await ethers.deployContract('$ERC7786GatewayMock');
const target = await ethers.deployContract('CallReceiverMock');

// Deploy controller
const controller = await ethers.deployContract('$CrosschainRemoteControllerMock', [[]]);

// Deploy executor
const executor = await ethers.deployContract('$CrosschainRemoteExecutor', [gateway, chain.toErc7930(controller)]);

// Configure controller
await expect(controller.$_setLink(gateway, chain.toErc7930(executor), false))
.to.emit(controller, 'LinkRegistered')
.withArgs(gateway, chain.toErc7930(executor));

return { chain, eoa, gateway, target, controller, executor };
}

describe('CrosschainRemoteController & CrosschainRemoteExecutor', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});

it('setup', async function () {
// controller
await expect(this.controller.getLink(this.chain.erc7930)).to.eventually.deep.equal([
this.gateway.target,
this.chain.toErc7930(this.executor),
]);

// executor
await expect(this.executor.gateway()).to.eventually.equal(this.gateway);
await expect(this.executor.controller()).to.eventually.equal(this.chain.toErc7930(this.controller));
});

describe('crosschain operation', function () {
it('support single mode', async function () {
const mode = encodeMode({ callType: CALL_TYPE_SINGLE });
const data = encodeSingle(this.target, 0n, this.target.interface.encodeFunctionData('mockFunctionExtra'));

await expect(this.controller.$_crosschainExecute(this.chain.erc7930, mode, data))
.to.emit(this.target, 'MockFunctionCalledExtra')
.withArgs(this.executor, 0n);
});

it('support batch mode', async function () {
const mode = encodeMode({ callType: CALL_TYPE_BATCH });
const data = encodeBatch(
[this.target, 0n, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234'])],
[this.target, 0n, this.target.interface.encodeFunctionData('mockFunctionExtra')],
);

await expect(this.controller.$_crosschainExecute(this.chain.erc7930, mode, data))
.to.emit(this.target, 'MockFunctionCalledWithArgs')
.withArgs(42, '0x1234')
.to.emit(this.target, 'MockFunctionCalledExtra')
.withArgs(this.executor, 0n);
});

it('support delegate mode', async function () {
const mode = encodeMode({ callType: CALL_TYPE_DELEGATE });
const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionExtra'));

await expect(this.controller.$_crosschainExecute(this.chain.erc7930, mode, data))
.to.emit(this.target.attach(this.executor.target), 'MockFunctionCalledExtra')
.withArgs(this.gateway, 0n);
});

it('revert when mode is invalid', async function () {
const mode = encodeMode({ callType: '0x42' });
const data = '0x';

await expect(this.controller.$_crosschainExecute(this.chain.erc7930, mode, data))
.to.be.revertedWithCustomError(this.executor, 'ERC7579UnsupportedCallType')
.withArgs('0x42');
});
});

describe('reconfigure', function () {
beforeEach(async function () {
this.newGateway = await ethers.deployContract('$ERC7786GatewayMock');
this.newController = await ethers.deployContract('$CrosschainRemoteControllerMock', [[]]);
});

it('through a crosschain call: success', async function () {
const mode = encodeMode({ callType: CALL_TYPE_SINGLE });
const data = encodeSingle(
this.executor,
0n,
this.executor.interface.encodeFunctionData('reconfigure', [
this.newGateway.target,
this.chain.toErc7930(this.newController),
]),
);

await expect(this.controller.$_crosschainExecute(this.chain.erc7930, mode, data))
.to.emit(this.executor, 'CrosschainControllerSet')
.withArgs(this.newGateway, this.chain.toErc7930(this.newController));

await expect(this.executor.gateway()).to.eventually.equal(this.newGateway);
await expect(this.executor.controller()).to.eventually.equal(this.chain.toErc7930(this.newController));
});

it('through the internal setter: success', async function () {
await expect(this.executor.$_setup(this.newGateway, this.chain.toErc7930(this.newController)))
.to.emit(this.executor, 'CrosschainControllerSet')
.withArgs(this.newGateway, this.chain.toErc7930(this.newController));

await expect(this.executor.gateway()).to.eventually.equal(this.newGateway);
await expect(this.executor.controller()).to.eventually.equal(this.chain.toErc7930(this.newController));
});

it('with an invalid new gateway: revert', async function () {
// directly using the internal setter
await expect(this.executor.$_setup(this.eoa.address, this.chain.toErc7930(this.newController))).to.be.reverted;

// through a crosschain call
const mode = encodeMode({ callType: CALL_TYPE_SINGLE });
const data = encodeSingle(
this.executor,
0n,
this.executor.interface.encodeFunctionData('reconfigure', [
this.eoa.address,
this.chain.toErc7930(this.newController),
]),
);

await expect(this.controller.$_crosschainExecute(this.chain.erc7930, mode, data)).to.be.revertedWithCustomError(
this.executor,
'FailedCall',
);
});

it('is access controlled', async function () {
await expect(
this.executor.reconfigure(this.newGateway, this.chain.toErc7930(this.newController)),
).to.be.revertedWithCustomError(this.executor, 'AccessRestricted');
});
});
});
Loading