diff --git a/Cargo.lock b/Cargo.lock index 9cc8ec25e..17506343e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11888,6 +11888,8 @@ dependencies = [ "hydradx-runtime", "hydradx-traits", "libsecp256k1", + "module-evm-utility-macro", + "num_enum 0.5.11", "orml-tokens", "orml-traits", "orml-unknown-tokens", diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 1e63147da..c91444843 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -131,6 +131,8 @@ polkadot-primitives = { workspace = true } polkadot-service = { workspace = true, features = ["full-node"] } polkadot-runtime-parachains = { workspace = true } rococo-runtime = { workspace = true } +module-evm-utility-macro = { workspace = true } +num_enum = { workspace = true } ethabi = { workspace = true } serde_json = { workspace = true } diff --git a/integration-tests/src/contracts.rs b/integration-tests/src/contracts.rs new file mode 100644 index 000000000..922da353d --- /dev/null +++ b/integration-tests/src/contracts.rs @@ -0,0 +1,100 @@ +use crate::evm::native_asset_ethereum_address; +use crate::polkadot_test_net::Hydra; +use crate::polkadot_test_net::TestNet; +use crate::polkadot_test_net::ALICE; +use crate::utils::contracts::deploy_contract; +use hex_literal::hex; +use hydradx_runtime::evm::Executor; +use hydradx_runtime::AccountId; +use hydradx_runtime::EVMAccounts; +use hydradx_runtime::Runtime; +use hydradx_traits::evm::CallContext; +use hydradx_traits::evm::EvmAddress; +use hydradx_traits::evm::InspectEvmAccounts; +use hydradx_traits::evm::EVM; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use pallet_evm::ExitReason::Succeed; +use sp_core::H256; +use sp_core::{RuntimeDebug, U256}; +use xcm_emulator::Network; +use xcm_emulator::TestExt; + +pub fn deployer() -> EvmAddress { + EVMAccounts::evm_address(&Into::::into(ALICE)) +} + +#[module_evm_utility_macro::generate_function_selector] +#[derive(RuntimeDebug, Eq, PartialEq, TryFromPrimitive, IntoPrimitive)] +#[repr(u32)] +pub enum Function { + IsContract = "isContract(address)", + Check = "check(address)", +} + +fn is_contract(checker: EvmAddress, address: EvmAddress) -> bool { + let mut data = Into::::into(Function::Check).to_be_bytes().to_vec(); + data.extend_from_slice(H256::from(address).as_bytes()); + let context = CallContext { + contract: checker, + sender: Default::default(), + origin: Default::default(), + }; + let (res, _) = Executor::::call(context, data, U256::zero(), 100_000); + match res { + Succeed(_) => true, + _ => false, + } +} + +#[test] +fn contract_check_succeeds_on_deployed_contract() { + TestNet::reset(); + Hydra::execute_with(|| { + let checker = deploy_contract("ContractCheck", deployer()); + + assert_eq!(is_contract(checker, checker), true); + }); +} + +#[test] +fn contract_check_fails_on_eoa() { + TestNet::reset(); + Hydra::execute_with(|| { + let checker = deploy_contract("ContractCheck", deployer()); + + assert_eq!(is_contract(checker, deployer()), false); + }); +} + +#[test] +fn contract_check_fails_on_precompile() { + TestNet::reset(); + Hydra::execute_with(|| { + let checker = deploy_contract("ContractCheck", deployer()); + + assert_eq!(is_contract(checker, native_asset_ethereum_address()), false); + }); +} + +#[test] +fn contract_check_succeeds_on_precompile_with_code() { + TestNet::reset(); + Hydra::execute_with(|| { + let checker = deploy_contract("ContractCheck", deployer()); + pallet_evm::AccountCodes::::insert( + native_asset_ethereum_address(), + &hex!["365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3"][..], + ); + assert_eq!(is_contract(checker, native_asset_ethereum_address()), true); + }); +} + +#[test] +fn contract_check_succeeds_on_precompile_with_invalid_code() { + TestNet::reset(); + Hydra::execute_with(|| { + let checker = deploy_contract("ContractCheck", deployer()); + pallet_evm::AccountCodes::::insert(native_asset_ethereum_address(), &hex!["00"][..]); + assert_eq!(is_contract(checker, native_asset_ethereum_address()), true); + }); +} diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index ace79c584..c84f26b70 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -5,6 +5,7 @@ mod asset_registry; mod bonds; mod call_filter; mod circuit_breaker; +mod contracts; mod cross_chain_transfer; mod dca; mod dust; diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index abc635d96..913b9f2f1 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -19,7 +19,7 @@ codec = { workspace = true } scale-info = { version = "2.3.1", default-features = false, features = ["derive"] } smallvec = "1.9.0" log = { workspace = true } -num_enum = { version = "0.5.1", default-features = false } +num_enum = { workspace = true } evm = { workspace = true, features = ["with-codec"] } # local dependencies diff --git a/scripts/test-contracts/artifacts/contracts/Address.sol/Address.json b/scripts/test-contracts/artifacts/contracts/Address.sol/Address.json new file mode 100644 index 000000000..d6948ada8 --- /dev/null +++ b/scripts/test-contracts/artifacts/contracts/Address.sol/Address.json @@ -0,0 +1,10 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "Address", + "sourceName": "contracts/Address.sol", + "abi": [], + "bytecode": "0x60566050600b82828239805160001a6073146043577f4e487b7100000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea2646970667358221220a3bd1167f4b64c2712fda719899ee18c248d5045fce5d06bf79bc467f1f56b1064736f6c63430008180033", + "deployedBytecode": "0x73000000000000000000000000000000000000000030146080604052600080fdfea2646970667358221220a3bd1167f4b64c2712fda719899ee18c248d5045fce5d06bf79bc467f1f56b1064736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/scripts/test-contracts/artifacts/contracts/ContractCheck.sol/ContractCheck.json b/scripts/test-contracts/artifacts/contracts/ContractCheck.sol/ContractCheck.json new file mode 100644 index 000000000..7268d734a --- /dev/null +++ b/scripts/test-contracts/artifacts/contracts/ContractCheck.sol/ContractCheck.json @@ -0,0 +1,56 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ContractCheck", + "sourceName": "contracts/ContractCheck.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "Checked", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_addr", + "type": "address" + } + ], + "name": "check", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_addr", + "type": "address" + } + ], + "name": "isContract", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x608060405234801561001057600080fd5b506102b3806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063162790551461003b578063c23697a81461006b575b600080fd5b6100556004803603810190610050919061019d565b610087565b60405161006291906101e5565b60405180910390f35b6100856004803603810190610080919061019d565b610099565b005b600061009282610127565b9050919050565b6100a281610087565b6100e1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100d89061025d565b60405180910390fd5b8073ffffffffffffffffffffffffffffffffffffffff167fe20314ef67750397f75c06a974fa978eaf8f26660fa2e4c14078f87ed2f9e8d760405160405180910390a250565b600080823b905060008111915050919050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061016a8261013f565b9050919050565b61017a8161015f565b811461018557600080fd5b50565b60008135905061019781610171565b92915050565b6000602082840312156101b3576101b261013a565b5b60006101c184828501610188565b91505092915050565b60008115159050919050565b6101df816101ca565b82525050565b60006020820190506101fa60008301846101d6565b92915050565b600082825260208201905092915050565b7f61646472206973206e6f74206120636f6e747261637400000000000000000000600082015250565b6000610247601683610200565b915061025282610211565b602082019050919050565b600060208201905081810360008301526102768161023a565b905091905056fea2646970667358221220883f143f3e31616be16bf2a0118b6259b1a91e54ed3a7dbd91abad45e20fffd664736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100365760003560e01c8063162790551461003b578063c23697a81461006b575b600080fd5b6100556004803603810190610050919061019d565b610087565b60405161006291906101e5565b60405180910390f35b6100856004803603810190610080919061019d565b610099565b005b600061009282610127565b9050919050565b6100a281610087565b6100e1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016100d89061025d565b60405180910390fd5b8073ffffffffffffffffffffffffffffffffffffffff167fe20314ef67750397f75c06a974fa978eaf8f26660fa2e4c14078f87ed2f9e8d760405160405180910390a250565b600080823b905060008111915050919050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061016a8261013f565b9050919050565b61017a8161015f565b811461018557600080fd5b50565b60008135905061019781610171565b92915050565b6000602082840312156101b3576101b261013a565b5b60006101c184828501610188565b91505092915050565b60008115159050919050565b6101df816101ca565b82525050565b60006020820190506101fa60008301846101d6565b92915050565b600082825260208201905092915050565b7f61646472206973206e6f74206120636f6e747261637400000000000000000000600082015250565b6000610247601683610200565b915061025282610211565b602082019050919050565b600060208201905081810360008301526102768161023a565b905091905056fea2646970667358221220883f143f3e31616be16bf2a0118b6259b1a91e54ed3a7dbd91abad45e20fffd664736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/scripts/test-contracts/contracts/Address.sol b/scripts/test-contracts/contracts/Address.sol new file mode 100644 index 000000000..a4f6d07c3 --- /dev/null +++ b/scripts/test-contracts/contracts/Address.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Address.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + assembly { + size := extcodesize(account) + } + return size > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, 'Address: insufficient balance'); + + (bool success,) = recipient.call{value: amount}(''); + require(success, 'Address: unable to send value, recipient may have reverted'); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain `call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, 'Address: low-level call failed'); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value + ) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, 'Address: low-level call with value failed'); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue( + address target, + bytes memory data, + uint256 value, + string memory errorMessage + ) internal returns (bytes memory) { + require(address(this).balance >= value, 'Address: insufficient balance for call'); + require(isContract(target), 'Address: call to non-contract'); + + (bool success, bytes memory returndata) = target.call{value: value}(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data + ) internal view returns (bytes memory) { + return functionStaticCall(target, data, 'Address: low-level static call failed'); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall( + address target, + bytes memory data, + string memory errorMessage + ) internal view returns (bytes memory) { + require(isContract(target), 'Address: static call to non-contract'); + + (bool success, bytes memory returndata) = target.staticcall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { + return functionDelegateCall(target, data, 'Address: low-level delegate call failed'); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a delegate call. + * + * _Available since v3.4._ + */ + function functionDelegateCall( + address target, + bytes memory data, + string memory errorMessage + ) internal returns (bytes memory) { + require(isContract(target), 'Address: delegate call to non-contract'); + + (bool success, bytes memory returndata) = target.delegatecall(data); + return verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason using the provided one. + * + * _Available since v4.3._ + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} diff --git a/scripts/test-contracts/contracts/ContractCheck.sol b/scripts/test-contracts/contracts/ContractCheck.sol new file mode 100644 index 000000000..80c718bd0 --- /dev/null +++ b/scripts/test-contracts/contracts/ContractCheck.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import "./Address.sol"; + +contract ContractCheck { + + event Checked(address indexed addr); + + function isContract(address _addr) public view returns (bool) { + return Address.isContract(_addr); + } + + function check(address _addr) public { + require(isContract(_addr), 'addr is not a contract'); + emit Checked(_addr); + } +} diff --git a/scripts/test-contracts/hardhat.config.js b/scripts/test-contracts/hardhat.config.js index 5e3bfbe2a..f10169b97 100644 --- a/scripts/test-contracts/hardhat.config.js +++ b/scripts/test-contracts/hardhat.config.js @@ -1,7 +1,7 @@ require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-verify"); const {vars} = require("hardhat/config"); -const PRIVKEY = vars.get("PRIVKEY"); +const PRIVKEY = vars.get("PRIVKEY", "42d8d953e4f9246093a33e9ca6daa078501012f784adfe4bbed57918ff13be14"); /** @type import('hardhat/config').HardhatUserConfig */ module.exports = {