diff --git a/deploy/029_deploy_executor.ts b/deploy/029_deploy_executor.ts index 7b06b3f3b..c72c695d0 100644 --- a/deploy/029_deploy_executor.ts +++ b/deploy/029_deploy_executor.ts @@ -1,7 +1,6 @@ import { ethers, network } from 'hardhat' import { DeployFunction } from 'hardhat-deploy/types' import { HardhatRuntimeEnvironment } from 'hardhat/types' -import config from '../config/axelar' import sgConfig from '../config/stargate' import { Executor, ERC20Proxy, PeripheryRegistryFacet } from '../typechain' @@ -11,11 +10,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { deployer } = await getNamedAccounts() - let gateway = ethers.constants.AddressZero let sgRouter = ethers.constants.AddressZero - if (config[network.name]) { - gateway = config[network.name].gateway - } if (sgConfig[network.name]) { sgRouter = sgConfig[network.name].stargateRouter } @@ -49,7 +44,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { await deploy('Executor', { from: deployer, log: true, - args: [deployer, gateway, sgRouter, erc20Proxy.address], + args: [deployer, sgRouter, erc20Proxy.address], deterministicDeployment: true, }) @@ -68,7 +63,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { try { await hre.run('verify:verify', { address: executor.address, - constructorArguments: [deployer, gateway, sgRouter, erc20Proxy.address], + constructorArguments: [deployer, sgRouter, erc20Proxy.address], }) } catch (e) { console.log(`Failed to verify contract: ${e}`) diff --git a/deploy/032_deploy_axelar_executor.ts b/deploy/032_deploy_axelar_executor.ts new file mode 100644 index 000000000..fe94ec49d --- /dev/null +++ b/deploy/032_deploy_axelar_executor.ts @@ -0,0 +1,59 @@ +import { ethers, network } from 'hardhat' +import { DeployFunction } from 'hardhat-deploy/types' +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import config from '../config/axelar' +import { AxelarExecutor, PeripheryRegistryFacet } from '../typechain' + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre + const { deploy } = deployments + + const { deployer } = await getNamedAccounts() + + let gateway = ethers.constants.AddressZero + if (config[network.name]) { + gateway = config[network.name].gateway + } + + const diamond = await ethers.getContract('LiFiDiamond') + + const registryFacet = ( + await ethers.getContractAt('PeripheryRegistryFacet', diamond.address) + ) + + await deploy('AxelarExecutor', { + from: deployer, + log: true, + args: [deployer, gateway], + deterministicDeployment: true, + }) + + const executor: AxelarExecutor = await ethers.getContract('AxelarExecutor') + + const executorAddr = await registryFacet.getPeripheryContract( + 'AxelarExecutor' + ) + + if (executorAddr !== executor.address) { + console.log('Updating periphery registry...') + await registryFacet.registerPeripheryContract( + 'AxelarExecutor', + executor.address + ) + console.log('Done!') + } + + try { + await hre.run('verify:verify', { + address: executor.address, + constructorArguments: [deployer, gateway], + }) + } catch (e) { + console.log(`Failed to verify contract: ${e}`) + } +} + +export default func +func.id = 'deploy_axelar_executor' +func.tags = ['DeployAxelarExecutor'] +func.dependencies = ['DeployPeripheryRegistryFacet'] diff --git a/docs/AxelarExecutor.md b/docs/AxelarExecutor.md new file mode 100644 index 000000000..3fc638cc2 --- /dev/null +++ b/docs/AxelarExecutor.md @@ -0,0 +1,19 @@ +# Executor + +## Description + +Periphery contract used for aribitrary cross-chain execution using Axelar + +## How To Use + +The contract is used to parse payloads sent cross-chain using the Axelar cross-chain execution platform. + +The following external methods are available: + +The contract has one utility method for updating the Axelar gateway + +```solidity +/// @notice set the Axelar gateway +/// @param _gateway the Axelar gateway address +function setAxelarGateway(address _gateway) +``` diff --git a/docs/Executor.md b/docs/Executor.md index 1b06bd6b7..ace15e4ac 100644 --- a/docs/Executor.md +++ b/docs/Executor.md @@ -7,7 +7,7 @@ Periphery contract used for aribitrary cross-chain and same chain execution, swa ## How To Use The contract has a number of methods that can be called depending on the context of the transaction -and which third-party integration is being used (Stargate, Axelar, etc). +and which third-party integration is being used. The following methods are available: @@ -69,10 +69,6 @@ function swapAndExecute( The contract also has a number of utility methods that are self-explanatory ```solidity -/// @notice set the Axelar gateway -/// @param _gateway the Axelar gateway address -function setAxelarGateway(address _gateway) - /// @notice set Stargate Router /// @param _router the Stargate router address function setStargateRouter(address _router) diff --git a/src/Periphery/AxelarExecutor.sol b/src/Periphery/AxelarExecutor.sol new file mode 100644 index 000000000..9244f4f01 --- /dev/null +++ b/src/Periphery/AxelarExecutor.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.13; + +import { IAxelarExecutable } from "@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarExecutable.sol"; +import { IAxelarGateway } from "@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol"; +import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; +import { LibBytes } from "../Libraries/LibBytes.sol"; +import { LibAsset } from "../Libraries/LibAsset.sol"; + +contract AxelarExecutor is IAxelarExecutable, Ownable, ReentrancyGuard { + using LibBytes for bytes; + using SafeERC20 for IERC20; + + /// Errors /// + error UnAuthorized(); + error ExecutionFailed(); + error NotAContract(); + + /// Events /// + event AxelarGatewaySet(address indexed gateway); + event AxelarExecutionComplete(address indexed callTo, bytes4 selector); + + /// Constructor /// + constructor(address _owner, address _gateway) IAxelarExecutable(_gateway) { + transferOwnership(_owner); + emit AxelarGatewaySet(_gateway); + } + + /// External Methods /// + + /// @notice set the Axelar gateway + /// @param _gateway the Axelar gateway address + function setAxelarGateway(address _gateway) external onlyOwner { + gateway = IAxelarGateway(_gateway); + emit AxelarGatewaySet(_gateway); + } + + /// Internal Methods /// + + /// @dev override of IAxelarExecutable _execute() + /// @notice handles the parsing and execution of the payload + /// @param payload the abi.encodePacked payload [callTo:callData] + function _execute( + string memory, + string memory, + bytes calldata payload + ) internal override nonReentrant { + // The first 20 bytes of the payload are the callee address + address callTo = payload.toAddress(0); + + if (callTo == address(gateway)) revert UnAuthorized(); + if (!LibAsset.isContract(callTo)) revert NotAContract(); + + // The remaining bytes should be calldata + bytes memory callData = payload.slice(20, payload.length - 20); + + (bool success, ) = callTo.call(callData); + if (!success) revert ExecutionFailed(); + emit AxelarExecutionComplete(callTo, bytes4(callData)); + } + + /// @dev override of IAxelarExecutable _executeWithToken() + /// @notice handles the parsing and execution of the payload + /// @param payload the abi.encodePacked payload [callTo:callData] + /// @param tokenSymbol symbol of the token being bridged + /// @param amount of tokens being bridged + function _executeWithToken( + string memory, + string memory, + bytes calldata payload, + string memory tokenSymbol, + uint256 amount + ) internal override nonReentrant { + // The first 20 bytes of the payload are the callee address + address callTo = payload.toAddress(0); + + if (callTo == address(gateway)) revert UnAuthorized(); + if (!LibAsset.isContract(callTo)) revert NotAContract(); + + // The remaining bytes should be calldata + bytes memory callData = payload.slice(20, payload.length - 20); + + // get ERC-20 address from gateway + address tokenAddress = gateway.tokenAddresses(tokenSymbol); + + // transfer received tokens to the recipient + IERC20(tokenAddress).safeApprove(callTo, 0); + IERC20(tokenAddress).safeApprove(callTo, amount); + + (bool success, ) = callTo.call(callData); + if (!success) revert ExecutionFailed(); + } +} diff --git a/src/Periphery/Executor.sol b/src/Periphery/Executor.sol index 6781099e4..6e351f765 100644 --- a/src/Periphery/Executor.sol +++ b/src/Periphery/Executor.sol @@ -1,9 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.13; -import { IAxelarExecutable } from "@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarExecutable.sol"; -import { IAxelarGasService } from "@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarGasService.sol"; -import { IAxelarGateway } from "@axelar-network/axelar-cgp-solidity/contracts/interfaces/IAxelarGateway.sol"; import { IERC20 } from "@axelar-network/axelar-cgp-solidity/contracts/interfaces/IERC20.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { ReentrancyGuard } from "../Helpers/ReentrancyGuard.sol"; @@ -11,14 +8,11 @@ import { LibSwap } from "../Libraries/LibSwap.sol"; import { LibAsset } from "../Libraries/LibAsset.sol"; import { ILiFi } from "../Interfaces/ILiFi.sol"; import { IERC20Proxy } from "../Interfaces/IERC20Proxy.sol"; -import "../Libraries/LibBytes.sol"; /// @title Executor /// @author LI.FI (https://li.fi) /// @notice Arbitrary execution contract used for cross-chain swaps and message passing -contract Executor is IAxelarExecutable, Ownable, ReentrancyGuard, ILiFi { - using LibBytes for bytes; - +contract Executor is Ownable, ReentrancyGuard, ILiFi { /// Storage /// address public sgRouter; IERC20Proxy public erc20Proxy; @@ -30,10 +24,8 @@ contract Executor is IAxelarExecutable, Ownable, ReentrancyGuard, ILiFi { error UnAuthorized(); /// Events /// - event AxelarGatewaySet(address indexed gateway); event StargateRouterSet(address indexed router); event ERC20ProxySet(address indexed proxy); - event AxelarExecutionComplete(address indexed callTo, bytes4 selector); /// Modifiers /// @@ -59,27 +51,18 @@ contract Executor is IAxelarExecutable, Ownable, ReentrancyGuard, ILiFi { /// Constructor constructor( address _owner, - address _gateway, address _sgRouter, address _erc20Proxy - ) IAxelarExecutable(_gateway) { + ) { transferOwnership(_owner); sgRouter = _sgRouter; erc20Proxy = IERC20Proxy(_erc20Proxy); - emit AxelarGatewaySet(_gateway); emit StargateRouterSet(_sgRouter); emit ERC20ProxySet(_erc20Proxy); } /// External Methods /// - /// @notice set the Axelar gateway - /// @param _gateway the Axelar gateway address - function setAxelarGateway(address _gateway) external onlyOwner { - gateway = IAxelarGateway(_gateway); - emit AxelarGatewaySet(_gateway); - } - /// @notice set Stargate Router /// @param _router the Stargate router address function setStargateRouter(address _router) external onlyOwner { @@ -264,59 +247,6 @@ contract Executor is IAxelarExecutable, Ownable, ReentrancyGuard, ILiFi { emit LiFiTransferCompleted(_lifiData.transactionId, transferredAssetId, receiver, amount, block.timestamp); } - /// Internal Methods /// - - /// @dev override of IAxelarExecutable _execute() - /// @notice handles the parsing and execution of the payload - /// @param payload the abi.encodePacked payload [callTo:callData] - function _execute( - string memory, - string memory, - bytes calldata payload - ) internal override nonReentrant { - // The first 20 bytes of the payload are the callee address - address callTo = payload.toAddress(0); - - if (callTo == address(erc20Proxy)) revert UnAuthorized(); - - // The remaining bytes should be calldata - bytes memory callData = payload.slice(20, payload.length - 20); - - (bool success, ) = callTo.call(callData); - if (!success) revert ExecutionFailed(); - emit AxelarExecutionComplete(callTo, bytes4(callData)); - } - - /// @dev override of IAxelarExecutable _executeWithToken() - /// @notice handles the parsing and execution of the payload - /// @param payload the abi.encodePacked payload [callTo:callData] - /// @param tokenSymbol symbol of the token being bridged - /// @param amount of tokens being bridged - function _executeWithToken( - string memory, - string memory, - bytes calldata payload, - string memory tokenSymbol, - uint256 amount - ) internal override nonReentrant { - // The first 20 bytes of the payload are the callee address - address callTo = payload.toAddress(0); - - if (callTo == address(erc20Proxy)) revert UnAuthorized(); - - // The remaining bytes should be calldata - bytes memory callData = payload.slice(20, payload.length - 20); - - // get ERC-20 address from gateway - address tokenAddress = gateway.tokenAddresses(tokenSymbol); - - // transfer received tokens to the recipient - IERC20(tokenAddress).approve(callTo, amount); - - (bool success, ) = callTo.call(callData); - if (!success) revert ExecutionFailed(); - } - /// Private Methods /// /// @dev Executes swaps one after the other diff --git a/test/solidity/Periphery/AxelarExecutor.t.sol b/test/solidity/Periphery/AxelarExecutor.t.sol new file mode 100644 index 000000000..f1fc06ee4 --- /dev/null +++ b/test/solidity/Periphery/AxelarExecutor.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Unlicensed +pragma solidity 0.8.13; + +import { DSTest } from "ds-test/test.sol"; +import { console } from "../utils/Console.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { AxelarExecutor } from "lifi/Periphery/AxelarExecutor.sol"; +import { ERC20Proxy } from "lifi/Periphery/ERC20Proxy.sol"; +import { TestToken as ERC20 } from "../utils/TestToken.sol"; + +// Stub Vault Contract +contract Vault { + function deposit(address token, uint256 amount) external { + ERC20(token).transferFrom(msg.sender, address(this), amount); + } +} + +contract Setter { + string public message; + + function setMessage(string calldata _message) external { + message = _message; + } +} + +contract MockGateway { + mapping(string => address) public tokenAddresses; + + function validateContractCall( + bytes32, + string calldata, + string calldata, + bytes32 + ) external pure returns (bool) { + return true; + } + + function validateContractCallAndMint( + bytes32, + string calldata, + string calldata, + bytes32, + string memory, + uint256 + ) external pure returns (bool) { + return true; + } + + function setTokenAddress(string memory _symbol, address _tokenAddress) external { + tokenAddresses[_symbol] = _tokenAddress; + } +} + +contract ExecutorTest is DSTest { + Vm internal immutable vm = Vm(HEVM_ADDRESS); + AxelarExecutor internal executor; + Vault internal vault; + Setter internal setter; + MockGateway internal gw; + + function setUp() public { + gw = new MockGateway(); + executor = new AxelarExecutor(address(this), address(gw)); + vault = new Vault(); + setter = new Setter(); + } + + function testCanExecuteAxelarPayload() public { + executor.execute( + bytes32("abcde"), + "polygon", + "0x1234", + abi.encodePacked(address(setter), abi.encodeWithSignature("setMessage(string)", "lifi")) + ); + + assertEq(setter.message(), "lifi"); + } + + function testCanExecuteAxelarPayloadWithToken() public { + ERC20 aUSDC = new ERC20("Axelar USDC", "aUSDC", 18); + aUSDC.mint(address(this), 100 ether); + gw.setTokenAddress("aUSDC", address(aUSDC)); + aUSDC.transfer(address(executor), 0.01 ether); + executor.executeWithToken( + bytes32("abcde"), + "polygon", + "0x1234", + abi.encodePacked( + address(vault), + abi.encodeWithSignature("deposit(address,uint256)", address(aUSDC), 0.01 ether) + ), + "aUSDC", + 0.01 ether + ); + } +} diff --git a/test/solidity/Periphery/Executor.t.sol b/test/solidity/Periphery/Executor.t.sol index 8b461575e..d5dec05d9 100644 --- a/test/solidity/Periphery/Executor.t.sol +++ b/test/solidity/Periphery/Executor.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: Unlicensed pragma solidity 0.8.13; import { DSTest } from "ds-test/test.sol"; @@ -68,7 +68,7 @@ contract ExecutorTest is DSTest { function setUp() public { gw = new MockGateway(); erc20Proxy = new ERC20Proxy(address(this)); - executor = new Executor(address(this), address(gw), address(0), address(erc20Proxy)); + executor = new Executor(address(this), address(0), address(erc20Proxy)); erc20Proxy.setAuthorizedCaller(address(executor), true); amm = new TestAMM(); vault = new Vault(); @@ -269,35 +269,6 @@ contract ExecutorTest is DSTest { assertEq(tokenA.balanceOf(address(0xb33f)), 0.8 ether); } - function testCanExecuteAxelarPayload() public { - executor.execute( - bytes32("abcde"), - "polygon", - "0x1234", - abi.encodePacked(address(setter), abi.encodeWithSignature("setMessage(string)", "lifi")) - ); - - assertEq(setter.message(), "lifi"); - } - - function testCanExecuteAxelarPayloadWithToken() public { - ERC20 aUSDC = new ERC20("Axelar USDC", "aUSDC", 18); - aUSDC.mint(address(this), 100 ether); - gw.setTokenAddress("aUSDC", address(aUSDC)); - aUSDC.transfer(address(executor), 0.01 ether); - executor.executeWithToken( - bytes32("abcde"), - "polygon", - "0x1234", - abi.encodePacked( - address(vault), - abi.encodeWithSignature("deposit(address,uint256)", address(aUSDC), 0.01 ether) - ), - "aUSDC", - 0.01 ether - ); - } - function testCanPerformSameChainComplexSwap() public { ERC20 tokenA = new ERC20("Token A", "TOKA", 18); ERC20 tokenB = new ERC20("Token B", "TOKB", 18);