diff --git a/contracts/crosschain/axelar/AxelarGatewayAdaptor.sol b/contracts/crosschain/axelar/AxelarGatewayAdaptor.sol new file mode 100644 index 00000000..fa67afb1 --- /dev/null +++ b/contracts/crosschain/axelar/AxelarGatewayAdaptor.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT + +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 {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +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 {IERC7786Receiver} from "../../interfaces/IERC7786.sol"; +import {ERC7786Attributes} from "../utils/ERC7786Attributes.sol"; +import {AxelarGatewayBase} from "./AxelarGatewayBase.sol"; + +/** + * @dev Implementation of an ERC-7786 gateway destination adapter for the Axelar Network in dual mode. + * + * The contract implements AxelarExecutable's {_execute} function to execute the message, converting Axelar's native + * workflow into the standard ERC-7786. + */ +contract AxelarGatewayAdaptor is IERC7786GatewaySource, AxelarGatewayBase, AxelarExecutable { + using InteroperableAddress for bytes; + using Strings for *; + + struct MessageDetails { + string destination; + string target; + bytes payload; + } + + uint256 private _sendId; + mapping(bytes32 => MessageDetails) private _details; + + error UnsupportedNativeTransfer(); + error InvalidOriginGateway(string axelarSourceChain, string axelarSourceAddress); + error ReceiverExecutionFailed(); + + /// @dev Initializes the contract with the Axelar gateway and the initial owner. + constructor( + IAxelarGateway gateway, + IAxelarGasService gasService, + address initialOwner + ) Ownable(initialOwner) AxelarGatewayBase(gateway, gasService) AxelarExecutable(address(gateway)) {} + + /// @inheritdoc IERC7786GatewaySource + function supportsAttribute(bytes4 selector) public pure returns (bool) { + return selector == IERC7786Attributes.requestRelay.selector; + } + + /// @inheritdoc IERC7786GatewaySource + function sendMessage( + bytes calldata recipient, // Binary Interoperable Address + bytes calldata payload, + bytes[] calldata attributes + ) external payable returns (bytes32 sendId) { + // 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(++_sendId); + } + 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 early (stack too deep) + emit MessageSent(sendId, sender, recipient, payload, 0, attributes); + + // Send the message + (bytes2 chainType, bytes calldata chainReference, ) = recipient.parseV1Calldata(); + bytes memory remoteGateway = getRemoteGateway(chainType, chainReference); + string memory axelarDestination = getAxelarChain(InteroperableAddress.formatV1(chainType, chainReference, "")); + string memory axelarTarget = address(bytes20(remoteGateway)).toChecksumHexString(); // TODO non-evm chains? + + _axelarGateway.callContract(axelarDestination, axelarTarget, adapterPayload); + + if (withRelay) { + _axelarGasService.payNativeGasForContractCall{value: msg.value}( + address(this), + axelarDestination, + axelarTarget, + adapterPayload, + refundRecipient + ); + } else { + _details[sendId] = MessageDetails(axelarDestination, axelarTarget, adapterPayload); + } + } + + // TODO inheritdoc from interface when that is standardized + function requestRelay(bytes32 sendId, uint256 /*gasLimit*/, address refundRecipient) external payable { + MessageDetails storage details = _details[sendId]; + require(details.payload.length > 0); + + _axelarGasService.payNativeGasForContractCall{value: msg.value}( + address(this), + details.destination, + details.target, + details.payload, + refundRecipient + ); + } + + /** + * @dev Execution of a cross-chain message. + * + * In this function: + * + * - `axelarSourceChain` is in the Axelar format. It should not be expected to be a proper ERC-7930 format + * - `axelarSourceAddress` is the sender of the Axelar message. That should be the remote gateway on the chain + * which the message originates from. It is NOT the sender of the ERC-7786 crosschain message. + * + * Proper ERC-7930 encoding of the crosschain message sender can be found in the message + */ + function _execute( + bytes32 commandId, + string calldata axelarSourceChain, // chain of the remote gateway - axelar format + string calldata axelarSourceAddress, // address of the remote gateway + bytes calldata adapterPayload + ) internal override { + // Parse the package + (bytes memory sender, bytes memory recipient, bytes memory payload) = abi.decode( + adapterPayload, + (bytes, bytes, bytes) + ); + + // Axelar to ERC-7930 translation + bytes memory addr = getRemoteGateway(getErc7930Chain(axelarSourceChain)); + + // check message validity + // - `axelarSourceAddress` is the remote gateway on the origin chain. + require( + address(bytes20(addr)).toChecksumHexString().equal(axelarSourceAddress), // TODO non-evm chains? + InvalidOriginGateway(axelarSourceChain, axelarSourceAddress) + ); + + (, address target) = recipient.parseEvmV1(); + bytes4 result = IERC7786Receiver(target).receiveMessage(commandId, sender, payload); + require(result == IERC7786Receiver.receiveMessage.selector, ReceiverExecutionFailed()); + } +} diff --git a/contracts/crosschain/axelar/AxelarGatewayBase.sol b/contracts/crosschain/axelar/AxelarGatewayBase.sol index f88f808c..4777ca01 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,19 @@ 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; + } + + // This is already exposed by AxelarExecutable which AxelarDestinationGateway inherit from. Because its not + // virtual, resolution is not possible. Therefore, we should not expose it. + // function gateway() public view virtual returns (IAxelarGateway) { + // return _axelarGateway; + // } + + 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/AxelarGatewayDestination.sol b/contracts/crosschain/axelar/AxelarGatewayDestination.sol deleted file mode 100644 index d1164807..00000000 --- a/contracts/crosschain/axelar/AxelarGatewayDestination.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import {AxelarExecutable} from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; -import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {IERC7786Receiver} from "../../interfaces/IERC7786.sol"; -import {AxelarGatewayBase} from "./AxelarGatewayBase.sol"; - -/** - * @dev Implementation of an ERC-7786 gateway destination adapter for the Axelar Network in dual mode. - * - * The contract implements AxelarExecutable's {_execute} function to execute the message, converting Axelar's native - * workflow into the standard ERC-7786. - */ -abstract contract AxelarGatewayDestination is AxelarGatewayBase, AxelarExecutable { - using InteroperableAddress for bytes; - using Strings for *; - - error InvalidOriginGateway(string axelarSourceChain, string axelarSourceAddress); - error ReceiverExecutionFailed(); - - /** - * @dev Execution of a cross-chain message. - * - * In this function: - * - * - `axelarSourceChain` is in the Axelar format. It should not be expected to be a proper ERC-7930 format - * - `axelarSourceAddress` is the sender of the Axelar message. That should be the remote gateway on the chain - * which the message originates from. It is NOT the sender of the ERC-7786 crosschain message. - * - * Proper ERC-7930 encoding of the crosschain message sender can be found in the message - */ - function _execute( - bytes32 commandId, - string calldata axelarSourceChain, // chain of the remote gateway - axelar format - string calldata axelarSourceAddress, // address of the remote gateway - bytes calldata adapterPayload - ) internal override { - // Parse the package - (bytes memory sender, bytes memory recipient, bytes memory payload) = abi.decode( - adapterPayload, - (bytes, bytes, bytes) - ); - - // Axelar to ERC-7930 translation - bytes memory addr = getRemoteGateway(getErc7930Chain(axelarSourceChain)); - - // check message validity - // - `axelarSourceAddress` is the remote gateway on the origin chain. - require( - address(bytes20(addr)).toChecksumHexString().equal(axelarSourceAddress), // TODO non-evm chains? - InvalidOriginGateway(axelarSourceChain, axelarSourceAddress) - ); - - (, address target) = recipient.parseEvmV1(); - bytes4 result = IERC7786Receiver(target).receiveMessage(commandId, sender, payload); - require(result == IERC7786Receiver.receiveMessage.selector, ReceiverExecutionFailed()); - } -} diff --git a/contracts/crosschain/axelar/AxelarGatewayDuplex.sol b/contracts/crosschain/axelar/AxelarGatewayDuplex.sol deleted file mode 100644 index 73b22b07..00000000 --- a/contracts/crosschain/axelar/AxelarGatewayDuplex.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.27; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -import {AxelarGatewayBase, IAxelarGateway} from "./AxelarGatewayBase.sol"; -import {AxelarGatewayDestination, AxelarExecutable} from "./AxelarGatewayDestination.sol"; -import {AxelarGatewaySource} from "./AxelarGatewaySource.sol"; - -/** - * @dev A contract that combines the functionality of both the source and destination gateway - * adapters for the Axelar Network. Allowing to either send or receive messages across chains. - */ -// slither-disable-next-line locked-ether -contract AxelarGatewayDuplex is AxelarGatewaySource, AxelarGatewayDestination { - /// @dev Initializes the contract with the Axelar gateway and the initial owner. - constructor( - IAxelarGateway gateway, - address initialOwner - ) Ownable(initialOwner) AxelarGatewayBase(gateway) AxelarExecutable(address(gateway)) {} -} diff --git a/contracts/crosschain/axelar/AxelarGatewaySource.sol b/contracts/crosschain/axelar/AxelarGatewaySource.sol deleted file mode 100644 index 62399c9a..00000000 --- a/contracts/crosschain/axelar/AxelarGatewaySource.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: MIT - -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 {AxelarGatewayBase} from "./AxelarGatewayBase.sol"; - -/** - * @dev Implementation of an ERC-7786 gateway source adapter for the Axelar Network. - * - * The contract provides a way to send messages to a remote chain via the Axelar Network - * using the {sendMessage} function. - */ -abstract contract AxelarGatewaySource is IERC7786GatewaySource, AxelarGatewayBase { - using InteroperableAddress for bytes; - using Strings for address; - - error UnsupportedNativeTransfer(); - - /// @inheritdoc IERC7786GatewaySource - function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) { - return false; - } - - /// @inheritdoc IERC7786GatewaySource - function sendMessage( - bytes calldata recipient, // Binary Interoperable Address - 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])); - - // 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 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 - ); - - return sendId; - } -} diff --git a/contracts/crosschain/utils/ERC7786Attributes.sol b/contracts/crosschain/utils/ERC7786Attributes.sol new file mode 100644 index 00000000..e7290e04 --- /dev/null +++ b/contracts/crosschain/utils/ERC7786Attributes.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IERC7786Attributes} from "../../interfaces/IERC7786Attributes.sol"; + +library ERC7786Attributes { + 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; + if (success) { + assembly ("memory-safe") { + value := mload(add(attribute, 0x24)) + gasLimit := mload(add(attribute, 0x44)) + refundRecipient := mload(add(attribute, 0x64)) + } + } + } + + 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; + if (success) { + assembly ("memory-safe") { + value := calldataload(add(attribute.offset, 0x04)) + gasLimit := calldataload(add(attribute.offset, 0x24)) + refundRecipient := calldataload(add(attribute.offset, 0x44)) + } + } + } +} diff --git a/contracts/interfaces/IERC7786Attributes.sol b/contracts/interfaces/IERC7786Attributes.sol new file mode 100644 index 00000000..a398d2bc --- /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 ERC. + */ +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/axelar/AxelarGateway.test.js b/test/crosschain/axelar/AxelarGatewayAdaptor.test.js similarity index 53% rename from test/crosschain/axelar/AxelarGateway.test.js rename to test/crosschain/axelar/AxelarGatewayAdaptor.test.js index 3f0a601a..08441418 100644 --- a/test/crosschain/axelar/AxelarGateway.test.js +++ b/test/crosschain/axelar/AxelarGatewayAdaptor.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..a759050c 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('AxelarGatewayAdaptor', [axelar.gateway, axelar.gasService, owner]); + const gatewayB = await ethers.deployContract('AxelarGatewayAdaptor', [axelar.gateway, axelar.gasService, owner]); await Promise.all([ gatewayA.connect(owner).registerChainEquivalence(chain.erc7930, 'local'), 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)']);