diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/AdminMultisigBase.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/AdminMultisigBase.sol new file mode 100644 index 0000000000..ee0b5189ee --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/AdminMultisigBase.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { EternalStorage } from './EternalStorage.sol'; + +/* + * Deprecated. See InterchainGovernance instead. + * AxelarGateway still inherits this to preserve storage layout used by EternalStorage when upgrading. + */ +contract AdminMultisigBase is EternalStorage { + error NotAdmin(); + error AlreadyVoted(); + error InvalidAdmins(); + error InvalidAdminThreshold(); + error DuplicateAdmin(address admin); + + // AUDIT: slot names should be prefixed with some standard string + bytes32 internal constant KEY_ADMIN_EPOCH = keccak256('admin-epoch'); + + bytes32 internal constant PREFIX_ADMIN = keccak256('admin'); + bytes32 internal constant PREFIX_ADMIN_COUNT = keccak256('admin-count'); + bytes32 internal constant PREFIX_ADMIN_THRESHOLD = keccak256('admin-threshold'); + bytes32 internal constant PREFIX_ADMIN_VOTE_COUNTS = keccak256('admin-vote-counts'); + bytes32 internal constant PREFIX_ADMIN_VOTED = keccak256('admin-voted'); + bytes32 internal constant PREFIX_IS_ADMIN = keccak256('is-admin'); + + // NOTE: Given the early void return, this modifier should be used with care on functions that return data. + modifier onlyAdmin() { + uint256 adminEpoch = _adminEpoch(); + + if (!_isAdmin(adminEpoch, msg.sender)) revert NotAdmin(); + + bytes32 topic = keccak256(msg.data); + + // Check that admin has not voted, then record that they have voted. + if (_hasVoted(adminEpoch, topic, msg.sender)) revert AlreadyVoted(); + + _setHasVoted(adminEpoch, topic, msg.sender, true); + + // Determine the new vote count and update it. + uint256 adminVoteCount = _getVoteCount(adminEpoch, topic) + uint256(1); + _setVoteCount(adminEpoch, topic, adminVoteCount); + + // Do not proceed with operation execution if insufficient votes. + if (adminVoteCount < _getAdminThreshold(adminEpoch)) return; + + _; + + // Clear vote count and voted booleans. + _setVoteCount(adminEpoch, topic, uint256(0)); + + uint256 adminCount = _getAdminCount(adminEpoch); + + for (uint256 i; i < adminCount; ++i) { + _setHasVoted(adminEpoch, topic, _getAdmin(adminEpoch, i), false); + } + } + + /********************\ + |* Pure Key Getters *| + \********************/ + + function _getAdminKey(uint256 adminEpoch, uint256 index) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_ADMIN, adminEpoch, index)); + } + + function _getAdminCountKey(uint256 adminEpoch) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_ADMIN_COUNT, adminEpoch)); + } + + function _getAdminThresholdKey(uint256 adminEpoch) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_ADMIN_THRESHOLD, adminEpoch)); + } + + function _getAdminVoteCountsKey(uint256 adminEpoch, bytes32 topic) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_ADMIN_VOTE_COUNTS, adminEpoch, topic)); + } + + function _getAdminVotedKey( + uint256 adminEpoch, + bytes32 topic, + address account + ) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_ADMIN_VOTED, adminEpoch, topic, account)); + } + + function _getIsAdminKey(uint256 adminEpoch, address account) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_IS_ADMIN, adminEpoch, account)); + } + + /***********\ + |* Getters *| + \***********/ + + function _adminEpoch() internal view returns (uint256) { + return getUint(KEY_ADMIN_EPOCH); + } + + function _getAdmin(uint256 adminEpoch, uint256 index) internal view returns (address) { + return getAddress(_getAdminKey(adminEpoch, index)); + } + + function _getAdminCount(uint256 adminEpoch) internal view returns (uint256) { + return getUint(_getAdminCountKey(adminEpoch)); + } + + function _getAdminThreshold(uint256 adminEpoch) internal view returns (uint256) { + return getUint(_getAdminThresholdKey(adminEpoch)); + } + + function _getVoteCount(uint256 adminEpoch, bytes32 topic) internal view returns (uint256) { + return getUint(_getAdminVoteCountsKey(adminEpoch, topic)); + } + + function _hasVoted( + uint256 adminEpoch, + bytes32 topic, + address account + ) internal view returns (bool) { + return getBool(_getAdminVotedKey(adminEpoch, topic, account)); + } + + function _isAdmin(uint256 adminEpoch, address account) internal view returns (bool) { + return getBool(_getIsAdminKey(adminEpoch, account)); + } + + /***********\ + |* Setters *| + \***********/ + + function _setAdminEpoch(uint256 adminEpoch) internal { + _setUint(KEY_ADMIN_EPOCH, adminEpoch); + } + + function _setAdmin( + uint256 adminEpoch, + uint256 index, + address account + ) internal { + _setAddress(_getAdminKey(adminEpoch, index), account); + } + + function _setAdminCount(uint256 adminEpoch, uint256 adminCount) internal { + _setUint(_getAdminCountKey(adminEpoch), adminCount); + } + + function _setAdmins( + uint256 adminEpoch, + address[] memory accounts, + uint256 threshold + ) internal { + uint256 adminLength = accounts.length; + + if (adminLength < threshold) revert InvalidAdmins(); + + if (threshold == uint256(0)) revert InvalidAdminThreshold(); + + _setAdminThreshold(adminEpoch, threshold); + _setAdminCount(adminEpoch, adminLength); + + for (uint256 i; i < adminLength; ++i) { + address account = accounts[i]; + + // Check that the account wasn't already set as an admin for this epoch. + if (_isAdmin(adminEpoch, account)) revert DuplicateAdmin(account); + + if (account == address(0)) revert InvalidAdmins(); + + // Set this account as the i-th admin in this epoch (needed to we can clear topic votes in `onlyAdmin`). + _setAdmin(adminEpoch, i, account); + _setIsAdmin(adminEpoch, account, true); + } + } + + function _setAdminThreshold(uint256 adminEpoch, uint256 adminThreshold) internal { + _setUint(_getAdminThresholdKey(adminEpoch), adminThreshold); + } + + function _setVoteCount( + uint256 adminEpoch, + bytes32 topic, + uint256 voteCount + ) internal { + _setUint(_getAdminVoteCountsKey(adminEpoch, topic), voteCount); + } + + function _setHasVoted( + uint256 adminEpoch, + bytes32 topic, + address account, + bool voted + ) internal { + _setBool(_getAdminVotedKey(adminEpoch, topic, account), voted); + } + + function _setIsAdmin( + uint256 adminEpoch, + address account, + bool isAdmin + ) internal { + _setBool(_getIsAdminKey(adminEpoch, account), isAdmin); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/AxelarGateway.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/AxelarGateway.sol new file mode 100644 index 0000000000..83d09cf039 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/AxelarGateway.sol @@ -0,0 +1,684 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import { SafeTokenCall, SafeTokenTransfer, SafeTokenTransferFrom } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol'; +import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; +import { IAxelarGateway } from './interfaces/IAxelarGateway.sol'; +import { IGovernable } from './interfaces/IGovernable.sol'; +import { IAxelarAuth } from './interfaces/IAxelarAuth.sol'; +import { IBurnableMintableCappedERC20 } from './interfaces/IBurnableMintableCappedERC20.sol'; +import { ITokenDeployer } from './interfaces/ITokenDeployer.sol'; + +import { ECDSA } from './ECDSA.sol'; +import { DepositHandler } from './DepositHandler.sol'; +import { AdminMultisigBase } from './AdminMultisigBase.sol'; + +contract AxelarGateway is IAxelarGateway, IGovernable, AdminMultisigBase { + using SafeTokenCall for IERC20; + using SafeTokenTransfer for IERC20; + using SafeTokenTransferFrom for IERC20; + + enum TokenType { + InternalBurnable, + InternalBurnableFrom, + External + } + + /// @dev Removed slots; Should avoid re-using + // bytes32 internal constant KEY_ALL_TOKENS_FROZEN = keccak256('all-tokens-frozen'); + // bytes32 internal constant PREFIX_TOKEN_FROZEN = keccak256('token-frozen'); + + /// @dev Storage slot with the address of the current implementation. `keccak256('eip1967.proxy.implementation') - 1`. + bytes32 internal constant KEY_IMPLEMENTATION = bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc); + + /// @dev Storage slot with the address of the current governance. `keccak256('governance') - 1`. + bytes32 internal constant KEY_GOVERNANCE = bytes32(0xabea6fd3db56a6e6d0242111b43ebb13d1c42709651c032c7894962023a1f909); + + /// @dev Storage slot with the address of the current governance. `keccak256('mint-limiter') - 1`. + bytes32 internal constant KEY_MINT_LIMITER = bytes32(0x627f0c11732837b3240a2de89c0b6343512886dd50978b99c76a68c6416a4d92); + + // AUDIT: slot names should be prefixed with some standard string + bytes32 internal constant PREFIX_COMMAND_EXECUTED = keccak256('command-executed'); + bytes32 internal constant PREFIX_TOKEN_ADDRESS = keccak256('token-address'); + bytes32 internal constant PREFIX_TOKEN_TYPE = keccak256('token-type'); + bytes32 internal constant PREFIX_CONTRACT_CALL_APPROVED = keccak256('contract-call-approved'); + bytes32 internal constant PREFIX_CONTRACT_CALL_APPROVED_WITH_MINT = keccak256('contract-call-approved-with-mint'); + bytes32 internal constant PREFIX_TOKEN_MINT_LIMIT = keccak256('token-mint-limit'); + bytes32 internal constant PREFIX_TOKEN_MINT_AMOUNT = keccak256('token-mint-amount'); + + bytes32 internal constant SELECTOR_BURN_TOKEN = keccak256('burnToken'); + bytes32 internal constant SELECTOR_DEPLOY_TOKEN = keccak256('deployToken'); + bytes32 internal constant SELECTOR_MINT_TOKEN = keccak256('mintToken'); + bytes32 internal constant SELECTOR_APPROVE_CONTRACT_CALL = keccak256('approveContractCall'); + bytes32 internal constant SELECTOR_APPROVE_CONTRACT_CALL_WITH_MINT = keccak256('approveContractCallWithMint'); + bytes32 internal constant SELECTOR_TRANSFER_OPERATORSHIP = keccak256('transferOperatorship'); + + // solhint-disable-next-line var-name-mixedcase + address internal immutable AUTH_MODULE; + // solhint-disable-next-line var-name-mixedcase + address internal immutable TOKEN_DEPLOYER_IMPLEMENTATION; + + constructor(address authModule_, address tokenDeployerImplementation_) { + if (authModule_.code.length == 0) revert InvalidAuthModule(); + if (tokenDeployerImplementation_.code.length == 0) revert InvalidTokenDeployer(); + + AUTH_MODULE = authModule_; + TOKEN_DEPLOYER_IMPLEMENTATION = tokenDeployerImplementation_; + } + + modifier onlySelf() { + if (msg.sender != address(this)) revert NotSelf(); + + _; + } + + modifier onlyGovernance() { + if (msg.sender != getAddress(KEY_GOVERNANCE)) revert NotGovernance(); + + _; + } + + /* + * @dev Reverts with an error if the sender is not the mint limiter or governance. + */ + modifier onlyMintLimiter() { + if (msg.sender != getAddress(KEY_MINT_LIMITER) && msg.sender != getAddress(KEY_GOVERNANCE)) revert NotMintLimiter(); + + _; + } + + /******************\ + |* Public Methods *| + \******************/ + + function sendToken( + string calldata destinationChain, + string calldata destinationAddress, + string calldata symbol, + uint256 amount + ) external { + _burnTokenFrom(msg.sender, symbol, amount); + emit TokenSent(msg.sender, destinationChain, destinationAddress, symbol, amount); + } + + function callContract( + string calldata destinationChain, + string calldata destinationContractAddress, + bytes calldata payload + ) external { + emit ContractCall(msg.sender, destinationChain, destinationContractAddress, keccak256(payload), payload); + } + + function callContractWithToken( + string calldata destinationChain, + string calldata destinationContractAddress, + bytes calldata payload, + string calldata symbol, + uint256 amount + ) external { + _burnTokenFrom(msg.sender, symbol, amount); + emit ContractCallWithToken(msg.sender, destinationChain, destinationContractAddress, keccak256(payload), payload, symbol, amount); + } + + function isContractCallApproved( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + address contractAddress, + bytes32 payloadHash + ) external view override returns (bool) { + return getBool(_getIsContractCallApprovedKey(commandId, sourceChain, sourceAddress, contractAddress, payloadHash)); + } + + function isContractCallAndMintApproved( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + address contractAddress, + bytes32 payloadHash, + string calldata symbol, + uint256 amount + ) external view override returns (bool) { + return + getBool( + _getIsContractCallApprovedWithMintKey(commandId, sourceChain, sourceAddress, contractAddress, payloadHash, symbol, amount) + ); + } + + function validateContractCall( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes32 payloadHash + ) external override returns (bool valid) { + bytes32 key = _getIsContractCallApprovedKey(commandId, sourceChain, sourceAddress, msg.sender, payloadHash); + valid = getBool(key); + if (valid) _setBool(key, false); + } + + function validateContractCallAndMint( + bytes32 commandId, + string calldata sourceChain, + string calldata sourceAddress, + bytes32 payloadHash, + string calldata symbol, + uint256 amount + ) external override returns (bool valid) { + bytes32 key = _getIsContractCallApprovedWithMintKey(commandId, sourceChain, sourceAddress, msg.sender, payloadHash, symbol, amount); + valid = getBool(key); + if (valid) { + // Prevent re-entrance + _setBool(key, false); + _mintToken(symbol, msg.sender, amount); + } + } + + /***********\ + |* Getters *| + \***********/ + + function authModule() public view override returns (address) { + return AUTH_MODULE; + } + + function governance() public view override returns (address) { + return getAddress(KEY_GOVERNANCE); + } + + function mintLimiter() public view override returns (address) { + return getAddress(KEY_MINT_LIMITER); + } + + function tokenDeployer() public view returns (address) { + return TOKEN_DEPLOYER_IMPLEMENTATION; + } + + function tokenMintLimit(string memory symbol) public view override returns (uint256) { + return getUint(_getTokenMintLimitKey(symbol)); + } + + function tokenMintAmount(string memory symbol) public view override returns (uint256) { + return getUint(_getTokenMintAmountKey(symbol, block.timestamp / 6 hours)); + } + + /// @dev This function is kept around to keep things working for internal + /// tokens that were deployed before the token freeze functionality was removed + function allTokensFrozen() external pure override returns (bool) { + return false; + } + + /// @dev Deprecated. + function adminEpoch() external pure override returns (uint256) { + return 0; + } + + /// @dev Deprecated. + function adminThreshold(uint256) external pure override returns (uint256) { + return 0; + } + + /// @dev Deprecated. + function admins(uint256) external pure override returns (address[] memory) { + return new address[](0); + } + + function implementation() public view override returns (address) { + return getAddress(KEY_IMPLEMENTATION); + } + + function tokenAddresses(string memory symbol) public view override returns (address) { + return getAddress(_getTokenAddressKey(symbol)); + } + + /// @dev This function is kept around to keep things working for internal + /// tokens that were deployed before the token freeze functionality was removed + function tokenFrozen(string memory) external pure override returns (bool) { + return false; + } + + function isCommandExecuted(bytes32 commandId) public view override returns (bool) { + return getBool(_getIsCommandExecutedKey(commandId)); + } + + /************************\ + |* Governance Functions *| + \************************/ + + function transferGovernance(address newGovernance) external override onlyGovernance { + if (newGovernance == address(0)) revert InvalidGovernance(); + + _transferGovernance(newGovernance); + } + + function transferMintLimiter(address newMintLimiter) external override onlyMintLimiter { + if (newMintLimiter == address(0)) revert InvalidMintLimiter(); + + _transferMintLimiter(newMintLimiter); + } + + function setTokenMintLimits(string[] calldata symbols, uint256[] calldata limits) external override onlyMintLimiter { + uint256 length = symbols.length; + if (length != limits.length) revert InvalidSetMintLimitsParams(); + + for (uint256 i; i < length; ++i) { + string memory symbol = symbols[i]; + uint256 limit = limits[i]; + + if (tokenAddresses(symbol) == address(0)) revert TokenDoesNotExist(symbol); + + _setTokenMintLimit(symbol, limit); + } + } + + function upgrade( + address newImplementation, + bytes32 newImplementationCodeHash, + bytes calldata setupParams + ) external override onlyGovernance { + if (newImplementationCodeHash != newImplementation.codehash) revert InvalidCodeHash(); + + emit Upgraded(newImplementation); + + // AUDIT: If `newImplementation.setup` performs `selfdestruct`, it will result in the loss of _this_ implementation (thereby losing the gateway) + // if `upgrade` is entered within the context of _this_ implementation itself. + if (setupParams.length != 0) { + (bool success, ) = newImplementation.delegatecall(abi.encodeWithSelector(IAxelarGateway.setup.selector, setupParams)); + + if (!success) revert SetupFailed(); + } + + _setImplementation(newImplementation); + } + + /**********************\ + |* External Functions *| + \**********************/ + + /// @dev Not publicly accessible as overshadowed in the proxy + function setup(bytes calldata params) external override { + // Prevent setup from being called on a non-proxy (the implementation). + if (implementation() == address(0)) revert NotProxy(); + + (address governance_, address mintLimiter_, bytes memory newOperatorsData) = abi.decode(params, (address, address, bytes)); + + if (governance_ != address(0)) _transferGovernance(governance_); + if (mintLimiter_ != address(0)) _transferMintLimiter(mintLimiter_); + + if (newOperatorsData.length != 0) { + IAxelarAuth(AUTH_MODULE).transferOperatorship(newOperatorsData); + + emit OperatorshipTransferred(newOperatorsData); + } + } + + function execute(bytes calldata input) external override { + (bytes memory data, bytes memory proof) = abi.decode(input, (bytes, bytes)); + + bytes32 messageHash = ECDSA.toEthSignedMessageHash(keccak256(data)); + + // returns true for current operators + bool allowOperatorshipTransfer = IAxelarAuth(AUTH_MODULE).validateProof(messageHash, proof); + + uint256 chainId; + bytes32[] memory commandIds; + string[] memory commands; + bytes[] memory params; + + (chainId, commandIds, commands, params) = abi.decode(data, (uint256, bytes32[], string[], bytes[])); + + if (chainId != block.chainid) revert InvalidChainId(); + + uint256 commandsLength = commandIds.length; + + if (commandsLength != commands.length || commandsLength != params.length) revert InvalidCommands(); + + for (uint256 i; i < commandsLength; ++i) { + bytes32 commandId = commandIds[i]; + + if (isCommandExecuted(commandId)) continue; /* Ignore if duplicate commandId received */ + + bytes4 commandSelector; + bytes32 commandHash = keccak256(abi.encodePacked(commands[i])); + + if (commandHash == SELECTOR_DEPLOY_TOKEN) { + commandSelector = AxelarGateway.deployToken.selector; + } else if (commandHash == SELECTOR_MINT_TOKEN) { + commandSelector = AxelarGateway.mintToken.selector; + } else if (commandHash == SELECTOR_APPROVE_CONTRACT_CALL) { + commandSelector = AxelarGateway.approveContractCall.selector; + } else if (commandHash == SELECTOR_APPROVE_CONTRACT_CALL_WITH_MINT) { + commandSelector = AxelarGateway.approveContractCallWithMint.selector; + } else if (commandHash == SELECTOR_BURN_TOKEN) { + commandSelector = AxelarGateway.burnToken.selector; + } else if (commandHash == SELECTOR_TRANSFER_OPERATORSHIP) { + if (!allowOperatorshipTransfer) continue; + + allowOperatorshipTransfer = false; + commandSelector = AxelarGateway.transferOperatorship.selector; + } else { + continue; /* Ignore if unknown command received */ + } + + // Prevent a re-entrancy from executing this command before it can be marked as successful. + _setCommandExecuted(commandId, true); + + (bool success, ) = address(this).call(abi.encodeWithSelector(commandSelector, params[i], commandId)); + + if (success) emit Executed(commandId); + else _setCommandExecuted(commandId, false); + } + } + + /******************\ + |* Self Functions *| + \******************/ + + function deployToken(bytes calldata params, bytes32) external onlySelf { + (string memory name, string memory symbol, uint8 decimals, uint256 cap, address tokenAddress, uint256 mintLimit) = abi.decode( + params, + (string, string, uint8, uint256, address, uint256) + ); + + // Ensure that this symbol has not been taken. + if (tokenAddresses(symbol) != address(0)) revert TokenAlreadyExists(symbol); + + if (tokenAddress == address(0)) { + // If token address is no specified, it indicates a request to deploy one. + bytes32 salt = keccak256(abi.encodePacked(symbol)); + + (bool success, bytes memory data) = TOKEN_DEPLOYER_IMPLEMENTATION.delegatecall( + abi.encodeWithSelector(ITokenDeployer.deployToken.selector, name, symbol, decimals, cap, salt) + ); + + if (!success) revert TokenDeployFailed(symbol); + + tokenAddress = abi.decode(data, (address)); + + _setTokenType(symbol, TokenType.InternalBurnableFrom); + } else { + // If token address is specified, ensure that there is a contact at the specified address. + if (tokenAddress.code.length == uint256(0)) revert TokenContractDoesNotExist(tokenAddress); + + // Mark that this symbol is an external token, which is needed to differentiate between operations on mint and burn. + _setTokenType(symbol, TokenType.External); + } + + _setTokenAddress(symbol, tokenAddress); + _setTokenMintLimit(symbol, mintLimit); + + emit TokenDeployed(symbol, tokenAddress); + } + + function mintToken(bytes calldata params, bytes32) external onlySelf { + (string memory symbol, address account, uint256 amount) = abi.decode(params, (string, address, uint256)); + + _mintToken(symbol, account, amount); + } + + function burnToken(bytes calldata params, bytes32) external onlySelf { + (string memory symbol, bytes32 salt) = abi.decode(params, (string, bytes32)); + + address tokenAddress = tokenAddresses(symbol); + + if (tokenAddress == address(0)) revert TokenDoesNotExist(symbol); + + if (_getTokenType(symbol) == TokenType.External) { + address depositHandlerAddress = _getCreate2Address(salt, keccak256(abi.encodePacked(type(DepositHandler).creationCode))); + + if (_hasCode(depositHandlerAddress)) return; + + DepositHandler depositHandler = new DepositHandler{ salt: salt }(); + + (bool success, bytes memory returnData) = depositHandler.execute( + tokenAddress, + abi.encodeWithSelector(IERC20.transfer.selector, address(this), IERC20(tokenAddress).balanceOf(address(depositHandler))) + ); + + if (!success || (returnData.length != uint256(0) && !abi.decode(returnData, (bool)))) revert BurnFailed(symbol); + + // NOTE: `depositHandler` must always be destroyed in the same runtime context that it is deployed. + depositHandler.destroy(address(this)); + } else { + IBurnableMintableCappedERC20(tokenAddress).burn(salt); + } + } + + function approveContractCall(bytes calldata params, bytes32 commandId) external onlySelf { + ( + string memory sourceChain, + string memory sourceAddress, + address contractAddress, + bytes32 payloadHash, + bytes32 sourceTxHash, + uint256 sourceEventIndex + ) = abi.decode(params, (string, string, address, bytes32, bytes32, uint256)); + + _setContractCallApproved(commandId, sourceChain, sourceAddress, contractAddress, payloadHash); + emit ContractCallApproved(commandId, sourceChain, sourceAddress, contractAddress, payloadHash, sourceTxHash, sourceEventIndex); + } + + function approveContractCallWithMint(bytes calldata params, bytes32 commandId) external onlySelf { + ( + string memory sourceChain, + string memory sourceAddress, + address contractAddress, + bytes32 payloadHash, + string memory symbol, + uint256 amount, + bytes32 sourceTxHash, + uint256 sourceEventIndex + ) = abi.decode(params, (string, string, address, bytes32, string, uint256, bytes32, uint256)); + + _setContractCallApprovedWithMint(commandId, sourceChain, sourceAddress, contractAddress, payloadHash, symbol, amount); + emit ContractCallApprovedWithMint( + commandId, + sourceChain, + sourceAddress, + contractAddress, + payloadHash, + symbol, + amount, + sourceTxHash, + sourceEventIndex + ); + } + + function transferOperatorship(bytes calldata newOperatorsData, bytes32) external onlySelf { + IAxelarAuth(AUTH_MODULE).transferOperatorship(newOperatorsData); + + emit OperatorshipTransferred(newOperatorsData); + } + + /********************\ + |* Internal Methods *| + \********************/ + + function _hasCode(address addr) internal view returns (bool) { + bytes32 codehash = addr.codehash; + + // https://eips.ethereum.org/EIPS/eip-1052 + return codehash != bytes32(0) && codehash != 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + } + + function _mintToken( + string memory symbol, + address account, + uint256 amount + ) internal { + address tokenAddress = tokenAddresses(symbol); + + if (tokenAddress == address(0)) revert TokenDoesNotExist(symbol); + + _setTokenMintAmount(symbol, tokenMintAmount(symbol) + amount); + + if (_getTokenType(symbol) == TokenType.External) { + IERC20(tokenAddress).safeTransfer(account, amount); + } else { + IBurnableMintableCappedERC20(tokenAddress).mint(account, amount); + } + } + + function _burnTokenFrom( + address sender, + string memory symbol, + uint256 amount + ) internal { + address tokenAddress = tokenAddresses(symbol); + + if (tokenAddress == address(0)) revert TokenDoesNotExist(symbol); + if (amount == 0) revert InvalidAmount(); + + TokenType tokenType = _getTokenType(symbol); + + if (tokenType == TokenType.External) { + IERC20(tokenAddress).safeTransferFrom(sender, address(this), amount); + } else if (tokenType == TokenType.InternalBurnableFrom) { + IERC20(tokenAddress).safeCall(abi.encodeWithSelector(IBurnableMintableCappedERC20.burnFrom.selector, sender, amount)); + } else { + IERC20(tokenAddress).safeTransferFrom(sender, IBurnableMintableCappedERC20(tokenAddress).depositAddress(bytes32(0)), amount); + IBurnableMintableCappedERC20(tokenAddress).burn(bytes32(0)); + } + } + + /********************\ + |* Pure Key Getters *| + \********************/ + + function _getTokenMintLimitKey(string memory symbol) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_TOKEN_MINT_LIMIT, symbol)); + } + + function _getTokenMintAmountKey(string memory symbol, uint256 day) internal pure returns (bytes32) { + // abi.encode to securely hash dynamic-length symbol data followed by day + return keccak256(abi.encode(PREFIX_TOKEN_MINT_AMOUNT, symbol, day)); + } + + function _getTokenTypeKey(string memory symbol) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_TOKEN_TYPE, symbol)); + } + + function _getTokenAddressKey(string memory symbol) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_TOKEN_ADDRESS, symbol)); + } + + function _getIsCommandExecutedKey(bytes32 commandId) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(PREFIX_COMMAND_EXECUTED, commandId)); + } + + function _getIsContractCallApprovedKey( + bytes32 commandId, + string memory sourceChain, + string memory sourceAddress, + address contractAddress, + bytes32 payloadHash + ) internal pure returns (bytes32) { + return keccak256(abi.encode(PREFIX_CONTRACT_CALL_APPROVED, commandId, sourceChain, sourceAddress, contractAddress, payloadHash)); + } + + function _getIsContractCallApprovedWithMintKey( + bytes32 commandId, + string memory sourceChain, + string memory sourceAddress, + address contractAddress, + bytes32 payloadHash, + string memory symbol, + uint256 amount + ) internal pure returns (bytes32) { + return + keccak256( + abi.encode( + PREFIX_CONTRACT_CALL_APPROVED_WITH_MINT, + commandId, + sourceChain, + sourceAddress, + contractAddress, + payloadHash, + symbol, + amount + ) + ); + } + + /********************\ + |* Internal Getters *| + \********************/ + + function _getCreate2Address(bytes32 salt, bytes32 codeHash) internal view returns (address) { + return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, codeHash))))); + } + + function _getTokenType(string memory symbol) internal view returns (TokenType) { + return TokenType(getUint(_getTokenTypeKey(symbol))); + } + + /********************\ + |* Internal Setters *| + \********************/ + + function _setTokenMintLimit(string memory symbol, uint256 limit) internal { + _setUint(_getTokenMintLimitKey(symbol), limit); + + emit TokenMintLimitUpdated(symbol, limit); + } + + function _setTokenMintAmount(string memory symbol, uint256 amount) internal { + uint256 limit = tokenMintLimit(symbol); + if (limit > 0 && amount > limit) revert ExceedMintLimit(symbol); + + _setUint(_getTokenMintAmountKey(symbol, block.timestamp / 6 hours), amount); + } + + function _setTokenType(string memory symbol, TokenType tokenType) internal { + _setUint(_getTokenTypeKey(symbol), uint256(tokenType)); + } + + function _setTokenAddress(string memory symbol, address tokenAddress) internal { + _setAddress(_getTokenAddressKey(symbol), tokenAddress); + } + + function _setCommandExecuted(bytes32 commandId, bool executed) internal { + _setBool(_getIsCommandExecutedKey(commandId), executed); + } + + function _setContractCallApproved( + bytes32 commandId, + string memory sourceChain, + string memory sourceAddress, + address contractAddress, + bytes32 payloadHash + ) internal { + _setBool(_getIsContractCallApprovedKey(commandId, sourceChain, sourceAddress, contractAddress, payloadHash), true); + } + + function _setContractCallApprovedWithMint( + bytes32 commandId, + string memory sourceChain, + string memory sourceAddress, + address contractAddress, + bytes32 payloadHash, + string memory symbol, + uint256 amount + ) internal { + _setBool( + _getIsContractCallApprovedWithMintKey(commandId, sourceChain, sourceAddress, contractAddress, payloadHash, symbol, amount), + true + ); + } + + function _setImplementation(address newImplementation) internal { + _setAddress(KEY_IMPLEMENTATION, newImplementation); + } + + function _transferGovernance(address newGovernance) internal { + emit GovernanceTransferred(getAddress(KEY_GOVERNANCE), newGovernance); + + _setAddress(KEY_GOVERNANCE, newGovernance); + } + + function _transferMintLimiter(address newMintLimiter) internal { + emit MintLimiterTransferred(getAddress(KEY_MINT_LIMITER), newMintLimiter); + + _setAddress(KEY_MINT_LIMITER, newMintLimiter); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/AxelarGatewayProxy.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/AxelarGatewayProxy.sol new file mode 100644 index 0000000000..461b86c4b3 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/AxelarGatewayProxy.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { IAxelarGateway } from './interfaces/IAxelarGateway.sol'; + +import { EternalStorage } from './EternalStorage.sol'; + +contract AxelarGatewayProxy is EternalStorage { + error InvalidImplementation(); + error SetupFailed(); + error NativeCurrencyNotAccepted(); + + /// @dev Storage slot with the address of the current factory. `keccak256('eip1967.proxy.implementation') - 1`. + bytes32 internal constant KEY_IMPLEMENTATION = bytes32(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc); + + constructor(address gatewayImplementation, bytes memory params) { + _setAddress(KEY_IMPLEMENTATION, gatewayImplementation); + + if (gatewayImplementation.code.length == 0) revert InvalidImplementation(); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = gatewayImplementation.delegatecall(abi.encodeWithSelector(IAxelarGateway.setup.selector, params)); + + if (!success) revert SetupFailed(); + } + + // solhint-disable-next-line no-empty-blocks + function setup(bytes calldata params) external {} + + // solhint-disable-next-line no-complex-fallback + fallback() external payable { + address implementation = getAddress(KEY_IMPLEMENTATION); + + // solhint-disable-next-line no-inline-assembly + assembly { + calldatacopy(0, 0, calldatasize()) + + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + returndatacopy(0, 0, returndatasize()) + + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + + receive() external payable { + revert NativeCurrencyNotAccepted(); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/BurnableMintableCappedERC20.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/BurnableMintableCappedERC20.sol new file mode 100644 index 0000000000..384f2b0332 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/BurnableMintableCappedERC20.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { IAxelarGateway } from './interfaces/IAxelarGateway.sol'; +import { IBurnableMintableCappedERC20 } from './interfaces/IBurnableMintableCappedERC20.sol'; + +import { MintableCappedERC20 } from './MintableCappedERC20.sol'; +import { DepositHandler } from './DepositHandler.sol'; + +contract BurnableMintableCappedERC20 is IBurnableMintableCappedERC20, MintableCappedERC20 { + constructor( + string memory name, + string memory symbol, + uint8 decimals, + uint256 capacity + ) MintableCappedERC20(name, symbol, decimals, capacity) {} + + function depositAddress(bytes32 salt) public view returns (address) { + /* Convert a hash which is bytes32 to an address which is 20-byte long + according to https://docs.soliditylang.org/en/v0.8.1/control-structures.html?highlight=create2#salted-contract-creations-create2 */ + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked(bytes1(0xff), owner, salt, keccak256(abi.encodePacked(type(DepositHandler).creationCode))) + ) + ) + ) + ); + } + + function burn(bytes32 salt) external onlyOwner { + address account = depositAddress(salt); + _burn(account, balanceOf[account]); + } + + function burnFrom(address account, uint256 amount) external onlyOwner { + uint256 _allowance = allowance[account][msg.sender]; + if (_allowance != type(uint256).max) { + _approve(account, msg.sender, _allowance - amount); + } + _burn(account, amount); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/DepositHandler.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/DepositHandler.sol new file mode 100644 index 0000000000..b93efc9b83 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/DepositHandler.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +contract DepositHandler { + error IsLocked(); + error NotContract(); + + uint256 internal constant IS_NOT_LOCKED = uint256(1); + uint256 internal constant IS_LOCKED = uint256(2); + + uint256 internal _lockedStatus = IS_NOT_LOCKED; + + modifier noReenter() { + if (_lockedStatus == IS_LOCKED) revert IsLocked(); + + _lockedStatus = IS_LOCKED; + _; + _lockedStatus = IS_NOT_LOCKED; + } + + function execute(address callee, bytes calldata data) external noReenter returns (bool success, bytes memory returnData) { + if (callee.code.length == 0) revert NotContract(); + (success, returnData) = callee.call(data); + } + + // NOTE: The gateway should always destroy the `DepositHandler` in the same runtime context that deploys it. + function destroy(address etherDestination) external noReenter { + selfdestruct(payable(etherDestination)); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/ECDSA.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/ECDSA.sol new file mode 100644 index 0000000000..7b7cca8900 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/ECDSA.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + error InvalidSignatureLength(); + error InvalidS(); + error InvalidV(); + error InvalidSignature(); + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address signer) { + // Check the signature length + if (signature.length != 65) revert InvalidSignatureLength(); + + // Divide the signature in r, s and v variables + bytes32 r; + bytes32 s; + uint8 v; + + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + // solhint-disable-next-line no-inline-assembly + assembly { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); + + if (v != 27 && v != 28) revert InvalidV(); + + // If the signature is valid (and not malleable), return the signer address + if ((signer = ecrecover(hash, v, r, s)) == address(0)) revert InvalidSignature(); + } + + /** + * @dev Returns an Ethereum Signed Message, created from a `hash`. This + * replicates the behavior of the + * https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign[`eth_sign`] + * JSON-RPC method. + * + * See {recover}. + */ + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', hash)); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/ERC20.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/ERC20.sol new file mode 100644 index 0000000000..81deaf5a6b --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/ERC20.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { IERC20 } from './interfaces/IERC20.sol'; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is nonetheless conventional + * and does not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20 is IERC20 { + mapping(address => uint256) public override balanceOf; + + mapping(address => mapping(address => uint256)) public override allowance; + + uint256 public override totalSupply; + + string public name; + string public symbol; + + uint8 public immutable decimals; + + /** + * @dev Sets the values for {name}, {symbol}, and {decimals}. + */ + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) { + name = name_; + symbol = symbol_; + decimals = decimals_; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) external virtual override returns (bool) { + _transfer(msg.sender, recipient, amount); + return true; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) external virtual override returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * Requirements: + * + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external virtual override returns (bool) { + uint256 _allowance = allowance[sender][msg.sender]; + + if (_allowance != type(uint256).max) { + _approve(sender, msg.sender, _allowance - amount); + } + + _transfer(sender, recipient, amount); + + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) external virtual returns (bool) { + _approve(msg.sender, spender, allowance[msg.sender][spender] + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) external virtual returns (bool) { + _approve(msg.sender, spender, allowance[msg.sender][spender] - subtractedValue); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer( + address sender, + address recipient, + uint256 amount + ) internal virtual { + if (sender == address(0) || recipient == address(0)) revert InvalidAccount(); + + balanceOf[sender] -= amount; + balanceOf[recipient] += amount; + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + if (account == address(0)) revert InvalidAccount(); + + totalSupply += amount; + balanceOf[account] += amount; + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + if (account == address(0)) revert InvalidAccount(); + + balanceOf[account] -= amount; + totalSupply -= amount; + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + if (owner == address(0) || spender == address(0)) revert InvalidAccount(); + + allowance[owner][spender] = amount; + emit Approval(owner, spender, amount); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/ERC20Permit.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/ERC20Permit.sol new file mode 100644 index 0000000000..06789bc9a6 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/ERC20Permit.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { IERC20 } from './interfaces/IERC20.sol'; +import { IERC20Permit } from './interfaces/IERC20Permit.sol'; + +import { ERC20 } from './ERC20.sol'; + +abstract contract ERC20Permit is IERC20, IERC20Permit, ERC20 { + error PermitExpired(); + error InvalidS(); + error InvalidV(); + error InvalidSignature(); + + bytes32 public immutable DOMAIN_SEPARATOR; + + string private constant EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA = '\x19\x01'; + + // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') + bytes32 private constant DOMAIN_TYPE_SIGNATURE_HASH = bytes32(0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f); + + // keccak256('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)') + bytes32 private constant PERMIT_SIGNATURE_HASH = bytes32(0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9); + + mapping(address => uint256) public nonces; + + constructor(string memory name) { + DOMAIN_SEPARATOR = keccak256( + abi.encode(DOMAIN_TYPE_SIGNATURE_HASH, keccak256(bytes(name)), keccak256(bytes('1')), block.chainid, address(this)) + ); + } + + function permit( + address issuer, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external { + if (block.timestamp > deadline) revert PermitExpired(); + + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) revert InvalidS(); + + if (v != 27 && v != 28) revert InvalidV(); + + bytes32 digest = keccak256( + abi.encodePacked( + EIP191_PREFIX_FOR_EIP712_STRUCTURED_DATA, + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_SIGNATURE_HASH, issuer, spender, value, nonces[issuer]++, deadline)) + ) + ); + + address recoveredAddress = ecrecover(digest, v, r, s); + + if (recoveredAddress != issuer) revert InvalidSignature(); + + // _approve will revert if issuer is address(0x0) + _approve(issuer, spender, value); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/EternalStorage.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/EternalStorage.sol new file mode 100644 index 0000000000..8dc5619c8a --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/EternalStorage.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +/** + * @title EternalStorage + * @dev This contract holds all the necessary state variables to carry out the storage of any contract. + */ +contract EternalStorage { + mapping(bytes32 => uint256) private _uintStorage; + mapping(bytes32 => string) private _stringStorage; + mapping(bytes32 => address) private _addressStorage; + mapping(bytes32 => bytes) private _bytesStorage; + mapping(bytes32 => bool) private _boolStorage; + mapping(bytes32 => int256) private _intStorage; + + // *** Getter Methods *** + function getUint(bytes32 key) public view returns (uint256) { + return _uintStorage[key]; + } + + function getString(bytes32 key) public view returns (string memory) { + return _stringStorage[key]; + } + + function getAddress(bytes32 key) public view returns (address) { + return _addressStorage[key]; + } + + function getBytes(bytes32 key) public view returns (bytes memory) { + return _bytesStorage[key]; + } + + function getBool(bytes32 key) public view returns (bool) { + return _boolStorage[key]; + } + + function getInt(bytes32 key) public view returns (int256) { + return _intStorage[key]; + } + + // *** Setter Methods *** + function _setUint(bytes32 key, uint256 value) internal { + _uintStorage[key] = value; + } + + function _setString(bytes32 key, string memory value) internal { + _stringStorage[key] = value; + } + + function _setAddress(bytes32 key, address value) internal { + _addressStorage[key] = value; + } + + function _setBytes(bytes32 key, bytes memory value) internal { + _bytesStorage[key] = value; + } + + function _setBool(bytes32 key, bool value) internal { + _boolStorage[key] = value; + } + + function _setInt(bytes32 key, int256 value) internal { + _intStorage[key] = value; + } + + // *** Delete Methods *** + function _deleteUint(bytes32 key) internal { + delete _uintStorage[key]; + } + + function _deleteString(bytes32 key) internal { + delete _stringStorage[key]; + } + + function _deleteAddress(bytes32 key) internal { + delete _addressStorage[key]; + } + + function _deleteBytes(bytes32 key) internal { + delete _bytesStorage[key]; + } + + function _deleteBool(bytes32 key) internal { + delete _boolStorage[key]; + } + + function _deleteInt(bytes32 key) internal { + delete _intStorage[key]; + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/MintableCappedERC20.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/MintableCappedERC20.sol new file mode 100644 index 0000000000..2c86ed50cf --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/MintableCappedERC20.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { IMintableCappedERC20 } from './interfaces/IMintableCappedERC20.sol'; + +import { ERC20 } from './ERC20.sol'; +import { ERC20Permit } from './ERC20Permit.sol'; +import { Ownable } from './Ownable.sol'; + +contract MintableCappedERC20 is IMintableCappedERC20, ERC20, ERC20Permit, Ownable { + uint256 public immutable cap; + + constructor( + string memory name, + string memory symbol, + uint8 decimals, + uint256 capacity + ) ERC20(name, symbol, decimals) ERC20Permit(name) Ownable() { + cap = capacity; + } + + function mint(address account, uint256 amount) external onlyOwner { + uint256 capacity = cap; + + _mint(account, amount); + + if (capacity == 0) return; + + if (totalSupply > capacity) revert CapExceeded(); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/Ownable.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/Ownable.sol new file mode 100644 index 0000000000..4739a1aa72 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/Ownable.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { IOwnable } from './interfaces/IOwnable.sol'; + +abstract contract Ownable is IOwnable { + address public owner; + + constructor() { + owner = msg.sender; + emit OwnershipTransferred(address(0), msg.sender); + } + + modifier onlyOwner() { + if (owner != msg.sender) revert NotOwner(); + + _; + } + + function transferOwnership(address newOwner) external virtual onlyOwner { + if (newOwner == address(0)) revert InvalidOwner(); + + emit OwnershipTransferred(owner, newOwner); + owner = newOwner; + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/TokenDeployer.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/TokenDeployer.sol new file mode 100644 index 0000000000..6f084f49c5 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/TokenDeployer.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { ITokenDeployer } from './interfaces/ITokenDeployer.sol'; + +import { BurnableMintableCappedERC20 } from './BurnableMintableCappedERC20.sol'; + +contract TokenDeployer is ITokenDeployer { + function deployToken( + string calldata name, + string calldata symbol, + uint8 decimals, + uint256 cap, + bytes32 salt + ) external returns (address tokenAddress) { + tokenAddress = address(new BurnableMintableCappedERC20{ salt: salt }(name, symbol, decimals, cap)); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/auth/AxelarAuthWeighted.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/auth/AxelarAuthWeighted.sol new file mode 100644 index 0000000000..f26340b75e --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/auth/AxelarAuthWeighted.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import { IAxelarAuthWeighted } from '../interfaces/IAxelarAuthWeighted.sol'; +import { ECDSA } from '../ECDSA.sol'; +import { Ownable } from '../Ownable.sol'; + +contract AxelarAuthWeighted is Ownable, IAxelarAuthWeighted { + uint256 public currentEpoch; + mapping(uint256 => bytes32) public hashForEpoch; + mapping(bytes32 => uint256) public epochForHash; + + uint256 internal constant OLD_KEY_RETENTION = 16; + + constructor(bytes[] memory recentOperators) { + uint256 length = recentOperators.length; + + for (uint256 i; i < length; ++i) { + _transferOperatorship(recentOperators[i]); + } + } + + /**************************\ + |* External Functionality *| + \**************************/ + + /// @dev This function takes messageHash and proof data and reverts if proof is invalid + /// @return True if provided operators are the current ones + function validateProof(bytes32 messageHash, bytes calldata proof) external view returns (bool) { + (address[] memory operators, uint256[] memory weights, uint256 threshold, bytes[] memory signatures) = abi.decode( + proof, + (address[], uint256[], uint256, bytes[]) + ); + + bytes32 operatorsHash = keccak256(abi.encode(operators, weights, threshold)); + uint256 operatorsEpoch = epochForHash[operatorsHash]; + uint256 epoch = currentEpoch; + + if (operatorsEpoch == 0 || epoch - operatorsEpoch >= OLD_KEY_RETENTION) revert InvalidOperators(); + + _validateSignatures(messageHash, operators, weights, threshold, signatures); + + return operatorsEpoch == epoch; + } + + /***********************\ + |* Owner Functionality *| + \***********************/ + + function transferOperatorship(bytes calldata params) external onlyOwner { + _transferOperatorship(params); + } + + /**************************\ + |* Internal Functionality *| + \**************************/ + + function _transferOperatorship(bytes memory params) internal { + (address[] memory newOperators, uint256[] memory newWeights, uint256 newThreshold) = abi.decode( + params, + (address[], uint256[], uint256) + ); + uint256 operatorsLength = newOperators.length; + uint256 weightsLength = newWeights.length; + + // operators must be sorted binary or alphabetically in lower case + if (operatorsLength == 0 || !_isSortedAscAndContainsNoDuplicate(newOperators)) revert InvalidOperators(); + + if (weightsLength != operatorsLength) revert InvalidWeights(); + + uint256 totalWeight; + for (uint256 i; i < weightsLength; ++i) { + totalWeight = totalWeight + newWeights[i]; + } + if (newThreshold == 0 || totalWeight < newThreshold) revert InvalidThreshold(); + + bytes32 newOperatorsHash = keccak256(params); + + if (epochForHash[newOperatorsHash] != 0) revert DuplicateOperators(); + + uint256 epoch = currentEpoch + 1; + currentEpoch = epoch; + hashForEpoch[epoch] = newOperatorsHash; + epochForHash[newOperatorsHash] = epoch; + + emit OperatorshipTransferred(newOperators, newWeights, newThreshold); + } + + function _validateSignatures( + bytes32 messageHash, + address[] memory operators, + uint256[] memory weights, + uint256 threshold, + bytes[] memory signatures + ) internal pure { + uint256 operatorsLength = operators.length; + uint256 signaturesLength = signatures.length; + uint256 operatorIndex; + uint256 weight; + // looking for signers within operators + // assuming that both operators and signatures are sorted + for (uint256 i; i < signaturesLength; ++i) { + address signer = ECDSA.recover(messageHash, signatures[i]); + // looping through remaining operators to find a match + for (; operatorIndex < operatorsLength && signer != operators[operatorIndex]; ++operatorIndex) {} + // checking if we are out of operators + if (operatorIndex == operatorsLength) revert MalformedSigners(); + // accumulating signatures weight + weight = weight + weights[operatorIndex]; + // weight needs to reach or surpass threshold + if (weight >= threshold) return; + // increasing operators index if match was found + ++operatorIndex; + } + // if weight sum below threshold + revert LowSignaturesWeight(); + } + + function _isSortedAscAndContainsNoDuplicate(address[] memory accounts) internal pure returns (bool) { + uint256 accountsLength = accounts.length; + address prevAccount = accounts[0]; + + if (prevAccount == address(0)) return false; + + for (uint256 i = 1; i < accountsLength; ++i) { + address currAccount = accounts[i]; + + if (prevAccount >= currAccount) { + return false; + } + + prevAccount = currAccount; + } + + return true; + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/auth/MultisigBase.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/auth/MultisigBase.sol new file mode 100644 index 0000000000..51366d2191 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/auth/MultisigBase.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IMultisigBase } from '../interfaces/IMultisigBase.sol'; + +/** + * @title MultisigBase Contract + * @notice This contract implements a custom multisignature wallet where transactions must be confirmed by a + * threshold of signers. The signers and threshold may be updated every `epoch`. + */ +contract MultisigBase is IMultisigBase { + struct Voting { + uint256 voteCount; + mapping(address => bool) hasVoted; + } + + struct Signers { + address[] accounts; + uint256 threshold; + mapping(address => bool) isSigner; + } + + Signers public signers; + uint256 public signerEpoch; + // uint256 is for epoch, bytes32 for vote topic hash + mapping(uint256 => mapping(bytes32 => Voting)) public votingPerTopic; + + /** + * @notice Contract constructor + * @dev Sets the initial list of signers and corresponding threshold. + * @param accounts Address array of the signers + * @param threshold Signature threshold required to validate a transaction + */ + constructor(address[] memory accounts, uint256 threshold) { + _rotateSigners(accounts, threshold); + } + + /** + * @notice Modifier to ensure the caller is a signer + * @dev Keeps track of votes for each operation and resets the vote count if the operation is executed. + * @dev Given the early void return, this modifier should be used with care on functions that return data. + */ + modifier onlySigners() { + if (!signers.isSigner[msg.sender]) revert NotSigner(); + + bytes32 topic = keccak256(msg.data); + Voting storage voting = votingPerTopic[signerEpoch][topic]; + + // Check that signer has not voted, then record that they have voted. + if (voting.hasVoted[msg.sender]) revert AlreadyVoted(); + + voting.hasVoted[msg.sender] = true; + + // Determine the new vote count. + uint256 voteCount = voting.voteCount + 1; + + // Do not proceed with operation execution if insufficient votes. + if (voteCount < signers.threshold) { + // Save updated vote count. + voting.voteCount = voteCount; + return; + } + + // Clear vote count and voted booleans. + voting.voteCount = 0; + + uint256 count = signers.accounts.length; + + for (uint256 i; i < count; ++i) { + voting.hasVoted[signers.accounts[i]] = false; + } + + emit MultisigOperationExecuted(topic); + + _; + } + + /******************\ + |* Public Getters *| + \******************/ + + /** + * @notice Returns the current signer threshold + * @return uint The signer threshold + */ + function signerThreshold() external view override returns (uint256) { + return signers.threshold; + } + + /** + * @notice Returns an array of current signers + * @return array of signer addresses + */ + function signerAccounts() external view override returns (address[] memory) { + return signers.accounts; + } + + /***********\ + |* Setters *| + \***********/ + + /** + * @notice Rotate the signers for the multisig + * @dev Updates the current set of signers and threshold and increments the `epoch` + * @dev This function is protected by the onlySigners modifier + * @param newAccounts Address array of the new signers + * @param newThreshold The new signature threshold for executing operations + */ + function rotateSigners(address[] memory newAccounts, uint256 newThreshold) external virtual onlySigners { + _rotateSigners(newAccounts, newThreshold); + } + + /** + * @dev Internal function that implements signer rotation logic + */ + function _rotateSigners(address[] memory newAccounts, uint256 newThreshold) internal { + uint256 length = signers.accounts.length; + + // Clean up old signers. + for (uint256 i; i < length; ++i) { + signers.isSigner[signers.accounts[i]] = false; + } + + length = newAccounts.length; + + if (newThreshold > length) revert InvalidSigners(); + + if (newThreshold == 0) revert InvalidSignerThreshold(); + + ++signerEpoch; + + signers.accounts = newAccounts; + signers.threshold = newThreshold; + + for (uint256 i; i < length; ++i) { + address account = newAccounts[i]; + + // Check that the account wasn't already set as a signer for this epoch. + if (signers.isSigner[account]) revert DuplicateSigner(account); + if (account == address(0)) revert InvalidSigners(); + + signers.isSigner[account] = true; + } + + emit SignersRotated(newAccounts, newThreshold); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/AxelarDepositService.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/AxelarDepositService.sol new file mode 100644 index 0000000000..1aacb263aa --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/AxelarDepositService.sol @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { SafeTokenTransfer, SafeNativeTransfer } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol'; +import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; +import { IAxelarDepositService } from '../interfaces/IAxelarDepositService.sol'; +import { IAxelarGateway } from '../interfaces/IAxelarGateway.sol'; +import { IWETH9 } from '../interfaces/IWETH9.sol'; +import { Upgradable } from '../util/Upgradable.sol'; +import { DepositServiceBase } from './DepositServiceBase.sol'; +import { DepositReceiver } from './DepositReceiver.sol'; +import { ReceiverImplementation } from './ReceiverImplementation.sol'; + +// This should be owned by the microservice that is paying for gas. +contract AxelarDepositService is Upgradable, DepositServiceBase, IAxelarDepositService { + using SafeTokenTransfer for IERC20; + using SafeNativeTransfer for address; + + // This public storage for ERC20 token intended to be refunded. + // It triggers the DepositReceiver/ReceiverImplementation to switch into a refund mode. + // Address is stored and deleted withing the same refund transaction. + address public refundToken; + + address public immutable receiverImplementation; + address public immutable refundIssuer; + + constructor( + address gateway_, + string memory wrappedSymbol_, + address refundIssuer_ + ) DepositServiceBase(gateway_, wrappedSymbol_) { + if (refundIssuer_ == address(0)) revert InvalidAddress(); + + refundIssuer = refundIssuer_; + receiverImplementation = address(new ReceiverImplementation(gateway_, wrappedSymbol_)); + } + + // @dev This method is meant to be called directly by user to send native token cross-chain + function sendNative(string calldata destinationChain, string calldata destinationAddress) external payable { + address wrappedTokenAddress = wrappedToken(); + uint256 amount = msg.value; + + if (amount == 0) revert NothingDeposited(); + + // Wrapping the native currency and into WETH-like token + IWETH9(wrappedTokenAddress).deposit{ value: amount }(); + // Not doing safe approval as gateway will revert anyway if approval fails + // We expect allowance to always be 0 at this point + IWETH9(wrappedTokenAddress).approve(gateway, amount); + // Sending the token trough the gateway + IAxelarGateway(gateway).sendToken(destinationChain, destinationAddress, wrappedSymbol(), amount); + } + + // @dev Generates a deposit address for sending an ERC20 token cross-chain + function addressForTokenDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress, + string calldata tokenSymbol + ) external view returns (address) { + return + _depositAddress( + salt, + abi.encodeWithSelector( + ReceiverImplementation.receiveAndSendToken.selector, + refundAddress, + destinationChain, + destinationAddress, + tokenSymbol + ), + refundAddress + ); + } + + // @dev Generates a deposit address for sending native currency cross-chain + function addressForNativeDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress + ) public view returns (address) { + return + _depositAddress( + salt, + abi.encodeWithSelector( + ReceiverImplementation.receiveAndSendNative.selector, + refundAddress, + destinationChain, + destinationAddress + ), + refundAddress + ); + } + + // @dev Generates a deposit address for unwrapping WETH-like token into native currency + function addressForNativeUnwrap( + bytes32 salt, + address refundAddress, + address recipient + ) external view returns (address) { + return + _depositAddress( + salt, + abi.encodeWithSelector(ReceiverImplementation.receiveAndUnwrapNative.selector, refundAddress, recipient), + refundAddress + ); + } + + // @dev Receives ERC20 token from the deposit address and sends it cross-chain + function sendTokenDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress, + string calldata tokenSymbol + ) external { + // NOTE: `DepositReceiver` is destroyed in the same runtime context that it is deployed. + new DepositReceiver{ salt: salt }( + abi.encodeWithSelector( + ReceiverImplementation.receiveAndSendToken.selector, + refundAddress, + destinationChain, + destinationAddress, + tokenSymbol + ), + refundAddress + ); + } + + // @dev Refunds ERC20 tokens from the deposit address if they don't match the intended token + // Only refundAddress can refund the token that was intended to go cross-chain (if not sent yet) + function refundTokenDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress, + string calldata tokenSymbol, + address[] calldata refundTokens + ) external { + address intendedToken = IAxelarGateway(gateway).tokenAddresses(tokenSymbol); + + uint256 tokensLength = refundTokens.length; + for (uint256 i; i < tokensLength; ++i) { + // Allowing only the refundAddress to refund the intended token + if (refundTokens[i] == intendedToken && msg.sender != refundAddress) continue; + + // Saving to public storage to be accessed by the DepositReceiver + refundToken = refundTokens[i]; + // NOTE: `DepositReceiver` is destroyed in the same runtime context that it is deployed. + new DepositReceiver{ salt: salt }( + abi.encodeWithSelector( + ReceiverImplementation.receiveAndSendToken.selector, + refundAddress, + destinationChain, + destinationAddress, + tokenSymbol + ), + refundAddress + ); + } + + refundToken = address(0); + } + + // @dev Receives native currency, wraps it into WETH-like token and sends cross-chain + function sendNativeDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress + ) external { + // NOTE: `DepositReceiver` is destroyed in the same runtime context that it is deployed. + new DepositReceiver{ salt: salt }( + abi.encodeWithSelector( + ReceiverImplementation.receiveAndSendNative.selector, + refundAddress, + destinationChain, + destinationAddress + ), + refundAddress + ); + } + + // @dev Refunds ERC20 tokens from the deposit address after the native deposit was sent + // Only refundAddress can refund the native currency intended to go cross-chain (if not sent yet) + function refundNativeDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress, + address[] calldata refundTokens + ) external { + // Allowing only the refundAddress to refund the native currency + if (addressForNativeDeposit(salt, refundAddress, destinationChain, destinationAddress).balance > 0 && msg.sender != refundAddress) + return; + + uint256 tokensLength = refundTokens.length; + for (uint256 i; i < tokensLength; ++i) { + refundToken = refundTokens[i]; + // NOTE: `DepositReceiver` is destroyed in the same runtime context that it is deployed. + new DepositReceiver{ salt: salt }( + abi.encodeWithSelector( + ReceiverImplementation.receiveAndSendNative.selector, + refundAddress, + destinationChain, + destinationAddress + ), + refundAddress + ); + } + + refundToken = address(0); + } + + // @dev Receives WETH-like token, unwraps and send native currency to the recipient + function nativeUnwrap( + bytes32 salt, + address refundAddress, + address payable recipient + ) external { + // NOTE: `DepositReceiver` is destroyed in the same runtime context that it is deployed. + new DepositReceiver{ salt: salt }( + abi.encodeWithSelector(ReceiverImplementation.receiveAndUnwrapNative.selector, refundAddress, recipient), + refundAddress + ); + } + + // @dev Refunds ERC20 tokens from the deposit address except WETH-like token + // Only refundAddress can refund the WETH-like token intended to be unwrapped (if not yet) + function refundNativeUnwrap( + bytes32 salt, + address refundAddress, + address payable recipient, + address[] calldata refundTokens + ) external { + address wrappedTokenAddress = wrappedToken(); + + uint256 tokensLength = refundTokens.length; + for (uint256 i; i < tokensLength; ++i) { + // Allowing only the refundAddress to refund the intended WETH-like token + if (refundTokens[i] == wrappedTokenAddress && msg.sender != refundAddress) continue; + + refundToken = refundTokens[i]; + // NOTE: `DepositReceiver` is destroyed in the same runtime context that it is deployed. + new DepositReceiver{ salt: salt }( + abi.encodeWithSelector(ReceiverImplementation.receiveAndUnwrapNative.selector, refundAddress, recipient), + refundAddress + ); + } + + refundToken = address(0); + } + + function refundLockedAsset( + address receiver, + address token, + uint256 amount + ) external { + if (msg.sender != refundIssuer) revert NotRefundIssuer(); + if (receiver == address(0)) revert InvalidAddress(); + if (amount == 0) revert InvalidAmount(); + + if (token == address(0)) { + receiver.safeNativeTransfer(amount); + } else { + IERC20(token).safeTransfer(receiver, amount); + } + } + + function _depositAddress( + bytes32 salt, + bytes memory delegateData, + address refundAddress + ) internal view returns (address) { + /* Convert a hash which is bytes32 to an address which is 20-byte long + according to https://docs.soliditylang.org/en/v0.8.9/control-structures.html?highlight=create2#salted-contract-creations-create2 */ + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex'ff', + address(this), + salt, + // Encoding delegateData and refundAddress as constructor params + keccak256(abi.encodePacked(type(DepositReceiver).creationCode, abi.encode(delegateData, refundAddress))) + ) + ) + ) + ) + ); + } + + function contractId() external pure returns (bytes32) { + return keccak256('axelar-deposit-service'); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/AxelarDepositServiceProxy.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/AxelarDepositServiceProxy.sol new file mode 100644 index 0000000000..cf87855be9 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/AxelarDepositServiceProxy.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { Proxy } from '../util/Proxy.sol'; + +contract AxelarDepositServiceProxy is Proxy { + function contractId() internal pure override returns (bytes32) { + return keccak256('axelar-deposit-service'); + } + + // @dev This function is for receiving refunds when refundAddress was 0x0 + // solhint-disable-next-line no-empty-blocks + receive() external payable override {} +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/DepositReceiver.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/DepositReceiver.sol new file mode 100644 index 0000000000..5676d75682 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/DepositReceiver.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IAxelarDepositService } from '../interfaces/IAxelarDepositService.sol'; + +contract DepositReceiver { + constructor(bytes memory delegateData, address refundAddress) { + // Reading the implementation of the AxelarDepositService + // and delegating the call back to it + (bool success, ) = IAxelarDepositService(msg.sender).receiverImplementation().delegatecall(delegateData); + + // if not success revert with the original revert data + if (!success) { + assembly { + let ptr := mload(0x40) + let size := returndatasize() + returndatacopy(ptr, 0, size) + revert(ptr, size) + } + } + + if (refundAddress == address(0)) refundAddress = msg.sender; + + selfdestruct(payable(refundAddress)); + } + + // @dev This function is for receiving Ether from unwrapping WETH9 + receive() external payable {} +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/DepositServiceBase.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/DepositServiceBase.sol new file mode 100644 index 0000000000..566a60b05e --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/DepositServiceBase.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { SafeTokenTransfer } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol'; +import { StringToBytes32, Bytes32ToString } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/Bytes32String.sol'; +import { IAxelarGateway } from '../interfaces/IAxelarGateway.sol'; +import { IDepositServiceBase } from '../interfaces/IDepositServiceBase.sol'; + +// This should be owned by the microservice that is paying for gas. +abstract contract DepositServiceBase is IDepositServiceBase { + using SafeTokenTransfer for address; + using StringToBytes32 for string; + using Bytes32ToString for bytes32; + + // Using immutable storage to keep the constants in the bytecode + address public immutable gateway; + address public immutable wrappedTokenAddress; + bytes32 internal immutable wrappedSymbolBytes; + + constructor(address gateway_, string memory wrappedSymbol_) { + if (gateway_ == address(0)) revert InvalidAddress(); + + bool wrappedTokenEnabled = bytes(wrappedSymbol_).length > 0; + + gateway = gateway_; + wrappedTokenAddress = wrappedTokenEnabled ? IAxelarGateway(gateway_).tokenAddresses(wrappedSymbol_) : address(0); + wrappedSymbolBytes = wrappedTokenEnabled ? wrappedSymbol_.toBytes32() : bytes32(0); + + // Wrapped token symbol param is optional + // When specified we are checking if token exists in the gateway + if (wrappedTokenEnabled && wrappedTokenAddress == address(0)) revert InvalidSymbol(); + } + + function wrappedToken() public view returns (address) { + return wrappedTokenAddress; + } + + // @dev Converts bytes32 from immutable storage into a string + function wrappedSymbol() public view returns (string memory) { + return wrappedSymbolBytes.toTrimmedString(); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/ReceiverImplementation.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/ReceiverImplementation.sol new file mode 100644 index 0000000000..f320affb31 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/deposit-service/ReceiverImplementation.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { SafeTokenTransfer, SafeNativeTransfer } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol'; +import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; +import { IAxelarGateway } from '../interfaces/IAxelarGateway.sol'; +import { IWETH9 } from '../interfaces/IWETH9.sol'; +import { IAxelarDepositService } from '../interfaces/IAxelarDepositService.sol'; +import { DepositServiceBase } from './DepositServiceBase.sol'; + +// This should be owned by the microservice that is paying for gas. +contract ReceiverImplementation is DepositServiceBase { + using SafeTokenTransfer for IERC20; + using SafeNativeTransfer for address; + + constructor(address gateway_, string memory wrappedSymbol_) DepositServiceBase(gateway_, wrappedSymbol_) {} + + // @dev This function is used for delegate call by DepositReceiver + // Context: msg.sender == AxelarDepositService, this == DepositReceiver + function receiveAndSendToken( + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress, + string calldata symbol + ) external { + address tokenAddress = IAxelarGateway(gateway).tokenAddresses(symbol); + // Checking with AxelarDepositService if need to refund a token + address refund = IAxelarDepositService(msg.sender).refundToken(); + + if (refund != address(0)) { + if (refundAddress == address(0)) refundAddress = msg.sender; + IERC20(refund).safeTransfer(refundAddress, IERC20(refund).balanceOf(address(this))); + return; + } + + uint256 amount = IERC20(tokenAddress).balanceOf(address(this)); + + if (tokenAddress == address(0)) revert InvalidSymbol(); + if (amount == 0) revert NothingDeposited(); + + // Not doing safe approval as gateway will revert anyway if approval fails + // We expect allowance to always be 0 at this point + IERC20(tokenAddress).approve(gateway, amount); + // Sending the token trough the gateway + IAxelarGateway(gateway).sendToken(destinationChain, destinationAddress, symbol, amount); + } + + // @dev This function is used for delegate call by DepositReceiver + // Context: msg.sender == AxelarDepositService, this == DepositReceiver + function receiveAndSendNative( + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress + ) external { + address refund = IAxelarDepositService(msg.sender).refundToken(); + + if (refund != address(0)) { + if (refundAddress == address(0)) refundAddress = msg.sender; + IERC20(refund).safeTransfer(refundAddress, IERC20(refund).balanceOf(address(this))); + return; + } + + address wrappedTokenAddress = wrappedToken(); + uint256 amount = address(this).balance; + + if (wrappedTokenAddress == address(0)) revert WrappedTokenNotSupported(); + if (amount == 0) revert NothingDeposited(); + + // Wrapping the native currency and into WETH-like + IWETH9(wrappedTokenAddress).deposit{ value: amount }(); + // Not doing safe approval as gateway will revert anyway if approval fails + // We expect allowance to always be 0 at this point + IWETH9(wrappedTokenAddress).approve(gateway, amount); + // Sending the token trough the gateway + IAxelarGateway(gateway).sendToken(destinationChain, destinationAddress, wrappedSymbol(), amount); + } + + // @dev This function is used for delegate call by DepositReceiver + // Context: msg.sender == AxelarDepositService, this == DepositReceiver + function receiveAndUnwrapNative(address refundAddress, address recipient) external { + address wrappedTokenAddress = wrappedToken(); + address refund = IAxelarDepositService(msg.sender).refundToken(); + + if (refund != address(0)) { + if (refundAddress == address(0)) refundAddress = msg.sender; + IERC20(refund).safeTransfer(refundAddress, IERC20(refund).balanceOf(address(this))); + return; + } + + uint256 amount = IERC20(wrappedTokenAddress).balanceOf(address(this)); + + if (wrappedTokenAddress == address(0)) revert WrappedTokenNotSupported(); + if (amount == 0) revert NothingDeposited(); + + // Unwrapping the token into native currency and sending it to the recipient + IWETH9(wrappedTokenAddress).withdraw(amount); + recipient.safeNativeTransfer(amount); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/gas-service/AxelarGasService.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/gas-service/AxelarGasService.sol new file mode 100644 index 0000000000..fd729ad448 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/gas-service/AxelarGasService.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; +import { SafeTokenTransfer, SafeTokenTransferFrom, SafeNativeTransfer } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/SafeTransfer.sol'; +import { IAxelarGasService } from '../interfaces/IAxelarGasService.sol'; +import { Upgradable } from '../util/Upgradable.sol'; + +// This should be owned by the microservice that is paying for gas. +contract AxelarGasService is Upgradable, IAxelarGasService { + using SafeTokenTransfer for IERC20; + using SafeTokenTransferFrom for IERC20; + using SafeNativeTransfer for address payable; + + address public immutable gasCollector; + + constructor(address gasCollector_) { + gasCollector = gasCollector_; + } + + modifier onlyCollector() { + if (msg.sender != gasCollector) revert NotCollector(); + + _; + } + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payGasForContractCall( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external override { + IERC20(gasToken).safeTransferFrom(msg.sender, address(this), gasFeeAmount); + + emit GasPaidForContractCall( + sender, + destinationChain, + destinationAddress, + keccak256(payload), + gasToken, + gasFeeAmount, + refundAddress + ); + } + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payGasForContractCallWithToken( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + string memory symbol, + uint256 amount, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external override { + IERC20(gasToken).safeTransferFrom(msg.sender, address(this), gasFeeAmount); + + emit GasPaidForContractCallWithToken( + sender, + destinationChain, + destinationAddress, + keccak256(payload), + symbol, + amount, + gasToken, + gasFeeAmount, + refundAddress + ); + } + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payNativeGasForContractCall( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + address refundAddress + ) external payable override { + if (msg.value == 0) revert NothingReceived(); + + emit NativeGasPaidForContractCall(sender, destinationChain, destinationAddress, keccak256(payload), msg.value, refundAddress); + } + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payNativeGasForContractCallWithToken( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + string calldata symbol, + uint256 amount, + address refundAddress + ) external payable override { + if (msg.value == 0) revert NothingReceived(); + + emit NativeGasPaidForContractCallWithToken( + sender, + destinationChain, + destinationAddress, + keccak256(payload), + symbol, + amount, + msg.value, + refundAddress + ); + } + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payGasForExpressCallWithToken( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + string memory symbol, + uint256 amount, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external override { + IERC20(gasToken).safeTransferFrom(msg.sender, address(this), gasFeeAmount); + + emit GasPaidForExpressCallWithToken( + sender, + destinationChain, + destinationAddress, + keccak256(payload), + symbol, + amount, + gasToken, + gasFeeAmount, + refundAddress + ); + } + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payNativeGasForExpressCallWithToken( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + string calldata symbol, + uint256 amount, + address refundAddress + ) external payable override { + if (msg.value == 0) revert NothingReceived(); + + emit NativeGasPaidForExpressCallWithToken( + sender, + destinationChain, + destinationAddress, + keccak256(payload), + symbol, + amount, + msg.value, + refundAddress + ); + } + + // This can be called on the source chain after calling the gateway to execute a remote contract. + function addGas( + bytes32 txHash, + uint256 logIndex, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external override { + IERC20(gasToken).safeTransferFrom(msg.sender, address(this), gasFeeAmount); + + emit GasAdded(txHash, logIndex, gasToken, gasFeeAmount, refundAddress); + } + + function addNativeGas( + bytes32 txHash, + uint256 logIndex, + address refundAddress + ) external payable override { + if (msg.value == 0) revert NothingReceived(); + + emit NativeGasAdded(txHash, logIndex, msg.value, refundAddress); + } + + // This can be called on the source chain after calling the gateway to express execute a remote contract. + function addExpressGas( + bytes32 txHash, + uint256 logIndex, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external override { + IERC20(gasToken).safeTransferFrom(msg.sender, address(this), gasFeeAmount); + + emit ExpressGasAdded(txHash, logIndex, gasToken, gasFeeAmount, refundAddress); + } + + // This can be called on the source chain after calling the gateway to express execute a remote contract. + function addNativeExpressGas( + bytes32 txHash, + uint256 logIndex, + address refundAddress + ) external payable override { + if (msg.value == 0) revert NothingReceived(); + + emit NativeExpressGasAdded(txHash, logIndex, msg.value, refundAddress); + } + + function collectFees( + address payable receiver, + address[] calldata tokens, + uint256[] calldata amounts + ) external onlyCollector { + if (receiver == address(0)) revert InvalidAddress(); + + uint256 tokensLength = tokens.length; + if (tokensLength != amounts.length) revert InvalidAmounts(); + + for (uint256 i; i < tokensLength; i++) { + address token = tokens[i]; + uint256 amount = amounts[i]; + if (amount == 0) revert InvalidAmounts(); + + if (token == address(0)) { + if (amount <= address(this).balance) receiver.safeNativeTransfer(amount); + } else { + if (amount <= IERC20(token).balanceOf(address(this))) IERC20(token).safeTransfer(receiver, amount); + } + } + } + + // deprecated + function refund( + address payable receiver, + address token, + uint256 amount + ) external onlyCollector { + _refund(bytes32(0), 0, receiver, token, amount); + } + + function refund( + bytes32 txHash, + uint256 logIndex, + address payable receiver, + address token, + uint256 amount + ) external onlyCollector { + _refund(txHash, logIndex, receiver, token, amount); + } + + function _refund( + bytes32 txHash, + uint256 logIndex, + address payable receiver, + address token, + uint256 amount + ) private { + if (receiver == address(0)) revert InvalidAddress(); + + if (token == address(0)) { + receiver.safeNativeTransfer(amount); + } else { + IERC20(token).safeTransfer(receiver, amount); + } + + emit Refunded(txHash, logIndex, receiver, token, amount); + } + + function contractId() external pure returns (bytes32) { + return keccak256('axelar-gas-service'); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/gas-service/AxelarGasServiceProxy.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/gas-service/AxelarGasServiceProxy.sol new file mode 100644 index 0000000000..92c45edbfe --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/gas-service/AxelarGasServiceProxy.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { Proxy } from '../util/Proxy.sol'; +import { IUpgradable } from '../interfaces/IUpgradable.sol'; + +contract AxelarGasServiceProxy is Proxy { + function contractId() internal pure override returns (bytes32) { + return keccak256('axelar-gas-service'); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/governance/AxelarServiceGovernance.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/governance/AxelarServiceGovernance.sol new file mode 100644 index 0000000000..425e2b05f1 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/governance/AxelarServiceGovernance.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IAxelarServiceGovernance } from '../interfaces/IAxelarServiceGovernance.sol'; +import { InterchainGovernance } from './InterchainGovernance.sol'; +import { MultisigBase } from '../auth/MultisigBase.sol'; + +/** + * @title AxelarServiceGovernance Contract + * @dev This contract is part of the Axelar Governance system, it inherits the Interchain Governance contract + * with added functionality to approve and execute multisig proposals. + */ +contract AxelarServiceGovernance is InterchainGovernance, MultisigBase, IAxelarServiceGovernance { + enum ServiceGovernanceCommand { + ScheduleTimeLockProposal, + CancelTimeLockProposal, + ApproveMultisigProposal, + CancelMultisigApproval + } + + mapping(bytes32 => bool) public multisigApprovals; + + /** + * @notice Initializes the contract. + * @param gateway The address of the Axelar gateway contract + * @param governanceChain The name of the governance chain + * @param governanceAddress The address of the governance contract + * @param minimumTimeDelay The minimum time delay for timelock operations + * @param cosigners The list of initial signers + * @param threshold The number of required signers to validate a transaction + */ + constructor( + address gateway, + string memory governanceChain, + string memory governanceAddress, + uint256 minimumTimeDelay, + address[] memory cosigners, + uint256 threshold + ) InterchainGovernance(gateway, governanceChain, governanceAddress, minimumTimeDelay) MultisigBase(cosigners, threshold) {} + + /** + * @notice Executes a multisig proposal. + * @param target The target address the proposal will call + * @param callData The data that encodes the function and arguments to call on the target contract + * @param nativeValue The value of native token to be sent to the target contract + */ + function executeMultisigProposal( + address target, + bytes calldata callData, + uint256 nativeValue + ) external payable onlySigners { + bytes32 proposalHash = keccak256(abi.encodePacked(target, callData, nativeValue)); + + if (!multisigApprovals[proposalHash]) revert NotApproved(); + + multisigApprovals[proposalHash] = false; + + _call(target, callData, nativeValue); + + emit MultisigExecuted(proposalHash, target, callData, nativeValue); + } + + /** + * @notice Internal function to process a governance command + * @param commandId The id of the command + * @param target The target address the proposal will call + * @param callData The data the encodes the function and arguments to call on the target contract + * @param nativeValue The value of native token to be sent to the target contract + * @param eta The time after which the proposal can be executed + */ + function _processCommand( + uint256 commandId, + address target, + bytes memory callData, + uint256 nativeValue, + uint256 eta + ) internal override { + if (commandId > uint256(type(ServiceGovernanceCommand).max)) { + revert InvalidCommand(); + } + + ServiceGovernanceCommand command = ServiceGovernanceCommand(commandId); + bytes32 proposalHash = keccak256(abi.encodePacked(target, callData, nativeValue)); + + if (command == ServiceGovernanceCommand.ScheduleTimeLockProposal) { + eta = _scheduleTimeLock(proposalHash, eta); + + emit ProposalScheduled(proposalHash, target, callData, nativeValue, eta); + return; + } else if (command == ServiceGovernanceCommand.CancelTimeLockProposal) { + _cancelTimeLock(proposalHash); + + emit ProposalCancelled(proposalHash, target, callData, nativeValue, eta); + return; + } else if (command == ServiceGovernanceCommand.ApproveMultisigProposal) { + multisigApprovals[proposalHash] = true; + + emit MultisigApproved(proposalHash, target, callData, nativeValue); + return; + } else if (command == ServiceGovernanceCommand.CancelMultisigApproval) { + multisigApprovals[proposalHash] = false; + + emit MultisigCancelled(proposalHash, target, callData, nativeValue); + return; + } + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/governance/InterchainGovernance.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/governance/InterchainGovernance.sol new file mode 100644 index 0000000000..339dc42575 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/governance/InterchainGovernance.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol'; +import { TimeLock } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/TimeLock.sol'; +import { IInterchainGovernance } from '../interfaces/IInterchainGovernance.sol'; +import { Caller } from '../util/Caller.sol'; + +/** + * @title Interchain Governance contract + * @notice This contract handles cross-chain governance actions. It includes functionality + * to create, cancel, and execute governance proposals. + */ +contract InterchainGovernance is AxelarExecutable, TimeLock, Caller, IInterchainGovernance { + enum GovernanceCommand { + ScheduleTimeLockProposal, + CancelTimeLockProposal + } + + string public governanceChain; + string public governanceAddress; + bytes32 public immutable governanceChainHash; + bytes32 public immutable governanceAddressHash; + + /** + * @notice Initializes the contract + * @param gateway The address of the Axelar gateway contract + * @param governanceChain_ The name of the governance chain + * @param governanceAddress_ The address of the governance contract + * @param minimumTimeDelay The minimum time delay for timelock operations + */ + constructor( + address gateway, + string memory governanceChain_, + string memory governanceAddress_, + uint256 minimumTimeDelay + ) AxelarExecutable(gateway) TimeLock(minimumTimeDelay) { + governanceChain = governanceChain_; + governanceAddress = governanceAddress_; + governanceChainHash = keccak256(bytes(governanceChain_)); + governanceAddressHash = keccak256(bytes(governanceAddress_)); + } + + /** + * @notice Returns the ETA of a proposal + * @param target The address of the contract targeted by the proposal + * @param callData The call data to be sent to the target contract + * @param nativeValue The amount of native tokens to be sent to the target contract + * @return uint256 The ETA of the proposal + */ + function getProposalEta( + address target, + bytes calldata callData, + uint256 nativeValue + ) external view returns (uint256) { + return _getTimeLockEta(_getProposalHash(target, callData, nativeValue)); + } + + /** + * @notice Executes a proposal + * @dev The proposal is executed by calling the target contract with calldata. Native value is + * transferred with the call to the target contract. + * @param target The target address of the contract to call + * @param callData The data containing the function and arguments for the contract to call + * @param nativeValue The amount of native token to send to the target contract + */ + function executeProposal( + address target, + bytes calldata callData, + uint256 nativeValue + ) external payable { + bytes32 proposalHash = keccak256(abi.encodePacked(target, callData, nativeValue)); + + _finalizeTimeLock(proposalHash); + _call(target, callData, nativeValue); + + emit ProposalExecuted(proposalHash, target, callData, nativeValue, block.timestamp); + } + + /** + * @notice Internal function to execute a proposal action + * @param sourceChain The source chain of the proposal, must equal the governance chain + * @param sourceAddress The source address of the proposal, must equal the governance address + * @param payload The payload of the proposal + */ + function _execute( + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload + ) internal override { + if (keccak256(bytes(sourceChain)) != governanceChainHash || keccak256(bytes(sourceAddress)) != governanceAddressHash) + revert NotGovernance(); + + (uint256 command, address target, bytes memory callData, uint256 nativeValue, uint256 eta) = abi.decode( + payload, + (uint256, address, bytes, uint256, uint256) + ); + + if (target == address(0)) revert InvalidTarget(); + + _processCommand(command, target, callData, nativeValue, eta); + } + + /** + * @notice Internal function to process a governance command + * @param commandId The id of the command, 0 for proposal creation and 1 for proposal cancellation + * @param target The target address the proposal will call + * @param callData The data the encodes the function and arguments to call on the target contract + * @param nativeValue The nativeValue of native token to be sent to the target contract + * @param eta The time after which the proposal can be executed + */ + function _processCommand( + uint256 commandId, + address target, + bytes memory callData, + uint256 nativeValue, + uint256 eta + ) internal virtual { + if (commandId > uint256(type(GovernanceCommand).max)) { + revert InvalidCommand(); + } + + GovernanceCommand command = GovernanceCommand(commandId); + bytes32 proposalHash = _getProposalHash(target, callData, nativeValue); + + if (command == GovernanceCommand.ScheduleTimeLockProposal) { + eta = _scheduleTimeLock(proposalHash, eta); + + emit ProposalScheduled(proposalHash, target, callData, nativeValue, eta); + return; + } else if (command == GovernanceCommand.CancelTimeLockProposal) { + _cancelTimeLock(proposalHash); + + emit ProposalCancelled(proposalHash, target, callData, nativeValue, eta); + return; + } + } + + /** + * @dev Get proposal hash using the target, callData, and nativeValue + */ + function _getProposalHash( + address target, + bytes memory callData, + uint256 nativeValue + ) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(target, callData, nativeValue)); + } + + /** + * @notice Overrides internal function of AxelarExecutable, will always revert + * as this governance module does not support execute with token. + */ + function _executeWithToken( + string calldata, /* sourceChain */ + string calldata, /* sourceAddress */ + bytes calldata, /* payload */ + string calldata, /* tokenSymbol */ + uint256 /* amount */ + ) internal pure override { + revert TokenNotSupported(); + } + + /** + * @notice Making contact able to receive native value + */ + receive() external payable {} +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/governance/Multisig.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/governance/Multisig.sol new file mode 100644 index 0000000000..f80bb66609 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/governance/Multisig.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IMultisig } from '../interfaces/IMultisig.sol'; +import { MultisigBase } from '../auth/MultisigBase.sol'; +import { Caller } from '../util/Caller.sol'; + +/** + * @title Multisig Contract + * @notice An extension of MultisigBase that can call functions on any contract. + */ +contract Multisig is Caller, MultisigBase, IMultisig { + /** + * @notice Contract constructor + * @dev Sets the initial list of signers and corresponding threshold. + * @param accounts Address array of the signers + * @param threshold Signature threshold required to validate a transaction + */ + constructor(address[] memory accounts, uint256 threshold) MultisigBase(accounts, threshold) {} + + /** + * @notice Executes an external contract call. + * @dev Calls a target address with specified calldata and optionally sends value. + * This function is protected by the onlySigners modifier. + * @param target The address of the contract to call + * @param callData The data encoding the function and arguments to call + * @param nativeValue The amount of native currency (e.g., ETH) to send along with the call + */ + function execute( + address target, + bytes calldata callData, + uint256 nativeValue + ) external payable onlySigners { + _call(target, callData, nativeValue); + } + + /** + * @notice Making contact able to receive native value + */ + receive() external payable {} +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarAuth.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarAuth.sol new file mode 100644 index 0000000000..8b97f0b1d8 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarAuth.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import { IOwnable } from './IOwnable.sol'; + +interface IAxelarAuth is IOwnable { + function validateProof(bytes32 messageHash, bytes calldata proof) external returns (bool currentOperators); + + function transferOperatorship(bytes calldata params) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarAuthWeighted.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarAuthWeighted.sol new file mode 100644 index 0000000000..8981eacbc5 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarAuthWeighted.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import { IAxelarAuth } from './IAxelarAuth.sol'; + +interface IAxelarAuthWeighted is IAxelarAuth { + error InvalidOperators(); + error InvalidThreshold(); + error DuplicateOperators(); + error MalformedSigners(); + error LowSignaturesWeight(); + error InvalidWeights(); + + event OperatorshipTransferred(address[] newOperators, uint256[] newWeights, uint256 newThreshold); + + function currentEpoch() external view returns (uint256); + + function hashForEpoch(uint256 epoch) external view returns (bytes32); + + function epochForHash(bytes32 hash) external view returns (uint256); +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarDepositService.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarDepositService.sol new file mode 100644 index 0000000000..126f3cb97f --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarDepositService.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IUpgradable } from './IUpgradable.sol'; +import { IDepositServiceBase } from './IDepositServiceBase.sol'; + +interface IAxelarDepositService is IUpgradable, IDepositServiceBase { + function sendNative(string calldata destinationChain, string calldata destinationAddress) external payable; + + function addressForTokenDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress, + string calldata tokenSymbol + ) external view returns (address); + + function addressForNativeDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress + ) external view returns (address); + + function addressForNativeUnwrap( + bytes32 salt, + address refundAddress, + address recipient + ) external view returns (address); + + function sendTokenDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress, + string calldata tokenSymbol + ) external; + + function refundTokenDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress, + string calldata tokenSymbol, + address[] calldata refundTokens + ) external; + + function sendNativeDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress + ) external; + + function refundNativeDeposit( + bytes32 salt, + address refundAddress, + string calldata destinationChain, + string calldata destinationAddress, + address[] calldata refundTokens + ) external; + + function nativeUnwrap( + bytes32 salt, + address refundAddress, + address payable recipient + ) external; + + function refundNativeUnwrap( + bytes32 salt, + address refundAddress, + address payable recipient, + address[] calldata refundTokens + ) external; + + function refundLockedAsset( + address receiver, + address token, + uint256 amount + ) external; + + function receiverImplementation() external returns (address receiver); + + function refundToken() external returns (address); +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarGasService.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarGasService.sol new file mode 100644 index 0000000000..b2a3aab342 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarGasService.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IUpgradable } from './IUpgradable.sol'; + +interface IAxelarGasService is IUpgradable { + error NothingReceived(); + error InvalidAddress(); + error NotCollector(); + error InvalidAmounts(); + + event GasPaidForContractCall( + address indexed sourceAddress, + string destinationChain, + string destinationAddress, + bytes32 indexed payloadHash, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ); + + event GasPaidForContractCallWithToken( + address indexed sourceAddress, + string destinationChain, + string destinationAddress, + bytes32 indexed payloadHash, + string symbol, + uint256 amount, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ); + + event NativeGasPaidForContractCall( + address indexed sourceAddress, + string destinationChain, + string destinationAddress, + bytes32 indexed payloadHash, + uint256 gasFeeAmount, + address refundAddress + ); + + event NativeGasPaidForContractCallWithToken( + address indexed sourceAddress, + string destinationChain, + string destinationAddress, + bytes32 indexed payloadHash, + string symbol, + uint256 amount, + uint256 gasFeeAmount, + address refundAddress + ); + + event GasPaidForExpressCallWithToken( + address indexed sourceAddress, + string destinationChain, + string destinationAddress, + bytes32 indexed payloadHash, + string symbol, + uint256 amount, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ); + + event NativeGasPaidForExpressCallWithToken( + address indexed sourceAddress, + string destinationChain, + string destinationAddress, + bytes32 indexed payloadHash, + string symbol, + uint256 amount, + uint256 gasFeeAmount, + address refundAddress + ); + + event GasAdded(bytes32 indexed txHash, uint256 indexed logIndex, address gasToken, uint256 gasFeeAmount, address refundAddress); + + event NativeGasAdded(bytes32 indexed txHash, uint256 indexed logIndex, uint256 gasFeeAmount, address refundAddress); + + event ExpressGasAdded(bytes32 indexed txHash, uint256 indexed logIndex, address gasToken, uint256 gasFeeAmount, address refundAddress); + + event NativeExpressGasAdded(bytes32 indexed txHash, uint256 indexed logIndex, uint256 gasFeeAmount, address refundAddress); + + event Refunded(bytes32 indexed txHash, uint256 indexed logIndex, address payable receiver, address token, uint256 amount); + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payGasForContractCall( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external; + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payGasForContractCallWithToken( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + string calldata symbol, + uint256 amount, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external; + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payNativeGasForContractCall( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + address refundAddress + ) external payable; + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payNativeGasForContractCallWithToken( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + string calldata symbol, + uint256 amount, + address refundAddress + ) external payable; + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payGasForExpressCallWithToken( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + string calldata symbol, + uint256 amount, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external; + + // This is called on the source chain before calling the gateway to execute a remote contract. + function payNativeGasForExpressCallWithToken( + address sender, + string calldata destinationChain, + string calldata destinationAddress, + bytes calldata payload, + string calldata symbol, + uint256 amount, + address refundAddress + ) external payable; + + function addGas( + bytes32 txHash, + uint256 txIndex, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external; + + function addNativeGas( + bytes32 txHash, + uint256 logIndex, + address refundAddress + ) external payable; + + function addExpressGas( + bytes32 txHash, + uint256 txIndex, + address gasToken, + uint256 gasFeeAmount, + address refundAddress + ) external; + + function addNativeExpressGas( + bytes32 txHash, + uint256 logIndex, + address refundAddress + ) external payable; + + function collectFees( + address payable receiver, + address[] calldata tokens, + uint256[] calldata amounts + ) external; + + function refund( + address payable receiver, + address token, + uint256 amount + ) external; + + function gasCollector() external returns (address); +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarGateway.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarGateway.sol similarity index 100% rename from pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarGateway.sol rename to pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarGateway.sol diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarServiceGovernance.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarServiceGovernance.sol new file mode 100644 index 0000000000..e688a2830a --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IAxelarServiceGovernance.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IInterchainGovernance } from './IInterchainGovernance.sol'; +import { IMultisigBase } from './IMultisigBase.sol'; + +/** + * @title IAxelarServiceGovernance Interface + * @dev This interface extends IInterchainGovernance and IMultisigBase for multisig proposal actions + */ +interface IAxelarServiceGovernance is IMultisigBase, IInterchainGovernance { + error NotApproved(); + + event MultisigApproved(bytes32 indexed proposalHash, address indexed targetContract, bytes callData, uint256 nativeValue); + event MultisigCancelled(bytes32 indexed proposalHash, address indexed targetContract, bytes callData, uint256 nativeValue); + event MultisigExecuted(bytes32 indexed proposalHash, address indexed targetContract, bytes callData, uint256 nativeValue); + + /** + * @notice Executes a multisig proposal + * @param targetContract The target address the proposal will call + * @param callData The data that encodes the function and arguments to call on the target contract + */ + function executeMultisigProposal( + address targetContract, + bytes calldata callData, + uint256 value + ) external payable; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IBurnableMintableCappedERC20.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IBurnableMintableCappedERC20.sol new file mode 100644 index 0000000000..216b9590e2 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IBurnableMintableCappedERC20.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import { IERC20Burn } from './IERC20Burn.sol'; +import { IERC20BurnFrom } from './IERC20BurnFrom.sol'; +import { IMintableCappedERC20 } from './IMintableCappedERC20.sol'; + +interface IBurnableMintableCappedERC20 is IERC20Burn, IERC20BurnFrom, IMintableCappedERC20 { + function depositAddress(bytes32 salt) external view returns (address); + + function burn(bytes32 salt) external; + + function burnFrom(address account, uint256 amount) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/ICaller.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/ICaller.sol new file mode 100644 index 0000000000..99dfbf2506 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/ICaller.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface ICaller { + error InsufficientBalance(); + error ExecutionFailed(); +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IDepositServiceBase.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IDepositServiceBase.sol new file mode 100644 index 0000000000..444486ff69 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IDepositServiceBase.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IDepositServiceBase { + error InvalidAddress(); + error InvalidSymbol(); + error InvalidAmount(); + error NothingDeposited(); + error WrapFailed(); + error UnwrapFailed(); + error TokenApproveFailed(); + error NotRefundIssuer(); + error WrappedTokenNotSupported(); + + function gateway() external returns (address); + + function wrappedSymbol() external returns (string memory); + + function wrappedToken() external returns (address); +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IERC20.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20.sol similarity index 100% rename from pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IERC20.sol rename to pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20.sol diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20Burn.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20Burn.sol new file mode 100644 index 0000000000..4dd499e11e --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20Burn.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +interface IERC20Burn { + function burn(bytes32 salt) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20BurnFrom.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20BurnFrom.sol new file mode 100644 index 0000000000..652eb743a8 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20BurnFrom.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +interface IERC20BurnFrom { + function burnFrom(address account, uint256 amount) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20Permit.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20Permit.sol new file mode 100644 index 0000000000..1d685fffcb --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IERC20Permit.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +interface IERC20Permit { + function DOMAIN_SEPARATOR() external view returns (bytes32); + + function nonces(address account) external view returns (uint256); + + function permit( + address issuer, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IGovernable.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IGovernable.sol new file mode 100644 index 0000000000..64831b58ce --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IGovernable.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +interface IGovernable { + error NotGovernance(); + error NotMintLimiter(); + error InvalidGovernance(); + error InvalidMintLimiter(); + + event GovernanceTransferred(address indexed previousGovernance, address indexed newGovernance); + event MintLimiterTransferred(address indexed previousGovernance, address indexed newGovernance); + + function governance() external view returns (address); + + function mintLimiter() external view returns (address); + + function transferGovernance(address newGovernance) external; + + function transferMintLimiter(address newGovernance) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IInterchainGovernance.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IInterchainGovernance.sol new file mode 100644 index 0000000000..3e8d142531 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IInterchainGovernance.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IAxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarExecutable.sol'; +import { ICaller } from './ICaller.sol'; + +/** + * @title IInterchainGovernance Interface + * @notice This interface extends IAxelarExecutable for interchain governance mechanisms. + */ +interface IInterchainGovernance is IAxelarExecutable, ICaller { + error NotGovernance(); + error InvalidCommand(); + error InvalidTarget(); + error TokenNotSupported(); + + event ProposalScheduled(bytes32 indexed proposalHash, address indexed target, bytes callData, uint256 value, uint256 indexed eta); + event ProposalCancelled(bytes32 indexed proposalHash, address indexed target, bytes callData, uint256 value, uint256 indexed eta); + event ProposalExecuted(bytes32 indexed proposalHash, address indexed target, bytes callData, uint256 value, uint256 indexed timestamp); + + /** + * @notice Returns the name of the governance chain. + * @return string The name of the governance chain + */ + function governanceChain() external view returns (string memory); + + /** + * @notice Returns the address of the governance address. + * @return string The address of the governance address + */ + function governanceAddress() external view returns (string memory); + + /** + * @notice Returns the hash of the governance chain. + * @return bytes32 The hash of the governance chain + */ + function governanceChainHash() external view returns (bytes32); + + /** + * @notice Returns the hash of the governance address. + * @return bytes32 The hash of the governance address + */ + function governanceAddressHash() external view returns (bytes32); + + /** + * @notice Returns the ETA of a proposal. + * @param target The address of the contract targeted by the proposal + * @param callData The call data to be sent to the target contract + * @param nativeValue The amount of native tokens to be sent to the target contract + * @return uint256 The ETA of the proposal + */ + function getProposalEta( + address target, + bytes calldata callData, + uint256 nativeValue + ) external view returns (uint256); + + /** + * @notice Executes a governance proposal. + * @param targetContract The address of the contract targeted by the proposal + * @param callData The call data to be sent to the target contract + * @param value The amount of ETH to be sent to the target contract + */ + function executeProposal( + address targetContract, + bytes calldata callData, + uint256 value + ) external payable; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IMintableCappedERC20.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IMintableCappedERC20.sol new file mode 100644 index 0000000000..a72ddc0801 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IMintableCappedERC20.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import { IERC20 } from './IERC20.sol'; +import { IERC20Permit } from './IERC20Permit.sol'; +import { IOwnable } from './IOwnable.sol'; + +interface IMintableCappedERC20 is IERC20, IERC20Permit, IOwnable { + error CapExceeded(); + + function cap() external view returns (uint256); + + function mint(address account, uint256 amount) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IMultisig.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IMultisig.sol new file mode 100644 index 0000000000..5e38a729a8 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IMultisig.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IMultisigBase } from './IMultisigBase.sol'; +import { ICaller } from './ICaller.sol'; + +/** + * @title IMultisig Interface + * @notice This interface extends IMultisigBase by adding an execute function for multisignature transactions. + */ +interface IMultisig is ICaller, IMultisigBase { + /** + * @notice Executes a function on an external target. + * @param target The address of the target to call + * @param callData The call data to be sent + * @param nativeValue The native token value to be sent (e.g., ETH) + */ + function execute( + address target, + bytes calldata callData, + uint256 nativeValue + ) external payable; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IMultisigBase.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IMultisigBase.sol new file mode 100644 index 0000000000..e1d296b2e3 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IMultisigBase.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @title IMultisigBase Interface + * @notice An interface defining the base operations for a multisignature contract. + */ +interface IMultisigBase { + error NotSigner(); + error AlreadyVoted(); + error InvalidSigners(); + error InvalidSignerThreshold(); + error DuplicateSigner(address account); + + /**********\ + |* Events *| + \**********/ + + event MultisigOperationExecuted(bytes32 indexed operationHash); + + event SignersRotated(address[] newAccounts, uint256 newThreshold); + + /***********\ + |* Getters *| + \***********/ + + /** + * @notice Gets the current epoch. + * @return uint The current epoch + */ + function signerEpoch() external view returns (uint256); + + /** + * @notice Gets the threshold of current signers. + * @return uint The threshold number + */ + function signerThreshold() external view returns (uint256); + + /** + * @notice Gets the array of current signers. + * @return array of signer addresses + */ + function signerAccounts() external view returns (address[] memory); + + /***********\ + |* Setters *| + \***********/ + + /** + * @notice Update the signers and threshold for the multisig contract. + * @param newAccounts The array of new signers + * @param newThreshold The new threshold of signers required + */ + function rotateSigners(address[] memory newAccounts, uint256 newThreshold) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IOwnable.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IOwnable.sol new file mode 100644 index 0000000000..dc07aacd79 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IOwnable.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +interface IOwnable { + error NotOwner(); + error InvalidOwner(); + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + function owner() external view returns (address); + + function transferOwnership(address newOwner) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/ITokenDeployer.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/ITokenDeployer.sol new file mode 100644 index 0000000000..683e90dc93 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/ITokenDeployer.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +interface ITokenDeployer { + function deployToken( + string calldata name, + string calldata symbol, + uint8 decimals, + uint256 cap, + bytes32 salt + ) external returns (address tokenAddress); +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IUpgradable.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IUpgradable.sol new file mode 100644 index 0000000000..ad1d1ab1fc --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IUpgradable.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +// General interface for upgradable contracts +interface IUpgradable { + error NotOwner(); + error InvalidOwner(); + error InvalidCodeHash(); + error InvalidImplementation(); + error SetupFailed(); + error NotProxy(); + + event Upgraded(address indexed newImplementation); + event OwnershipTransferred(address indexed newOwner); + + // Get current owner + function owner() external view returns (address); + + function contractId() external pure returns (bytes32); + + function implementation() external view returns (address); + + function upgrade( + address newImplementation, + bytes32 newImplementationCodeHash, + bytes calldata params + ) external; + + function setup(bytes calldata data) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IWETH9.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IWETH9.sol new file mode 100644 index 0000000000..a33b670bec --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/interfaces/IWETH9.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.9; + +import { IERC20 } from './IERC20.sol'; + +// WETH9 specific interface +interface IWETH9 is IERC20 { + function deposit() external payable; + + function withdraw(uint256 amount) external; +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/Target.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/Target.sol new file mode 100644 index 0000000000..948cef524f --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/Target.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +contract Target { + event TargetCalled(); + + function callTarget() external payable { + emit TargetCalled(); + } + + receive() external payable {} +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestInterchainGovernance.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestInterchainGovernance.sol new file mode 100644 index 0000000000..bea942c2eb --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestInterchainGovernance.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { InterchainGovernance } from '../governance/InterchainGovernance.sol'; + +contract TestInterchainGovernance is InterchainGovernance { + constructor( + address gatewayAddress, + string memory governanceChain_, + string memory governanceAddress_, + uint256 minimumTimeDelay + ) InterchainGovernance(gatewayAddress, governanceChain_, governanceAddress_, minimumTimeDelay) {} + + function executeProposalAction( + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload + ) external { + _execute(sourceChain, sourceAddress, payload); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestMultiSigBase.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestMultiSigBase.sol new file mode 100644 index 0000000000..23c8a719eb --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestMultiSigBase.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { MultisigBase } from '../auth/MultisigBase.sol'; + +contract TestMultiSigBase is MultisigBase { + constructor(address[] memory accounts, uint256 threshold) MultisigBase(accounts, threshold) {} +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestServiceGovernance.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestServiceGovernance.sol new file mode 100644 index 0000000000..08b443c1d6 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestServiceGovernance.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { AxelarServiceGovernance } from '../governance/AxelarServiceGovernance.sol'; + +contract TestServiceGovernance is AxelarServiceGovernance { + constructor( + address gateway, + string memory governanceChain_, + string memory governanceAddress_, + uint256 minimumTimeDelay, + address[] memory signers, + uint256 threshold + ) AxelarServiceGovernance(gateway, governanceChain_, governanceAddress_, minimumTimeDelay, signers, threshold) {} + + function executeProposalAction( + string calldata sourceChain, + string calldata sourceAddress, + bytes calldata payload + ) external { + _execute(sourceChain, sourceAddress, payload); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestWeth.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestWeth.sol new file mode 100644 index 0000000000..00b480d4fa --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/TestWeth.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { MintableCappedERC20 } from '../MintableCappedERC20.sol'; +import { IWETH9 } from '../interfaces/IWETH9.sol'; + +contract TestWeth is MintableCappedERC20, IWETH9 { + constructor( + string memory name, + string memory symbol, + uint8 decimals, + uint256 capacity + ) MintableCappedERC20(name, symbol, decimals, capacity) {} + + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + function withdraw(uint256 amount) external { + require(balanceOf[msg.sender] >= amount, 'Insufficient balance'); + balanceOf[msg.sender] -= amount; + payable(msg.sender).transfer(amount); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/gmp/DestinationChainSwapExecutable.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/gmp/DestinationChainSwapExecutable.sol new file mode 100644 index 0000000000..76afd8316f --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/gmp/DestinationChainSwapExecutable.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { AxelarExecutable } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol'; +import { IERC20 } from '../../interfaces/IERC20.sol'; +import { DestinationChainTokenSwapper } from './DestinationChainTokenSwapper.sol'; + +contract DestinationChainSwapExecutable is AxelarExecutable { + DestinationChainTokenSwapper public immutable swapper; + + constructor(address gatewayAddress, address swapperAddress) AxelarExecutable(gatewayAddress) { + swapper = DestinationChainTokenSwapper(swapperAddress); + } + + function _executeWithToken( + string calldata sourceChain, + string calldata, + bytes calldata payload, + string calldata tokenSymbolA, + uint256 amount + ) internal override { + (string memory tokenSymbolB, string memory recipient) = abi.decode(payload, (string, string)); + + address tokenA = gateway.tokenAddresses(tokenSymbolA); + address tokenB = gateway.tokenAddresses(tokenSymbolB); + + IERC20(tokenA).approve(address(swapper), amount); + uint256 convertedAmount = swapper.swap(tokenA, tokenB, amount, address(this)); + + IERC20(tokenB).approve(address(gateway), convertedAmount); + gateway.sendToken(sourceChain, recipient, tokenSymbolB, convertedAmount); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/gmp/DestinationChainTokenSwapper.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/gmp/DestinationChainTokenSwapper.sol new file mode 100644 index 0000000000..1fbcafd5c5 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/gmp/DestinationChainTokenSwapper.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IERC20 } from '../../interfaces/IERC20.sol'; + +contract DestinationChainTokenSwapper { + error WrongTokenPair(); + + address public tokenA; + address public tokenB; + + constructor(address tokenA_, address tokenB_) { + tokenA = tokenA_; + tokenB = tokenB_; + } + + function swap( + address tokenAddress, + address toTokenAddress, + uint256 amount, + address recipient + ) external returns (uint256 convertedAmount) { + IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + + if (tokenAddress == tokenA) { + if (toTokenAddress != tokenB) revert WrongTokenPair(); + + convertedAmount = amount * 2; + } else { + if (tokenAddress != tokenB || toTokenAddress != tokenA) revert WrongTokenPair(); + + convertedAmount = amount / 2; + } + + IERC20(toTokenAddress).transfer(recipient, convertedAmount); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/gmp/SourceChainSwapCaller.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/gmp/SourceChainSwapCaller.sol new file mode 100644 index 0000000000..8bb5300191 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/test/gmp/SourceChainSwapCaller.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IERC20 } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IERC20.sol'; +import { IAxelarGateway } from '../../interfaces/IAxelarGateway.sol'; +import { IAxelarGasService } from '../../interfaces/IAxelarGasService.sol'; + +contract SourceChainSwapCaller { + IAxelarGateway public gateway; + IAxelarGasService public gasService; + string public destinationChain; + string public executableAddress; + + constructor( + address gateway_, + address gasService_, + string memory destinationChain_, + string memory executableAddress_ + ) { + gateway = IAxelarGateway(gateway_); + gasService = IAxelarGasService(gasService_); + destinationChain = destinationChain_; + executableAddress = executableAddress_; + } + + function swapToken( + string memory symbolA, + string memory symbolB, + uint256 amount, + string memory recipient + ) external payable { + address tokenX = gateway.tokenAddresses(symbolA); + bytes memory payload = abi.encode(symbolB, recipient); + + IERC20(tokenX).transferFrom(msg.sender, address(this), amount); + + if (msg.value > 0) + gasService.payNativeGasForContractCallWithToken{ value: msg.value }( + address(this), + destinationChain, + executableAddress, + payload, + symbolA, + amount, + msg.sender + ); + + IERC20(tokenX).approve(address(gateway), amount); + gateway.callContractWithToken(destinationChain, executableAddress, payload, symbolA, amount); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/util/Caller.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/util/Caller.sol new file mode 100644 index 0000000000..02f1dd5ab3 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/util/Caller.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { ICaller } from '../interfaces/ICaller.sol'; + +contract Caller is ICaller { + /** + * @dev Calls a target address with specified calldata and optionally sends value. + */ + function _call( + address target, + bytes calldata callData, + uint256 nativeValue + ) internal { + if (nativeValue > address(this).balance) revert InsufficientBalance(); + + (bool success, ) = target.call{ value: nativeValue }(callData); + if (!success) { + revert ExecutionFailed(); + } + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/util/Proxy.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/util/Proxy.sol new file mode 100644 index 0000000000..98a6796031 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/util/Proxy.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.9; + +import { IUpgradable } from '../interfaces/IUpgradable.sol'; + +contract Proxy { + error InvalidImplementation(); + error SetupFailed(); + error EtherNotAccepted(); + error NotOwner(); + error AlreadyInitialized(); + + // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + // keccak256('owner') + bytes32 internal constant _OWNER_SLOT = 0x02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c0; + + constructor() { + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(_OWNER_SLOT, caller()) + } + } + + function init( + address implementationAddress, + address newOwner, + bytes memory params + ) external { + address owner; + // solhint-disable-next-line no-inline-assembly + assembly { + owner := sload(_OWNER_SLOT) + } + if (msg.sender != owner) revert NotOwner(); + if (implementation() != address(0)) revert AlreadyInitialized(); + if (IUpgradable(implementationAddress).contractId() != contractId()) revert InvalidImplementation(); + + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(_IMPLEMENTATION_SLOT, implementationAddress) + sstore(_OWNER_SLOT, newOwner) + } + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = implementationAddress.delegatecall( + //0x9ded06df is the setup selector. + abi.encodeWithSelector(0x9ded06df, params) + ); + if (!success) revert SetupFailed(); + } + + // solhint-disable-next-line no-empty-blocks + function contractId() internal pure virtual returns (bytes32) {} + + function implementation() public view returns (address implementation_) { + // solhint-disable-next-line no-inline-assembly + assembly { + implementation_ := sload(_IMPLEMENTATION_SLOT) + } + } + + // solhint-disable-next-line no-empty-blocks + function setup(bytes calldata data) public {} + + // solhint-disable-next-line no-complex-fallback + fallback() external payable { + address implementaion_ = implementation(); + // solhint-disable-next-line no-inline-assembly + assembly { + calldatacopy(0, 0, calldatasize()) + + let result := delegatecall(gas(), implementaion_, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } + + receive() external payable virtual { + revert EtherNotAccepted(); + } +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/util/Upgradable.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/util/Upgradable.sol new file mode 100644 index 0000000000..50ef24bb71 --- /dev/null +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/contracts/util/Upgradable.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IUpgradable } from '../interfaces/IUpgradable.sol'; + +abstract contract Upgradable is IUpgradable { + // bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + // keccak256('owner') + bytes32 internal constant _OWNER_SLOT = 0x02016836a56b71f0d02689e69e326f4f4c1b9057164ef592671cf0d37c8040c0; + + modifier onlyOwner() { + if (owner() != msg.sender) revert NotOwner(); + _; + } + + function owner() public view returns (address owner_) { + assembly { + owner_ := sload(_OWNER_SLOT) + } + } + + function transferOwnership(address newOwner) external virtual onlyOwner { + if (newOwner == address(0)) revert InvalidOwner(); + + emit OwnershipTransferred(newOwner); + + assembly { + sstore(_OWNER_SLOT, newOwner) + } + } + + function implementation() public view returns (address implementation_) { + assembly { + implementation_ := sload(_IMPLEMENTATION_SLOT) + } + } + + function upgrade( + address newImplementation, + bytes32 newImplementationCodeHash, + bytes calldata params + ) external override onlyOwner { + if (IUpgradable(newImplementation).contractId() != IUpgradable(this).contractId()) revert InvalidImplementation(); + if (newImplementationCodeHash != newImplementation.codehash) revert InvalidCodeHash(); + + if (params.length > 0) { + (bool success, ) = newImplementation.delegatecall(abi.encodeWithSelector(this.setup.selector, params)); + + if (!success) revert SetupFailed(); + } + + emit Upgraded(newImplementation); + + assembly { + sstore(_IMPLEMENTATION_SLOT, newImplementation) + } + } + + function setup(bytes calldata data) external override { + // Prevent setup from being called on the implementation + if (implementation() == address(0)) revert NotProxy(); + + _setup(data); + } + + function _setup(bytes calldata data) internal virtual {} +} diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarForecallable.sol b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarForecallable.sol deleted file mode 100644 index ecaaafef86..0000000000 --- a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/IAxelarForecallable.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.9; - -import { IAxelarGateway } from './IAxelarGateway.sol'; -import { IERC20 } from './IERC20.sol'; - -abstract contract IAxelarForecallable { - error NotApprovedByGateway(); - error AlreadyForecalled(); - error TransferFailed(); - - IAxelarGateway public gateway; - mapping(bytes32 => address) forecallers; - - constructor(address gatewayAddress) { - gateway = IAxelarGateway(gatewayAddress); - } - - function forecall( - string calldata sourceChain, - string calldata sourceAddress, - bytes calldata payload, - address forecaller - ) external { - _checkForecall(sourceChain, sourceAddress, payload, forecaller); - if (forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload))] != address(0)) revert AlreadyForecalled(); - forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload))] = forecaller; - _execute(sourceChain, sourceAddress, payload); - } - - function execute( - bytes32 commandId, - string calldata sourceChain, - string calldata sourceAddress, - bytes calldata payload - ) external { - bytes32 payloadHash = keccak256(payload); - if (!gateway.validateContractCall(commandId, sourceChain, sourceAddress, payloadHash)) revert NotApprovedByGateway(); - address forecaller = forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload))]; - if (forecaller != address(0)) { - forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload))] = address(0); - } else { - _execute(sourceChain, sourceAddress, payload); - } - } - - function forecallWithToken( - string calldata sourceChain, - string calldata sourceAddress, - bytes calldata payload, - string calldata tokenSymbol, - uint256 amount, - address forecaller - ) external { - address token = gateway.tokenAddresses(tokenSymbol); - uint256 amountPost = amountPostFee(amount, payload); - _safeTransferFrom(token, msg.sender, amountPost); - _checkForecallWithToken(sourceChain, sourceAddress, payload, tokenSymbol, amount, forecaller); - if (forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload, tokenSymbol, amount))] != address(0)) - revert AlreadyForecalled(); - forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload, tokenSymbol, amount))] = forecaller; - _executeWithToken(sourceChain, sourceAddress, payload, tokenSymbol, amountPost); - } - - function executeWithToken( - bytes32 commandId, - string calldata sourceChain, - string calldata sourceAddress, - bytes calldata payload, - string calldata tokenSymbol, - uint256 amount - ) external { - bytes32 payloadHash = keccak256(payload); - if (!gateway.validateContractCallAndMint(commandId, sourceChain, sourceAddress, payloadHash, tokenSymbol, amount)) - revert NotApprovedByGateway(); - address forecaller = forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload, tokenSymbol, amount))]; - if (forecaller != address(0)) { - forecallers[keccak256(abi.encode(sourceChain, sourceAddress, payload, tokenSymbol, amount))] = address(0); - address token = gateway.tokenAddresses(tokenSymbol); - _safeTransfer(token, forecaller, amount); - } else { - _executeWithToken(sourceChain, sourceAddress, payload, tokenSymbol, amount); - } - } - - function _execute( - string memory sourceChain, - string memory sourceAddress, - bytes calldata payload - ) internal virtual {} - - function _executeWithToken( - string memory sourceChain, - string memory sourceAddress, - bytes calldata payload, - string memory tokenSymbol, - uint256 amount - ) internal virtual {} - - // Override this to keep a fee. - function amountPostFee( - uint256 amount, - bytes calldata /*payload*/ - ) public virtual returns (uint256) { - return amount; - } - - // Override this and revert if you want to only allow certain people/calls to be able to forecall. - function _checkForecall( - string calldata sourceChain, - string calldata sourceAddress, - bytes calldata payload, - address forecaller - ) internal virtual {} - - // Override this and revert if you want to only allow certain people/calls to be able to forecall. - function _checkForecallWithToken( - string calldata sourceChain, - string calldata sourceAddress, - bytes calldata payload, - string calldata tokenSymbol, - uint256 amount, - address forecaller - ) internal virtual {} - - function _safeTransfer( - address tokenAddress, - address receiver, - uint256 amount - ) internal { - (bool success, bytes memory returnData) = tokenAddress.call(abi.encodeWithSelector(IERC20.transfer.selector, receiver, amount)); - bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool))); - - if (!transferred || tokenAddress.code.length == 0) revert TransferFailed(); - } - - function _safeTransferFrom( - address tokenAddress, - address from, - uint256 amount - ) internal { - (bool success, bytes memory returnData) = tokenAddress.call( - abi.encodeWithSelector(IERC20.transferFrom.selector, from, address(this), amount) - ); - bool transferred = success && (returnData.length == uint256(0) || abi.decode(returnData, (bool))); - - if (!transferred || tokenAddress.code.length == 0) revert TransferFailed(); - } -} \ No newline at end of file diff --git a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/lib.rs b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/lib.rs index 8635842d12..818c7c1e43 100644 --- a/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/lib.rs +++ b/pallets/connectors-gateway/connectors-gateway-axelar-precompile/src/lib.rs @@ -16,7 +16,7 @@ use fp_evm::PrecompileHandle; use frame_support::dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}; use precompile_utils::prelude::*; use sp_core::{ConstU32, Get, H160, H256, U256}; -use sp_runtime::DispatchResult; +use sp_runtime::{DispatchError, DispatchResult}; pub const MAX_SOURCE_CHAIN_BYTES: u32 = 32; pub const MAX_SOURCE_ADDRESS_BYTES: u32 = 32; @@ -45,8 +45,10 @@ where ::RuntimeOrigin: From, Axelar: Get, - ConvertSource: - sp_runtime::traits::Convert<(Vec, Vec), cfg_types::domain_address::DomainAddress>, + ConvertSource: sp_runtime::traits::Convert< + (Vec, Vec), + Result, + >, { // Mimics: // @@ -121,7 +123,7 @@ where pallet_connectors_gateway::GatewayOrigin::Local(ConvertSource::convert(( source_chain.as_bytes().to_vec(), source_address.as_bytes().to_vec(), - ))) + ))?) .into(), payload.into(), ) @@ -238,8 +240,8 @@ where let mut bytes = Vec::new(); bytes.extend_from_slice(key.as_bytes()); - // TODO: Is endnianess correct here? let mut be_bytes: [u8; 32] = [0u8; 32]; + // TODO: Is endnianess correct here? slot.to_big_endian(&mut be_bytes); bytes.extend_from_slice(&be_bytes);