diff --git a/contracts/crosschain/axelar/AxelarGatewayBase.sol b/contracts/crosschain/axelar/AxelarGatewayBase.sol index f88f808c..458b536e 100644 --- a/contracts/crosschain/axelar/AxelarGatewayBase.sol +++ b/contracts/crosschain/axelar/AxelarGatewayBase.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.27; import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; +import {IAxelarGasService} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol"; @@ -18,6 +19,7 @@ abstract contract AxelarGatewayBase is Ownable { /// @dev Axelar's official gateway for the current chain. IAxelarGateway internal immutable _axelarGateway; + IAxelarGasService internal immutable _axelarGasService; // Remote gateway. // `addr` is the isolated address part of ERC-7930. Its not a full ERC-7930 interoperable address. @@ -41,8 +43,13 @@ abstract contract AxelarGatewayBase is Ownable { error RemoteGatewayAlreadyRegistered(bytes2 chainType, bytes chainReference); /// @dev Sets the local gateway address (i.e. Axelar's official gateway for the current chain). - constructor(IAxelarGateway _gateway) { + constructor(IAxelarGateway _gateway, IAxelarGasService _gasService) { _axelarGateway = _gateway; + _axelarGasService = _gasService; + } + + function gasService() public view virtual returns (IAxelarGasService) { + return _axelarGasService; } /// @dev Returns the equivalent chain given an id that can be either either a binary interoperable address or an Axelar network identifier. diff --git a/contracts/crosschain/axelar/AxelarGatewayDuplex.sol b/contracts/crosschain/axelar/AxelarGatewayDuplex.sol index 73b22b07..f08b8682 100644 --- a/contracts/crosschain/axelar/AxelarGatewayDuplex.sol +++ b/contracts/crosschain/axelar/AxelarGatewayDuplex.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.27; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {AxelarGatewayBase, IAxelarGateway} from "./AxelarGatewayBase.sol"; +import {AxelarGatewayBase, IAxelarGateway, IAxelarGasService} from "./AxelarGatewayBase.sol"; import {AxelarGatewayDestination, AxelarExecutable} from "./AxelarGatewayDestination.sol"; import {AxelarGatewaySource} from "./AxelarGatewaySource.sol"; @@ -16,6 +16,7 @@ contract AxelarGatewayDuplex is AxelarGatewaySource, AxelarGatewayDestination { /// @dev Initializes the contract with the Axelar gateway and the initial owner. constructor( IAxelarGateway gateway, + IAxelarGasService gasService, address initialOwner - ) Ownable(initialOwner) AxelarGatewayBase(gateway) AxelarExecutable(address(gateway)) {} + ) Ownable(initialOwner) AxelarGatewayBase(gateway, gasService) AxelarExecutable(address(gateway)) {} } diff --git a/contracts/crosschain/axelar/AxelarGatewaySource.sol b/contracts/crosschain/axelar/AxelarGatewaySource.sol index 62399c9a..aef7dc82 100644 --- a/contracts/crosschain/axelar/AxelarGatewaySource.sol +++ b/contracts/crosschain/axelar/AxelarGatewaySource.sol @@ -5,6 +5,8 @@ pragma solidity ^0.8.27; import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {IERC7786GatewaySource} from "../../interfaces/IERC7786.sol"; +import {IERC7786Attributes} from "../../interfaces/IERC7786Attributes.sol"; +import {ERC7786Attributes} from "../utils/ERC7786Attributes.sol"; import {AxelarGatewayBase} from "./AxelarGatewayBase.sol"; /** @@ -15,13 +17,21 @@ import {AxelarGatewayBase} from "./AxelarGatewayBase.sol"; */ abstract contract AxelarGatewaySource is IERC7786GatewaySource, AxelarGatewayBase { using InteroperableAddress for bytes; - using Strings for address; + + struct MessageDetails { + string destination; + string target; + bytes payload; + } + + uint256 private _lastSendId; + mapping(bytes32 => MessageDetails) private _details; error UnsupportedNativeTransfer(); /// @inheritdoc IERC7786GatewaySource - function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) { - return false; + function supportsAttribute(bytes4 selector) public pure returns (bool) { + return selector == IERC7786Attributes.requestRelay.selector; } /// @inheritdoc IERC7786GatewaySource @@ -30,29 +40,71 @@ abstract contract AxelarGatewaySource is IERC7786GatewaySource, AxelarGatewayBas bytes calldata payload, bytes[] calldata attributes ) external payable returns (bytes32 sendId) { - require(msg.value == 0, UnsupportedNativeTransfer()); - // Use of `if () revert` syntax to avoid accessing attributes[0] if it's empty - if (attributes.length > 0) - revert UnsupportedAttribute(attributes[0].length < 0x04 ? bytes4(0) : bytes4(attributes[0][0:4])); + // Process attributes (relay) + bool withRelay = false; + uint256 value = 0; + address refundRecipient = address(0); + + for (uint256 i = 0; i < attributes.length; ++i) { + (withRelay, value, , refundRecipient) = ERC7786Attributes.tryDecodeRequestRelayCalldata(attributes[i]); + require(withRelay, UnsupportedAttribute(attributes[i].length < 0x04 ? bytes4(0) : bytes4(attributes[i]))); + } + if (!withRelay) { + sendId = bytes32(++_lastSendId); + } + require(msg.value == value, UnsupportedNativeTransfer()); // Create the package bytes memory sender = InteroperableAddress.formatEvmV1(block.chainid, msg.sender); bytes memory adapterPayload = abi.encode(sender, recipient, payload); - // Emit event - sendId = bytes32(0); // Explicitly set to 0 + // Emit event early (stack too deep) emit MessageSent(sendId, sender, recipient, payload, 0, attributes); // Send the message (bytes2 chainType, bytes calldata chainReference, ) = recipient.parseV1Calldata(); - string memory axelarDestination = getAxelarChain(InteroperableAddress.formatV1(chainType, chainReference, "")); bytes memory remoteGateway = getRemoteGateway(chainType, chainReference); - _axelarGateway.callContract( - axelarDestination, - address(bytes20(remoteGateway)).toChecksumHexString(), // TODO non-evm chains? - adapterPayload - ); + string memory axelarDestination = getAxelarChain(InteroperableAddress.formatV1(chainType, chainReference, "")); + // TODO: How should we "stringify" addresses on non-evm chains. Axelar doesn't yet support hex format for all + // non evm addresses. Do we want to use Hex? Base58? Base64? + string memory axelarTarget = chainType == 0x0000 + ? Strings.toChecksumHexString(address(bytes20(remoteGateway))) + : Strings.toHexString(remoteGateway); - return sendId; + if (withRelay) { + _axelarGasService.payNativeGasForContractCall{value: msg.value}( + address(this), + axelarDestination, + axelarTarget, + adapterPayload, + refundRecipient + ); + } else { + _details[sendId] = MessageDetails(axelarDestination, axelarTarget, adapterPayload); + } + + _axelarGateway.callContract(axelarDestination, axelarTarget, adapterPayload); + } + + /** + * @dev Request relaying of a message initiated using `sendMessage`. + * + * NOTE: AxelarGasService does NOT take a gasLimit. Instead it uses the msg.value sent to determine the gas limit. + * This function ignores the provided `gasLimit` parameter. + */ + function requestRelay(bytes32 sendId, uint256 /*gasLimit*/, address refundRecipient) external payable { + MessageDetails memory details = _details[sendId]; + require(details.payload.length > 0); + + // delete storage for some refund + delete _details[sendId]; + + _axelarGasService.payNativeGasForContractCall{value: msg.value}( + address(this), + details.destination, + details.target, + details.payload, + refundRecipient + ); } } diff --git a/contracts/crosschain/utils/ERC7786Attributes.sol b/contracts/crosschain/utils/ERC7786Attributes.sol new file mode 100644 index 00000000..c2b8fbff --- /dev/null +++ b/contracts/crosschain/utils/ERC7786Attributes.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IERC7786Attributes} from "../../interfaces/IERC7786Attributes.sol"; + +/// @dev Library of helper to parse/process ERC-7786 attributes +library ERC7786Attributes { + /// @dev Parse the `requestRelay(uint256,uint256,address)` (0x4cbb573a) attribute into its components. + function tryDecodeRequestRelay( + bytes memory attribute + ) internal pure returns (bool success, uint256 value, uint256 gasLimit, address refundRecipient) { + success = bytes4(attribute) == IERC7786Attributes.requestRelay.selector && attribute.length >= 0x64; + + assembly ("memory-safe") { + value := mul(success, mload(add(attribute, 0x24))) + gasLimit := mul(success, mload(add(attribute, 0x44))) + refundRecipient := mul(success, mload(add(attribute, 0x64))) + } + } + + /// @dev Calldata variant of {tryDecodeRequestRelay}. + function tryDecodeRequestRelayCalldata( + bytes calldata attribute + ) internal pure returns (bool success, uint256 value, uint256 gasLimit, address refundRecipient) { + success = bytes4(attribute) == IERC7786Attributes.requestRelay.selector && attribute.length >= 0x64; + + assembly ("memory-safe") { + value := mul(success, calldataload(add(attribute.offset, 0x04))) + gasLimit := mul(success, calldataload(add(attribute.offset, 0x24))) + refundRecipient := mul(success, calldataload(add(attribute.offset, 0x44))) + } + } +} diff --git a/contracts/interfaces/IERC7786Attributes.sol b/contracts/interfaces/IERC7786Attributes.sol new file mode 100644 index 00000000..2bb829ce --- /dev/null +++ b/contracts/interfaces/IERC7786Attributes.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.4; + +/** + * @dev Standard attributes for ERC-7786. These attributes may be standardized in different ERCs. + */ +interface IERC7786Attributes { + function requestRelay(uint256 value, uint256 gasLimit, address refundRecipient) external; +} diff --git a/contracts/mocks/crosschain/axelar/AxelarGasServiceMock.sol b/contracts/mocks/crosschain/axelar/AxelarGasServiceMock.sol new file mode 100644 index 00000000..0829f23d --- /dev/null +++ b/contracts/mocks/crosschain/axelar/AxelarGasServiceMock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +contract AxelarGasServiceMock { + event NativeGasPaidForContractCall( + address indexed sourceAddress, + string destinationChain, + string destinationAddress, + bytes32 indexed payloadHash, + uint256 gasFeeAmount, + address refundAddress + ); + + function payNativeGasForContractCall( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + address refundAddress + ) external payable { + emit NativeGasPaidForContractCall( + sender, + destinationChain, + destinationAddress, + keccak256(payload), + msg.value, + refundAddress + ); + } +} diff --git a/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDestination.sol b/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDestination.sol deleted file mode 100644 index a6feab4c..00000000 --- a/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDestination.sol +++ /dev/null @@ -1,11 +0,0 @@ -// contracts/MyCustomAxelarGatewayDestination.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {AxelarGatewayDestination, AxelarExecutable} from "../../../crosschain/axelar/AxelarGatewayDestination.sol"; -import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; - -abstract contract MyCustomAxelarGatewayDestination is AxelarGatewayDestination { - /// @dev Initializes the contract with the Axelar gateway and the initial owner. - constructor(IAxelarGateway gateway, address initialOwner) AxelarExecutable(address(gateway)) {} -} diff --git a/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDuplex.sol b/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDuplex.sol deleted file mode 100644 index a7b0891d..00000000 --- a/contracts/mocks/docs/crosschain/MyCustomAxelarGatewayDuplex.sol +++ /dev/null @@ -1,11 +0,0 @@ -// contracts/MyCustomAxelarGatewayDuplex.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {AxelarGatewayDuplex, AxelarExecutable} from "../../../crosschain/axelar/AxelarGatewayDuplex.sol"; -import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; - -abstract contract MyCustomAxelarGatewayDuplex is AxelarGatewayDuplex { - /// @dev Initializes the contract with the Axelar gateway and the initial owner. - constructor(IAxelarGateway gateway, address initialOwner) AxelarGatewayDuplex(gateway, initialOwner) {} -} diff --git a/contracts/mocks/docs/crosschain/MyCustomAxelarGatewaySource.sol b/contracts/mocks/docs/crosschain/MyCustomAxelarGatewaySource.sol deleted file mode 100644 index 4f739882..00000000 --- a/contracts/mocks/docs/crosschain/MyCustomAxelarGatewaySource.sol +++ /dev/null @@ -1,13 +0,0 @@ -// contracts/MyERC7786ReceiverContract.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.27; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {AxelarGatewaySource} from "../../../crosschain/axelar/AxelarGatewaySource.sol"; -import {AxelarGatewayBase} from "../../../crosschain/axelar/AxelarGatewayBase.sol"; -import {IAxelarGateway} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; - -abstract contract MyCustomAxelarGatewaySource is AxelarGatewaySource { - /// @dev Initializes the contract with the Axelar gateway and the initial owner. - constructor(IAxelarGateway gateway, address initialOwner) Ownable(initialOwner) AxelarGatewayBase(gateway) {} -} diff --git a/test/crosschain/ERC7786OpenBridge.test.js b/test/crosschain/ERC7786OpenBridge.test.js index 9eaee54f..39a4c1e3 100644 --- a/test/crosschain/ERC7786OpenBridge.test.js +++ b/test/crosschain/ERC7786OpenBridge.test.js @@ -127,7 +127,7 @@ describe('ERC7786OpenBridge', function () { await expect(txPromise) .to.emit(this.bridgeA, 'MessageSent') .withArgs( - ethers.ZeroHash, + anyValue, this.chain.toErc7930(this.sender), this.chain.toErc7930(this.destination), this.payload, @@ -140,7 +140,7 @@ describe('ERC7786OpenBridge', function () { await expect(txPromise) .to.emit(gatewayA, 'MessageSent') .withArgs( - ethers.ZeroHash, + anyValue, this.chain.toErc7930(this.bridgeA), this.chain.toErc7930(this.bridgeB), anyValue, diff --git a/test/crosschain/axelar/AxelarGateway.test.js b/test/crosschain/axelar/AxelarGateway.test.js index 3f0a601a..08441418 100644 --- a/test/crosschain/axelar/AxelarGateway.test.js +++ b/test/crosschain/axelar/AxelarGateway.test.js @@ -3,17 +3,19 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); +const ERC7786Attributes = require('../../helpers/erc7786attributes'); + const AxelarHelper = require('./AxelarHelper'); async function fixture() { - const [owner, sender, ...accounts] = await ethers.getSigners(); + const [owner, sender, refundRecipient, ...accounts] = await ethers.getSigners(); const { chain, axelar, gatewayA, gatewayB } = await AxelarHelper.deploy(owner); const receiver = await ethers.deployContract('$ERC7786ReceiverMock', [gatewayB]); const invalidReceiver = await ethers.deployContract('$ERC7786ReceiverInvalidMock'); - return { owner, sender, accounts, chain, axelar, gatewayA, gatewayB, receiver, invalidReceiver }; + return { owner, sender, refundRecipient, accounts, chain, axelar, gatewayA, gatewayB, receiver, invalidReceiver }; } describe('AxelarGateway', function () { @@ -22,14 +24,16 @@ describe('AxelarGateway', function () { }); it('initial setup', async function () { - await expect(this.gatewayA.gateway()).to.eventually.equal(this.axelar); + await expect(this.gatewayA.gateway()).to.eventually.equal(this.axelar.gateway); + await expect(this.gatewayA.gasService()).to.eventually.equal(this.axelar.gasService); await expect(this.gatewayA.getAxelarChain(this.chain.erc7930)).to.eventually.equal('local'); await expect(this.gatewayA.getErc7930Chain('local')).to.eventually.equal(this.chain.erc7930); await expect(this.gatewayA.getRemoteGateway(this.chain.erc7930)).to.eventually.equal( this.gatewayB.target.toLowerCase(), ); - await expect(this.gatewayB.gateway()).to.eventually.equal(this.axelar); + await expect(this.gatewayB.gateway()).to.eventually.equal(this.axelar.gateway); + await expect(this.gatewayB.gasService()).to.eventually.equal(this.axelar.gasService); await expect(this.gatewayB.getAxelarChain(this.chain.erc7930)).to.eventually.equal('local'); await expect(this.gatewayB.getErc7930Chain('local')).to.eventually.equal(this.chain.erc7930); await expect(this.gatewayB.getRemoteGateway(this.chain.erc7930)).to.eventually.equal( @@ -47,13 +51,48 @@ describe('AxelarGateway', function () { [erc7930Sender, erc7930Recipient, payload], ); + const sendId = '0x0000000000000000000000000000000000000000000000000000000000000001'; + await expect(this.gatewayA.connect(this.sender).sendMessage(erc7930Recipient, payload, attributes)) .to.emit(this.gatewayA, 'MessageSent') - .withArgs(ethers.ZeroHash, erc7930Sender, erc7930Recipient, payload, 0n, attributes) - .to.emit(this.axelar, 'ContractCall') + .withArgs(sendId, erc7930Sender, erc7930Recipient, payload, 0n, attributes) + .to.emit(this.axelar.gateway, 'ContractCall') + .withArgs(this.gatewayA, 'local', this.gatewayB, ethers.keccak256(encoded), encoded) + .to.emit(this.axelar.gateway, 'MessageExecuted') + .withArgs(anyValue) + .to.emit(this.receiver, 'MessageReceived') + .withArgs(this.gatewayB, anyValue, erc7930Sender, payload); + + await expect(this.gatewayA.connect(this.sender).requestRelay(sendId, 0n, this.refundRecipient, { value: 1000n })) + .to.emit(this.axelar.gasService, 'NativeGasPaidForContractCall') + .withArgs(this.gatewayA, 'local', this.gatewayB, ethers.keccak256(encoded), 1000n, this.refundRecipient); + }); + + it('workflow (with requestRelay attribute)', async function () { + const erc7930Sender = this.chain.toErc7930(this.sender); + const erc7930Recipient = this.chain.toErc7930(this.receiver); + const payload = ethers.randomBytes(128); + const attributes = [ + ERC7786Attributes.encodeFunctionData('requestRelay', [1000n, 0n, this.refundRecipient.address]), + ]; + const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes', 'bytes', 'bytes'], + [erc7930Sender, erc7930Recipient, payload], + ); + + const sendId = '0x0000000000000000000000000000000000000000000000000000000000000000'; + + await expect( + this.gatewayA.connect(this.sender).sendMessage(erc7930Recipient, payload, attributes, { value: 1000n }), + ) + .to.emit(this.gatewayA, 'MessageSent') + .withArgs(sendId, erc7930Sender, erc7930Recipient, payload, 0n, attributes) + .to.emit(this.axelar.gateway, 'ContractCall') .withArgs(this.gatewayA, 'local', this.gatewayB, ethers.keccak256(encoded), encoded) - .to.emit(this.axelar, 'MessageExecuted') + .to.emit(this.axelar.gateway, 'MessageExecuted') .withArgs(anyValue) + .to.emit(this.axelar.gasService, 'NativeGasPaidForContractCall') + .withArgs(this.gatewayA, 'local', this.gatewayB, ethers.keccak256(encoded), 1000n, this.refundRecipient) .to.emit(this.receiver, 'MessageReceived') .withArgs(this.gatewayB, anyValue, erc7930Sender, payload); }); diff --git a/test/crosschain/axelar/AxelarHelper.js b/test/crosschain/axelar/AxelarHelper.js index 9eaab3bd..16807c4c 100644 --- a/test/crosschain/axelar/AxelarHelper.js +++ b/test/crosschain/axelar/AxelarHelper.js @@ -4,9 +4,13 @@ const { getLocalChain } = require('@openzeppelin/contracts/test/helpers/chains') async function deploy(owner) { const chain = await getLocalChain(); - const axelar = await ethers.deployContract('AxelarGatewayMock'); - const gatewayA = await ethers.deployContract('AxelarGatewayDuplex', [axelar, owner]); - const gatewayB = await ethers.deployContract('AxelarGatewayDuplex', [axelar, owner]); + const axelar = await Promise.all([ + ethers.deployContract('AxelarGatewayMock'), + ethers.deployContract('AxelarGasServiceMock'), + ]).then(([gateway, gasService]) => ({ gateway, gasService })); + + const gatewayA = await ethers.deployContract('AxelarGatewayDuplex', [axelar.gateway, axelar.gasService, owner]); + const gatewayB = await ethers.deployContract('AxelarGatewayDuplex', [axelar.gateway, axelar.gasService, owner]); await Promise.all([ gatewayA.connect(owner).registerChainEquivalence(chain.erc7930, 'local'), diff --git a/test/crosschain/utils/ERC7786Attributes.test.js b/test/crosschain/utils/ERC7786Attributes.test.js new file mode 100644 index 00000000..fef92b5c --- /dev/null +++ b/test/crosschain/utils/ERC7786Attributes.test.js @@ -0,0 +1,50 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const ERC7786Attributes = require('../../helpers/erc7786attributes'); + +async function fixture() { + const mock = await ethers.deployContract('$ERC7786Attributes'); + return { mock }; +} + +// NOTE: here we are only testing the receiver. Failures of the gateway itself (invalid attributes, ...) are out of scope. +describe('ERC7786Attributes', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('requestRelay', function () { + it('decode properly formatted attribute', async function () { + const value = ethers.toBigInt(ethers.randomBytes(32)); + const gasLimit = ethers.toBigInt(ethers.randomBytes(32)); + const refundRecipient = ethers.getAddress(ethers.hexlify(ethers.randomBytes(20))); + + this.input = ERC7786Attributes.encodeFunctionData('requestRelay', [value, gasLimit, refundRecipient]); + this.output = [true, value, gasLimit, refundRecipient]; + }); + + it('data is too short', async function () { + const value = ethers.toBigInt(ethers.randomBytes(32)); + const gasLimit = ethers.toBigInt(ethers.randomBytes(32)); + const refundRecipient = ethers.getAddress(ethers.hexlify(ethers.randomBytes(20))); + + this.input = ERC7786Attributes.encodeFunctionData('requestRelay', [value, gasLimit, refundRecipient]).slice( + 0, + -2, + ); // drop one byte + this.output = [false, 0n, 0n, ethers.ZeroAddress]; + }); + + it('wrong selector', async function () { + this.input = ethers.hexlify(ethers.randomBytes(0x64)); + this.output = [false, 0n, 0n, ethers.ZeroAddress]; + }); + + afterEach(async function () { + await expect(this.mock.$tryDecodeRequestRelay(this.input)).to.eventually.deep.equal(this.output); + await expect(this.mock.$tryDecodeRequestRelayCalldata(this.input)).to.eventually.deep.equal(this.output); + }); + }); +}); diff --git a/test/crosschain/ERC7786Receiver.test.js b/test/crosschain/utils/ERC7786Receiver.test.js similarity index 100% rename from test/crosschain/ERC7786Receiver.test.js rename to test/crosschain/utils/ERC7786Receiver.test.js diff --git a/test/helpers/erc7786attributes.js b/test/helpers/erc7786attributes.js new file mode 100644 index 00000000..a7d836fb --- /dev/null +++ b/test/helpers/erc7786attributes.js @@ -0,0 +1,3 @@ +const { Interface } = require('ethers'); + +module.exports = Interface.from(['function requestRelay(uint256 value, uint256 gasLimit, address refundRecipient)']);