diff --git a/Makefile b/Makefile index ec83231b..4316b8a5 100644 --- a/Makefile +++ b/Makefile @@ -18,11 +18,12 @@ BASE_KEY = --private-key ${PRIVATE_KEY} custom_ethereum := --with-gas-price 45000000000 # 53 gwei +custom_ethereum-testnet := --with-gas-price 130000000000 --legacy custom_polygon := --with-gas-price 190000000000 # 560 gwei custom_avalanche := --with-gas-price 27000000000 # 27 gwei custom_metis-testnet := --legacy --verifier-url https://goerli.explorer.metisdevops.link/api/ custom_metis := --verifier-url https://api.routescan.io/v2/network/mainnet/evm/1088/etherscan -custom_scroll-testnet := --legacy --with-gas-price 1000000000 # 1 gwei +custom_scroll-testnet := --legacy # 1 gwei # params: # 1 - path/file_name @@ -203,17 +204,20 @@ deploy-metis-adapters-test: deploy-base-adapters-test: $(call deploy_fn,Adapters/DeployCBaseAdapter,ethereum) +deploy-axelar-adapters-test: + $(call deploy_fn,Adapters/DeployAxelarAdapter,ethereum scroll) + ## Set sender bridge dapters. Only eth pol avax are needed as other networks will only receive set-ccf-sender-adapters-test: - $(call deploy_fn,CCC/Set_CCF_Sender_Adapters,avalanche) + $(call deploy_fn,CCC/Set_CCF_Sender_Adapters,ethereum) # Set the bridge adapters allowed to receive messages set-ccr-receiver-adapters-test: - $(call deploy_fn,CCC/Set_CCR_Receivers_Adapters,celo) + $(call deploy_fn,CCC/Set_CCR_Receivers_Adapters,scroll) # Sets the required confirmations set-ccr-confirmations-test: - $(call deploy_fn,CCC/Set_CCR_Confirmations,ethereum) + $(call deploy_fn,CCC/Set_CCR_Confirmations,scroll) # Funds CCC fund-crosschain-test: @@ -238,13 +242,13 @@ deploy-full-test: # ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------- HELPER SCRIPTS --------------------------------------------------------- remove-bridge-adapters: - $(call deploy_fn,helpers/RemoveBridgeAdapters,celo) + $(call deploy_fn,helpers/RemoveBridgeAdapters,ethereum) send-direct-message: - $(call deploy_fn,helpers/Send_Direct_CCMessage,avalanche) + $(call deploy_fn,helpers/Send_Direct_CCMessage,ethereum) deploy_mock_destination: - $(call deploy_fn,helpers/Deploy_Mock_destination,ethereum) + $(call deploy_fn,helpers/Deploy_Mock_destination,scroll) set-approved-ccf-senders: $(call deploy_fn,helpers/Set_Approved_Senders,ethereum) diff --git a/deployments/cc/testnet/scroll_sepolia.json b/deployments/cc/testnet/scroll_sepolia.json index 2a8a4ff0..d59f11f0 100644 --- a/deployments/cc/testnet/scroll_sepolia.json +++ b/deployments/cc/testnet/scroll_sepolia.json @@ -1,5 +1,6 @@ { "arbAdapter": "0x0000000000000000000000000000000000000000", + "axelarAdapter": "0x3003206c02a0EC2e556D5dAC9E87c8C9CACA6e5b", "baseAdapter": "0x0000000000000000000000000000000000000000", "ccipAdapter": "0x0000000000000000000000000000000000000000", "chainId": 534351, @@ -9,11 +10,12 @@ "crossChainControllerImpl": "0x4E097ce514bD5623E6dafE14118a6211d8226832", "emergencyRegistry": "0x0000000000000000000000000000000000000000", "gnosisAdapter": "0x0000000000000000000000000000000000000000", + "granularCCCGuardian": "0x0000000000000000000000000000000000000000", "guardian": "0x6D603081563784dB3f83ef1F65Cc389D94365Ac9", "hlAdapter": "0x0000000000000000000000000000000000000000", "lzAdapter": "0x0000000000000000000000000000000000000000", "metisAdapter": "0x0000000000000000000000000000000000000000", - "mockDestination": "0xF718d61c51EB65164e160A850b98E9038Fa873a5", + "mockDestination": "0xF1a818CE6b4F49452388099e6E671E42F1767AB6", "opAdapter": "0x0000000000000000000000000000000000000000", "owner": "0x6D603081563784dB3f83ef1F65Cc389D94365Ac9", "polAdapter": "0x0000000000000000000000000000000000000000", @@ -21,5 +23,6 @@ "proxyFactory": "0x72F460dA1f429391D2228369dB653E1441780810", "sameChainAdapter": "0x0000000000000000000000000000000000000000", "scrollAdapter": "0x9E96A989425d69Bec6DdA993Eadc8229D2AB1cef", + "wormholeAdapter": "0x0000000000000000000000000000000000000000", "zkevmAdapter": "0x0000000000000000000000000000000000000000" } \ No newline at end of file diff --git a/deployments/cc/testnet/sep.json b/deployments/cc/testnet/sep.json index 26972a98..0fb7fff5 100644 --- a/deployments/cc/testnet/sep.json +++ b/deployments/cc/testnet/sep.json @@ -1,5 +1,6 @@ { "arbAdapter": "0x0000000000000000000000000000000000000000", + "axelarAdapter": "0xf1E85fC9b66Aa0C74Fd86775860Cf7F9aF22fcb9", "baseAdapter": "0x0000000000000000000000000000000000000000", "ccipAdapter": "0x81Cb82983c973797FCADfC7b3F13ca15E03e7E98", "chainId": 11155111, @@ -9,6 +10,7 @@ "crossChainControllerImpl": "0x51Ca5ADc684f2C23deC1F8f762668D7a0Fd601fd", "emergencyRegistry": "0x0000000000000000000000000000000000000000", "gnosisAdapter": "0x0000000000000000000000000000000000000000", + "granularCCCGuardian": "0x0000000000000000000000000000000000000000", "guardian": "0x6D603081563784dB3f83ef1F65Cc389D94365Ac9", "hlAdapter": "0x158d2Ab713BD05a2a32F7a0776bA4F1c93d2937f", "lzAdapter": "0xd52F3438F755f46E35f4741B9443b1F0A5bA7eB1", @@ -23,4 +25,4 @@ "scrollAdapter": "0xba62183cc12d68ca9Cf9a8b2ed64107fAd1F1d2f", "wormholeAdapter": "0x42D8D601cB91df86367229d04D8cf3556464c5c7", "zkevmAdapter": "0x0000000000000000000000000000000000000000" -} +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 88362bad..57d0e5e7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -38,7 +38,7 @@ gnosis-testnet="${RPC_GNOSIS_TESTNET}" zkevm="${RPC_ZKEVM}" zkevm-testnet="${RPC_ZKEVM_TESTNET}" scroll="${RPC_SCROLL}" -scroll-testnet="${RPC_SCROLL_TESTNET}" +scroll-testnet="${RPC_SCROLL_SEPOLIA}" celo="${RPC_CELO}" celo-testnet="${RPC_CELO_ALFAJORES}" diff --git a/scripts/Adapters/DeployAxelarAdapter.sol b/scripts/Adapters/DeployAxelarAdapter.sol new file mode 100644 index 00000000..f86ab440 --- /dev/null +++ b/scripts/Adapters/DeployAxelarAdapter.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {AxelarAdapter, IAxelarAdapter, IBaseAdapter} from '../../src/contracts/adapters/axelar/AxelarAdapter.sol'; +import {AxelarAdapterTestnet} from '../contract_extensions/AxelarAdapter.sol'; +import './BaseAdapterScript.sol'; + +library AxelarAdapterDeploymentHelper { + struct AxelarAdapterArgs { + BaseAdapterArgs baseArgs; + address axelarGateway; + address axelarGasService; + } + + function getAdapterCode( + AxelarAdapterArgs memory axelarArgs + ) internal pure returns (bytes memory) { + bytes memory creationCode = axelarArgs.baseArgs.isTestnet + ? type(AxelarAdapterTestnet).creationCode + : type(AxelarAdapter).creationCode; + + return + abi.encodePacked( + creationCode, + abi.encode( + IAxelarAdapter.AxelarAdapterArgs({ + crossChainController: axelarArgs.baseArgs.crossChainController, + providerGasLimit: axelarArgs.baseArgs.providerGasLimit, + trustedRemotes: axelarArgs.baseArgs.trustedRemotes, + gateway: axelarArgs.axelarGateway, + gasService: axelarArgs.axelarGasService + }) + ) + ); + } +} + +abstract contract BaseDeployAxelarAdapter is BaseAdapterScript { + function AXELAR_GATEWAY() public view virtual returns (address); + + function AXELAR_GAS_SERVICE() public view virtual returns (address); + + function _getAdapterByteCode( + BaseAdapterArgs memory baseArgs + ) internal view override returns (bytes memory) { + require(AXELAR_GATEWAY() != address(0), 'Invalid Axelar Gateway'); + require(AXELAR_GAS_SERVICE() != address(0), 'Invalid Axelar Gas Service'); + + return + AxelarAdapterDeploymentHelper.getAdapterCode( + AxelarAdapterDeploymentHelper.AxelarAdapterArgs({ + baseArgs: baseArgs, + axelarGateway: AXELAR_GATEWAY(), + axelarGasService: AXELAR_GAS_SERVICE() + }) + ); + } +} diff --git a/scripts/contract_extensions/AxelarAdapter.sol b/scripts/contract_extensions/AxelarAdapter.sol new file mode 100644 index 00000000..154014cd --- /dev/null +++ b/scripts/contract_extensions/AxelarAdapter.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.8; + +import {TestNetChainIds} from 'aave-helpers/ChainIds.sol'; +import {IAxelarAdapter, AxelarAdapter, Strings} from '../../src/contracts/adapters/axelar/AxelarAdapter.sol'; + +/** + * @title AxelarAdapterTestnet + * @author BGD Labs + */ +contract AxelarAdapterTestnet is AxelarAdapter { + using Strings for string; + + /** + * @notice constructor for the Axelar adapter + * @param axelarArgs adapter base arguments + */ + constructor(IAxelarAdapter.AxelarAdapterArgs memory axelarArgs) AxelarAdapter(axelarArgs) {} + + /// @inheritdoc IAxelarAdapter + function axelarNativeToInfraChainId( + string memory nativeChainId + ) public pure override returns (uint256) { + if (nativeChainId.equal('ethereum-sepolia')) { + return TestNetChainIds.ETHEREUM_SEPOLIA; + } else if (nativeChainId.equal('Avalanche')) { + return TestNetChainIds.AVALANCHE_FUJI; + } else if (nativeChainId.equal('arbitrum-sepolia')) { + return TestNetChainIds.ARBITRUM_SEPOLIA; + } else if (nativeChainId.equal('optimism-sepolia')) { + return TestNetChainIds.OPTIMISM_SEPOLIA; + } else if (nativeChainId.equal('base-sepolia')) { + return TestNetChainIds.BASE_SEPOLIA; + } else if (nativeChainId.equal('binance')) { + return TestNetChainIds.BNB_TESTNET; + } else if (nativeChainId.equal('scroll')) { + return TestNetChainIds.SCROLL_SEPOLIA; + } else if (nativeChainId.equal('celo')) { + return TestNetChainIds.CELO_ALFAJORES; + } else if (nativeChainId.equal('Fantom')) { + return TestNetChainIds.FANTOM_TESTNET; + } else { + return 0; + } + } + + /// @inheritdoc IAxelarAdapter + function axelarInfraToNativeChainId( + uint256 infraChainId + ) public pure override returns (string memory) { + if (infraChainId == TestNetChainIds.ETHEREUM_SEPOLIA) { + return 'ethereum-sepolia'; + } else if (infraChainId == TestNetChainIds.AVALANCHE_FUJI) { + return 'Avalanche'; + } else if (infraChainId == TestNetChainIds.ARBITRUM_SEPOLIA) { + return 'arbitrum-sepolia'; + } else if (infraChainId == TestNetChainIds.OPTIMISM_SEPOLIA) { + return 'optimism-sepolia'; + } else if (infraChainId == TestNetChainIds.FANTOM_TESTNET) { + return 'Fantom'; + } else if (infraChainId == TestNetChainIds.BASE_SEPOLIA) { + return 'base-sepolia'; + } else if (infraChainId == TestNetChainIds.SCROLL_SEPOLIA) { + return 'scroll'; + } else if (infraChainId == TestNetChainIds.BNB_TESTNET) { + return 'binance'; + } else if (infraChainId == TestNetChainIds.CELO_ALFAJORES) { + return 'celo'; + } else { + return ''; + } + } +} diff --git a/src/contracts/adapters/axelar/AxelarAdapter.sol b/src/contracts/adapters/axelar/AxelarAdapter.sol new file mode 100644 index 00000000..5a8bda81 --- /dev/null +++ b/src/contracts/adapters/axelar/AxelarAdapter.sol @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import {IAxelarAdapter, IAxelarGateway, IAxelarGasService, IBaseAdapter} from './IAxelarAdapter.sol'; +import {SafeCast} from 'openzeppelin-contracts/contracts/utils/math/SafeCast.sol'; +import {BaseAdapter} from '../BaseAdapter.sol'; +import {ChainIds} from 'aave-helpers/ChainIds.sol'; +import {Errors} from '../../libs/Errors.sol'; +import {IAxelarExecutable} from './interfaces/IAxelarExecutable.sol'; +import {Strings} from 'openzeppelin-contracts/contracts/utils/Strings.sol'; +import {StringToAddress} from './libs/StringToAddress.sol'; + +/** + * @title AxelarAdapter + * @author BGD Labs + * @notice Axelar bridge adapter. Used to send and receive messages cross chain + * @dev it uses the eth balance of CrossChainController contract to pay for message bridging as the method to bridge + is called via delegate call + */ +contract AxelarAdapter is BaseAdapter, IAxelarAdapter, IAxelarExecutable { + using Strings for string; + + /// @inheritdoc IAxelarAdapter + IAxelarGateway public immutable AXELAR_GATEWAY; + + /// @inheritdoc IAxelarAdapter + IAxelarGasService public immutable AXELAR_GAS_SERVICE; + + /** + * @notice constructor for the Axelar adapter + * @param axelarArgs adapter base arguments + */ + constructor( + AxelarAdapterArgs memory axelarArgs + ) + BaseAdapter( + axelarArgs.crossChainController, + axelarArgs.providerGasLimit, + 'Axelar adapter', + axelarArgs.trustedRemotes + ) + { + require(axelarArgs.gateway != address(0), Errors.INVALID_AXELAR_GATEWAY); + require(axelarArgs.gasService != address(0), Errors.INVALID_AXELAR_GAS_SERVICE); + AXELAR_GATEWAY = IAxelarGateway(axelarArgs.gateway); + AXELAR_GAS_SERVICE = IAxelarGasService(axelarArgs.gasService); + } + + /// @inheritdoc IBaseAdapter + function forwardMessage( + address receiver, + uint256 executionGasLimit, + uint256 destinationChainId, + bytes calldata message + ) external returns (address, uint256) { + string memory nativeChainId = axelarInfraToNativeChainId(destinationChainId); + require(!nativeChainId.equal(''), Errors.DESTINATION_CHAIN_ID_NOT_SUPPORTED); + require(receiver != address(0), Errors.RECEIVER_NOT_SET); + + uint256 totalGasLimit = executionGasLimit + BASE_GAS_LIMIT; + + uint256 gasEstimate = AXELAR_GAS_SERVICE.estimateGasFee( + nativeChainId, + Strings.toHexString(receiver), + message, + totalGasLimit, + new bytes(0) + ); + + require(gasEstimate <= address(this).balance, Errors.NOT_ENOUGH_VALUE_TO_PAY_BRIDGE_FEES); + + AXELAR_GAS_SERVICE.payGas{value: gasEstimate}( + address(this), + nativeChainId, + Strings.toHexString(receiver), + message, + totalGasLimit, + true, + address(this), + new bytes(0) + ); + AXELAR_GATEWAY.callContract(nativeChainId, Strings.toHexString(receiver), message); + + return (address(AXELAR_GATEWAY), 0); + } + + /// @inheritdoc IAxelarExecutable + function execute( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload + ) external { + bytes32 payloadHash = keccak256(payload); + + require( + AXELAR_GATEWAY.validateContractCall(commandId, sourceChain, sourceAddress, payloadHash), + Errors.INVALID_AXELAR_GATEWAY_CONTRACT_CALL + ); + + uint256 originChainId = axelarNativeToInfraChainId(sourceChain); + address srcAddress = StringToAddress.stringToAddress(sourceAddress); + + require( + _trustedRemotes[originChainId] == srcAddress && srcAddress != address(0), + Errors.REMOTE_NOT_TRUSTED + ); + + _registerReceivedMessage(payload, originChainId); + } + + /// @inheritdoc IAxelarAdapter + function axelarNativeToInfraChainId( + string memory nativeChainId + ) public pure virtual returns (uint256) { + if (nativeChainId.equal('Ethereum')) { + return ChainIds.ETHEREUM; + } else if (nativeChainId.equal('Avalanche')) { + return ChainIds.AVALANCHE; + } else if (nativeChainId.equal('Polygon')) { + return ChainIds.POLYGON; + } else if (nativeChainId.equal('arbitrum')) { + return ChainIds.ARBITRUM; + } else if (nativeChainId.equal('optimism')) { + return ChainIds.OPTIMISM; + } else if (nativeChainId.equal('base')) { + return ChainIds.BASE; + } else if (nativeChainId.equal('binance')) { + return ChainIds.BNB; + } else if (nativeChainId.equal('scroll')) { + return ChainIds.SCROLL; + } else if (nativeChainId.equal('celo')) { + return ChainIds.CELO; + } else if (nativeChainId.equal('Fantom')) { + return ChainIds.FANTOM; + } else { + return 0; + } + } + + /// @inheritdoc IAxelarAdapter + function axelarInfraToNativeChainId( + uint256 infraChainId + ) public pure virtual returns (string memory) { + if (infraChainId == ChainIds.ETHEREUM) { + return 'Ethereum'; + } else if (infraChainId == ChainIds.AVALANCHE) { + return 'Avalanche'; + } else if (infraChainId == ChainIds.POLYGON) { + return 'Polygon'; + } else if (infraChainId == ChainIds.ARBITRUM) { + return 'arbitrum'; + } else if (infraChainId == ChainIds.OPTIMISM) { + return 'optimism'; + } else if (infraChainId == ChainIds.FANTOM) { + return 'Fantom'; + } else if (infraChainId == ChainIds.BASE) { + return 'base'; + } else if (infraChainId == ChainIds.SCROLL) { + return 'scroll'; + } else if (infraChainId == ChainIds.BNB) { + return 'binance'; + } else if (infraChainId == ChainIds.CELO) { + return 'celo'; + } else { + return ''; + } + } + + /// @inheritdoc IBaseAdapter + function nativeToInfraChainId(uint256) public pure override returns (uint256) { + revert('Use axelarNativeToInfraChainId instead'); + } + + /// @inheritdoc IBaseAdapter + function infraToNativeChainId(uint256) public pure override returns (uint256) { + revert('Use axelarInfraToNativeChainId instead'); + } +} diff --git a/src/contracts/adapters/axelar/IAxelarAdapter.sol b/src/contracts/adapters/axelar/IAxelarAdapter.sol new file mode 100644 index 00000000..60735f8d --- /dev/null +++ b/src/contracts/adapters/axelar/IAxelarAdapter.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IAxelarGateway} from './interfaces/IAxelarGateway.sol'; +import {IAxelarGasService} from './interfaces/IAxelarGasService.sol'; +import {IBaseAdapter} from '../BaseAdapter.sol'; + +/** + * @title IAxelarAdapter + * @author BGD Labs + * @notice interface containing the events, objects and method definitions used in the Axelar bridge adapter + */ +interface IAxelarAdapter { + /** + * @notice Adapter constructor arguments + * @param crossChainController address of the contract that manages cross chain infrastructure + * @param providerGasLimit base gas limit used by the bridge adapter + * @param trustedRemotes array of objects with chain id and origin addresses which will be allowed to send messages to this adapter + * @param gateway address of the axelar gateway endpoint on the current chain where adapter is deployed + * @param gasService address of the axelar gas service endpoint on the current chain where adapter is deployed + */ + struct AxelarAdapterArgs { + address crossChainController; + uint256 providerGasLimit; + IBaseAdapter.TrustedRemotesConfig[] trustedRemotes; + address gateway; + address gasService; + } + + /** + * @notice returns the Axelar gateway endpoint address + * @return Axelar gateway endpoint address + */ + function AXELAR_GATEWAY() external view returns (IAxelarGateway); + + /** + * @notice returns the Axelar gas service endpoint address + * @return Axelar gas service endpoint address + */ + function AXELAR_GAS_SERVICE() external view returns (IAxelarGasService); + + /** + * @notice method to get infrastructure chain id from bridge native chain id + * @param nativeChainId bridge native chain id + */ + function axelarNativeToInfraChainId(string memory nativeChainId) external returns (uint256); + + /** + * @notice method to get bridge native chain id from native bridge chain id + * @param infraChainId infrastructure chain id + */ + function axelarInfraToNativeChainId(uint256 infraChainId) external returns (string memory); +} diff --git a/src/contracts/adapters/axelar/interfaces/IAxelarExecutable.sol b/src/contracts/adapters/axelar/interfaces/IAxelarExecutable.sol new file mode 100644 index 00000000..4e7977b7 --- /dev/null +++ b/src/contracts/adapters/axelar/interfaces/IAxelarExecutable.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// Modified from commit https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/commit/f0222fe45be6c463914924850b5521826dec1b75 +pragma solidity ^0.8.0; + +interface IAxelarExecutable { + function execute( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload + ) external; +} diff --git a/src/contracts/adapters/axelar/interfaces/IAxelarGasService.sol b/src/contracts/adapters/axelar/interfaces/IAxelarGasService.sol new file mode 100644 index 00000000..515790b1 --- /dev/null +++ b/src/contracts/adapters/axelar/interfaces/IAxelarGasService.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// Modified from commit https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/commit/269a8980e12a9a0b81da59b32f98c3c41024ea4e +pragma solidity ^0.8.0; + +/** + * @title IAxelarGasService Interface + * @notice This is an interface for the AxelarGasService contract which manages gas payments + * and refunds for cross-chain communication on the Axelar network. + */ +interface IAxelarGasService { + /** + * @notice Pay for gas for any type of contract execution on a destination chain. + * @dev This function is called on the source chain before calling the gateway to execute a remote contract. + * @dev If estimateOnChain is true, the function will estimate the gas cost and revert if the payment is insufficient. + * @param sender The address making the payment + * @param destinationChain The target chain where the contract call will be made + * @param destinationAddress The target address on the destination chain + * @param payload Data payload for the contract call + * @param executionGasLimit The gas limit for the contract call + * @param estimateOnChain Flag to enable on-chain gas estimation + * @param refundAddress The address where refunds, if any, should be sent + * @param params Additional parameters for gas payment. This can be left empty for normal contract call payments. + */ + function payGas( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + uint256 executionGasLimit, + bool estimateOnChain, + address refundAddress, + bytes calldata params + ) external payable; + + /** + * @notice Estimates the gas fee for a cross-chain contract call. + * @param destinationChain Axelar registered name of the destination chain + * @param destinationAddress Destination contract address being called + * @param executionGasLimit The gas limit to be used for the destination contract execution, + * e.g. pass in 200k if your app consumes needs upto 200k for this contract call + * @param params Additional parameters for the gas estimation + * @return gasEstimate The cross-chain gas estimate, in terms of source chain's native gas token that should be forwarded to the gas service. + */ + function estimateGasFee( + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + uint256 executionGasLimit, + bytes calldata params + ) external view returns (uint256 gasEstimate); +} diff --git a/src/contracts/adapters/axelar/interfaces/IAxelarGateway.sol b/src/contracts/adapters/axelar/interfaces/IAxelarGateway.sol new file mode 100644 index 00000000..36a02bb5 --- /dev/null +++ b/src/contracts/adapters/axelar/interfaces/IAxelarGateway.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// Modified from commit: https://github.com/axelarnetwork/axelar-cgp-solidity/commit/f6c45b1c8b6c70199925a5c0e35205b544db0a41 +pragma solidity ^0.8.0; + +interface IAxelarGateway { + function callContract( + string calldata destinationChain, + string calldata contractAddress, + bytes calldata payload + ) external; + + function validateContractCall( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes32 payloadHash + ) external returns (bool); +} diff --git a/src/contracts/adapters/axelar/libs/StringToAddress.sol b/src/contracts/adapters/axelar/libs/StringToAddress.sol new file mode 100644 index 00000000..03df1c9f --- /dev/null +++ b/src/contracts/adapters/axelar/libs/StringToAddress.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library StringToAddress { + function stringToAddress(string memory _address) internal pure returns (address) { + string memory cleanAddress = remove0xPrefix(_address); + bytes20 _addressBytes = parseHexStringToBytes20(cleanAddress); + return address(_addressBytes); + } + + function remove0xPrefix(string memory _hexString) internal pure returns (string memory) { + if ( + bytes(_hexString).length >= 2 && + bytes(_hexString)[0] == '0' && + (bytes(_hexString)[1] == 'x' || bytes(_hexString)[1] == 'X') + ) { + return substring(_hexString, 2, bytes(_hexString).length); + } + return _hexString; + } + + function substring( + string memory _str, + uint256 _start, + uint256 _end + ) internal pure returns (string memory) { + bytes memory _strBytes = bytes(_str); + bytes memory _result = new bytes(_end - _start); + for (uint256 i = _start; i < _end; i++) { + _result[i - _start] = _strBytes[i]; + } + return string(_result); + } + + function parseHexStringToBytes20(string memory _hexString) internal pure returns (bytes20) { + bytes memory _bytesString = bytes(_hexString); + uint160 _parsedBytes = 0; + for (uint256 i = 0; i < _bytesString.length; i += 2) { + _parsedBytes *= 256; + uint8 _byteValue = parseByteToUint8(_bytesString[i]); + _byteValue *= 16; + _byteValue += parseByteToUint8(_bytesString[i + 1]); + _parsedBytes += _byteValue; + } + return bytes20(_parsedBytes); + } + + function parseByteToUint8(bytes1 _byte) internal pure returns (uint8) { + if (uint8(_byte) >= 48 && uint8(_byte) <= 57) { + return uint8(_byte) - 48; + } else if (uint8(_byte) >= 65 && uint8(_byte) <= 70) { + return uint8(_byte) - 55; + } else if (uint8(_byte) >= 97 && uint8(_byte) <= 102) { + return uint8(_byte) - 87; + } else { + revert(string(abi.encodePacked('Invalid byte value: ', _byte))); + } + } +} diff --git a/src/contracts/libs/Errors.sol b/src/contracts/libs/Errors.sol index fef31f8f..391580f3 100644 --- a/src/contracts/libs/Errors.sol +++ b/src/contracts/libs/Errors.sol @@ -49,4 +49,7 @@ library Errors { string public constant INVALID_HL_MAILBOX = '40'; // the Hyperlane mailbox address can not be 0 string public constant WORMHOLE_RELAYER_CANT_BE_ADDRESS_0 = '41'; // Wormhole relayer can not be address 0 string public constant CALLER_NOT_WORMHOLE_RELAYER = '42'; // caller must be the Wormhole relayer + string public constant INVALID_AXELAR_GATEWAY = '43'; // axelar gateway can not be address 0 + string public constant INVALID_AXELAR_GAS_SERVICE = '44'; // axelar gas service can not be address 0 + string public constant INVALID_AXELAR_GATEWAY_CONTRACT_CALL = '45'; // axelar gateway contract call validation failed } diff --git a/tests/adapters/AxelarAdapter.t.sol b/tests/adapters/AxelarAdapter.t.sol new file mode 100644 index 00000000..6ebd796e --- /dev/null +++ b/tests/adapters/AxelarAdapter.t.sol @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import 'forge-std/Test.sol'; +import {BaseAdapterTest} from './BaseAdapterTest.sol'; +import {ICrossChainReceiver} from '../../src/contracts/interfaces/ICrossChainReceiver.sol'; +import {ChainIds} from 'aave-helpers/ChainIds.sol'; +import {Errors} from '../../src/contracts/libs/Errors.sol'; +import {Strings} from 'openzeppelin-contracts/contracts/utils/Strings.sol'; +import {IAxelarAdapter, IBaseAdapter, AxelarAdapter, IAxelarGateway, IAxelarGasService} from '../../src/contracts/adapters/axelar/AxelarAdapter.sol'; + +contract AxelarAdapterTest is BaseAdapterTest { + using Strings for string; + AxelarAdapter internal axelarAdapter; + + event SetTrustedRemote(uint256 indexed originChainId, address indexed originForwarder); + + modifier setAxelarAdapter( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit, + uint256 originChainId + ) { + vm.assume(baseGasLimit < 1 ether); + _assumeSafeAddress(crossChainController); + _assumeSafeAddress(axelarGateway); + _assumeSafeAddress(axelarGasService); + _assumeSafeAddress(originForwarder); + vm.assume(originForwarder != address(0)); + vm.assume(originChainId > 0); + + IBaseAdapter.TrustedRemotesConfig memory originConfig = IBaseAdapter.TrustedRemotesConfig({ + originForwarder: originForwarder, + originChainId: originChainId + }); + IBaseAdapter.TrustedRemotesConfig[] + memory originConfigs = new IBaseAdapter.TrustedRemotesConfig[](1); + originConfigs[0] = originConfig; + + axelarAdapter = new AxelarAdapter( + IAxelarAdapter.AxelarAdapterArgs({ + crossChainController: crossChainController, + providerGasLimit: baseGasLimit, + trustedRemotes: originConfigs, + gateway: axelarGateway, + gasService: axelarGasService + }) + ); + _; + } + + function setUp() public {} + + function testWrongAxelarGateway( + address crossChainController, + uint256 baseGasLimit, + address originForwarder, + address axelarGasService, + uint256 originChainId + ) public { + vm.assume(crossChainController != address(0)); + vm.assume(originForwarder != address(0)); + vm.assume(originChainId > 0); + + IBaseAdapter.TrustedRemotesConfig memory originConfig = IBaseAdapter.TrustedRemotesConfig({ + originForwarder: originForwarder, + originChainId: originChainId + }); + IBaseAdapter.TrustedRemotesConfig[] + memory originConfigs = new IBaseAdapter.TrustedRemotesConfig[](1); + originConfigs[0] = originConfig; + + IAxelarAdapter.AxelarAdapterArgs memory baseArgs = IAxelarAdapter.AxelarAdapterArgs({ + crossChainController: crossChainController, + providerGasLimit: baseGasLimit, + trustedRemotes: originConfigs, + gateway: address(0), + gasService: axelarGasService + }); + + vm.expectRevert(bytes(Errors.INVALID_AXELAR_GATEWAY)); + new AxelarAdapter(baseArgs); + } + + function testWrongAxelarGasService( + address crossChainController, + uint256 baseGasLimit, + address originForwarder, + address axelarGateway, + uint256 originChainId + ) public { + vm.assume(axelarGateway != address(0)); + vm.assume(crossChainController != address(0)); + vm.assume(originForwarder != address(0)); + vm.assume(originChainId > 0); + + IBaseAdapter.TrustedRemotesConfig memory originConfig = IBaseAdapter.TrustedRemotesConfig({ + originForwarder: originForwarder, + originChainId: originChainId + }); + IBaseAdapter.TrustedRemotesConfig[] + memory originConfigs = new IBaseAdapter.TrustedRemotesConfig[](1); + originConfigs[0] = originConfig; + + IAxelarAdapter.AxelarAdapterArgs memory baseArgs = IAxelarAdapter.AxelarAdapterArgs({ + crossChainController: crossChainController, + providerGasLimit: baseGasLimit, + trustedRemotes: originConfigs, + gateway: axelarGateway, + gasService: address(0) + }); + + vm.expectRevert(bytes(Errors.INVALID_AXELAR_GAS_SERVICE)); + new AxelarAdapter(baseArgs); + } + + function testInitialize( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit, + uint256 originChainId + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + originChainId + ) + { + assertEq(axelarAdapter.getTrustedRemoteByChainId(originChainId), originForwarder); + } + + function testGetInfraChainFromBridgeChain( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + ChainIds.ETHEREUM + ) + { + assertEq(axelarAdapter.axelarNativeToInfraChainId('Ethereum'), ChainIds.ETHEREUM); + } + + function testGetBridgeChainFromInfraChain( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + ChainIds.ETHEREUM + ) + { + assertEq(axelarAdapter.axelarInfraToNativeChainId(ChainIds.ETHEREUM).equal('Ethereum'), true); + } + + function testForwardMessage( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit, + address caller, + uint256 dstGasLimit, + address receiver + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + ChainIds.ETHEREUM + ) + { + vm.assume(caller != address(0)); + vm.assume(dstGasLimit < 1 ether); + vm.assume(receiver != address(0)); + + _testForwardMessage(receiver, dstGasLimit, caller); + } + + function testForwardMessageWhenNoValue( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit, + uint256 dstGasLimit, + address receiver + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + ChainIds.ETHEREUM + ) + { + vm.assume(dstGasLimit < 1 ether); + vm.assume(receiver != address(0)); + _testForwardMessageWhenNoValue(receiver, dstGasLimit); + } + + function testForwardMessageWhenChainNotSupported( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit, + uint256 dstGasLimit, + address receiver, + bytes memory message + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + ChainIds.ETHEREUM + ) + { + vm.assume(dstGasLimit > 200000 && dstGasLimit < 1 ether); + vm.assume(receiver != address(0)); + + vm.expectRevert(bytes(Errors.DESTINATION_CHAIN_ID_NOT_SUPPORTED)); + axelarAdapter.forwardMessage(receiver, dstGasLimit, 11, message); + } + + function testForwardMessageWhenWrongReceiver( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit, + uint256 dstGasLimit, + bytes memory message + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + ChainIds.ETHEREUM + ) + { + vm.assume(dstGasLimit > 200000 && dstGasLimit < 1 ether); + + vm.expectRevert(bytes(Errors.RECEIVER_NOT_SET)); + axelarAdapter.forwardMessage(address(0), dstGasLimit, ChainIds.POLYGON, message); + } + + function testReceive( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit, + bytes memory payload + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + ChainIds.ETHEREUM + ) + { + vm.mockCall( + address(axelarAdapter.AXELAR_GATEWAY()), + abi.encodeWithSelector(IAxelarGateway.validateContractCall.selector), + abi.encode(true) + ); + vm.mockCall( + crossChainController, + abi.encodeWithSelector(ICrossChainReceiver.receiveCrossChainMessage.selector), + abi.encode() + ); + vm.expectCall( + crossChainController, + abi.encodeWithSelector(ICrossChainReceiver.receiveCrossChainMessage.selector, payload, 1) + ); + axelarAdapter.execute(bytes32(0), 'Ethereum', Strings.toHexString(originForwarder), payload); + } + + function testReceiveWhenContractCallNotConfirmed( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit, + bytes memory payload + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + ChainIds.ETHEREUM + ) + { + vm.mockCall( + address(axelarAdapter.AXELAR_GATEWAY()), + abi.encodeWithSelector(IAxelarGateway.validateContractCall.selector), + abi.encode(false) + ); + vm.expectRevert(bytes(Errors.INVALID_AXELAR_GATEWAY_CONTRACT_CALL)); + + axelarAdapter.execute(bytes32(0), 'Ethereum', Strings.toHexString(originForwarder), payload); + } + + function testReceiveWhenRemoteNotTrusted( + address crossChainController, + address axelarGateway, + address originForwarder, + address axelarGasService, + uint256 baseGasLimit, + bytes memory payload, + address remote + ) + public + setAxelarAdapter( + crossChainController, + axelarGateway, + originForwarder, + axelarGasService, + baseGasLimit, + ChainIds.ETHEREUM + ) + { + vm.assume(remote != originForwarder); + + vm.mockCall( + address(axelarAdapter.AXELAR_GATEWAY()), + abi.encodeWithSelector(IAxelarGateway.validateContractCall.selector), + abi.encode(true) + ); + + vm.expectRevert(bytes(Errors.REMOTE_NOT_TRUSTED)); + + axelarAdapter.execute(bytes32(0), 'Ethereum', Strings.toHexString(remote), payload); + } + + function _testForwardMessageWhenNoValue(address receiver, uint256 dstGasLimit) internal { + bytes memory message = abi.encode('test message'); + + vm.mockCall( + address(axelarAdapter.AXELAR_GAS_SERVICE()), + abi.encodeWithSelector(IAxelarGasService.estimateGasFee.selector), + abi.encode(10) + ); + + vm.expectRevert(bytes(Errors.NOT_ENOUGH_VALUE_TO_PAY_BRIDGE_FEES)); + (bool success, ) = address(axelarAdapter).delegatecall( + abi.encodeWithSelector( + IBaseAdapter.forwardMessage.selector, + receiver, + dstGasLimit, + ChainIds.POLYGON, + message + ) + ); + assertEq(success, false); + } + + function _testForwardMessage(address receiver, uint256 dstGasLimit, address caller) internal { + bytes memory message = abi.encode('test message'); + + hoax(caller, 10 ether); + + vm.mockCall( + address(axelarAdapter.AXELAR_GAS_SERVICE()), + abi.encodeWithSelector(IAxelarGasService.estimateGasFee.selector), + abi.encode(10) + ); + vm.mockCall( + address(axelarAdapter.AXELAR_GAS_SERVICE()), + 10, + abi.encodeWithSelector(IAxelarGasService.payGas.selector), + abi.encode() + ); + vm.mockCall( + address(axelarAdapter.AXELAR_GATEWAY()), + abi.encodeWithSelector(IAxelarGateway.callContract.selector), + abi.encode() + ); + (bool success, bytes memory returnData) = address(axelarAdapter).delegatecall( + abi.encodeWithSelector( + IBaseAdapter.forwardMessage.selector, + receiver, + dstGasLimit, + ChainIds.POLYGON, + message + ) + ); + vm.clearMockedCalls(); + + assertEq(success, true); + assertEq(returnData, abi.encode(address(axelarAdapter.AXELAR_GATEWAY()), 0)); + } +}