diff --git a/script/test/Arbitrum/DeployProxy.s.sol b/script/test/Arbitrum/DeployProxy.s.sol new file mode 100644 index 000000000..815bb824d --- /dev/null +++ b/script/test/Arbitrum/DeployProxy.s.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { AxelarProxy } from "src/AxelarProxy.sol"; + +import "forge-std/Script.sol"; +import { Math } from "src/utils/Math.sol"; + +/** + * @dev Run + * `source .env && forge script script/test/Arbitrum/DeployProxy.s.sol:DeployProxyScript --rpc-url $ARBITRUM_RPC_URL --private-key $PRIVATE_KEY —optimize —optimizer-runs 200 --verify --etherscan-api-key $ARBISCAN_KEY --slow --broadcast` + * @dev Optionally can change `--with-gas-price` to something more reasonable + */ +contract DeployProxyScript is Script { + address private devOwner = 0x552acA1343A6383aF32ce1B7c7B1b47959F7ad90; + + address private gateway = 0xe432150cce91c13a887f7D836923d5597adD8E31; + + string private sourceChain = "Polygon"; + string private sourceAddress = "0x552acA1343A6383aF32ce1B7c7B1b47959F7ad90"; + + function run() external { + vm.startBroadcast(); + + AxelarProxy proxy = new AxelarProxy(gateway); + + vm.stopBroadcast(); + } +} diff --git a/script/test/Polygon/SendMessageToArbitrum.s.sol b/script/test/Polygon/SendMessageToArbitrum.s.sol new file mode 100644 index 000000000..f9ee0f555 --- /dev/null +++ b/script/test/Polygon/SendMessageToArbitrum.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { ERC20 } from "src/base/Cellar.sol"; +import { AxelarProxy } from "src/AxelarProxy.sol"; +import { MockSommelier } from "src/mocks/MockSommelier.sol"; + +import "forge-std/Script.sol"; +import { Math } from "src/utils/Math.sol"; + +interface Gateway { + function callContract(string memory dest, string memory destAddress, bytes memory payload) external; +} + +/** + * @dev Run + * `source .env && forge script script/test/Polygon/SendMessageToArbitrum.s.sol:SendMessageToArbitrumScript --rpc-url $MATIC_RPC_URL --private-key $PRIVATE_KEY —optimize —optimizer-runs 200 --verify --etherscan-api-key $POLYGONSCAN_KEY --slow --broadcast` + * @dev Optionally can change `--with-gas-price` to something more reasonable + */ +contract SendMessageToArbitrumScript is Script { + address private devOwner = 0x552acA1343A6383aF32ce1B7c7B1b47959F7ad90; + + Gateway private gateway = Gateway(0x6f015F16De9fC8791b234eF68D486d2bF203FBA8); + + string private destChain = "arbitrum"; + string private destAddress = "0x2aF45D06C3d06af1E6B8Bc3f90c5a8DB0E5aa729"; + + address private usdcOnArb = 0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8; + + MockSommelier private mockSomm; + + function run() external { + vm.startBroadcast(); + + mockSomm = new MockSommelier(); + + mockSomm.sendMessage{ value: 10 ether }(destAddress, usdcOnArb, devOwner, 777); + + vm.stopBroadcast(); + } +} diff --git a/src/AxelarProxy.sol b/src/AxelarProxy.sol new file mode 100644 index 000000000..8338f026e --- /dev/null +++ b/src/AxelarProxy.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { AxelarExecutable } from "lib/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title Axelar Proxy + * @notice Allows for Cellars deployed on L2s to be controlled by the Sommelier Chain using Axelar messages. + * @dev This contract will be deployed on some L2, then to run a Cellar on that L2, + * deploy the Cellar, and make this contract the owner. + * @author crispymangoes + */ +contract AxelarProxy is AxelarExecutable { + using Address for address; + + event LogicCallEvent(address target, bytes callData); + + error AxelarProxy__WrongSource(); + error AxelarProxy__NoTokens(); + + bytes32 public constant SOMMELIER_CHAIN_HASH = keccak256(bytes("sommelier")); + + constructor(address gateway_) AxelarExecutable(gateway_) {} + + /** + * @notice Execution logic. + * @dev Verifies message is from Sommelier, otherwise reverts. + * @dev Verifies message is a valid Axelar message, otherwise reverts. + * See `AxelarExecutable.sol`. + */ + function _execute(string calldata sourceChain, string calldata, bytes calldata payload) internal override { + // Validate Source Chain + if (keccak256(bytes(sourceChain)) != SOMMELIER_CHAIN_HASH) revert AxelarProxy__WrongSource(); + + // Execute function call. + (address target, bytes memory callData) = abi.decode(payload, (address, bytes)); + target.functionCall(callData); + + emit LogicCallEvent(target, callData); + } + + /** + * @notice This contract is not setup to handle ERC20 tokens, so execution with token calls will revert. + */ + function _executeWithToken( + string calldata, + string calldata, + bytes calldata, + string calldata, + uint256 + ) internal pure override { + revert AxelarProxy__NoTokens(); + } +} diff --git a/src/mocks/MockSommelier.sol b/src/mocks/MockSommelier.sol new file mode 100644 index 000000000..09d750df2 --- /dev/null +++ b/src/mocks/MockSommelier.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.16; + +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; +import { ERC20 } from "src/base/Cellar.sol"; +import { IAxelarGateway } from "lib/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; +import { IAxelarGasService } from "lib/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol"; + +/** + * @notice This mock contract provides a method to send an Axelar message from one chain to another. + * It is called MockSommelier because it is mimicking what the Sommelier chain would do + * to send an Axelar message. + * @dev NOTE for actual Axelar messages from Sommelier, the Cosmos to EVM messaging logic will be used, not EVM to EVM. + */ +contract MockSommelier { + using Address for address; + + address constant ARB_USDC = 0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8; + IAxelarGateway private axelarGateway = IAxelarGateway(0x6f015F16De9fC8791b234eF68D486d2bF203FBA8); + IAxelarGasService private axelarGasService = IAxelarGasService(0x2d5d7d31F671F86C782533cc367F14109a082712); + string private destChain = "arbitrum"; + + function sendMessage(string memory target, address token, address spender, uint256 amount) external payable { + bytes memory payload = abi.encodeWithSelector(ERC20.approve.selector, spender, amount); + payload = abi.encode(token, payload); + + // Pay gas. + axelarGasService.payNativeGasForContractCall{ value: msg.value }( + msg.sender, + destChain, + target, + payload, + msg.sender + ); + // Send GMP. + axelarGateway.callContract(destChain, target, payload); + } +}