diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 625a683ace..49fe2882fc 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -100,6 +100,8 @@ dependencies = [ "lazy_static", "primitive-types", "serde_json", + "sha3", + "sp-core 20.0.0", ] [[package]] @@ -197,6 +199,7 @@ dependencies = [ name = "ain-rs-exports" version = "0.1.0" dependencies = [ + "ain-contracts", "ain-evm", "ain-grpc", "anyhow", diff --git a/lib/ain-contracts/Cargo.toml b/lib/ain-contracts/Cargo.toml index b5d790cd11..28ee5e1a44 100644 --- a/lib/ain-contracts/Cargo.toml +++ b/lib/ain-contracts/Cargo.toml @@ -12,6 +12,8 @@ ethereum = "0.14.0" serde_json = "1.0.96" hex = "0.4.3" lazy_static = "1.4" +sha3 = "0.10.6" +sp-core = "20.0.0" [build-dependencies] ethers = "2.0.7" diff --git a/lib/ain-contracts/build.rs b/lib/ain-contracts/build.rs index 7187023c42..d6cf7001a7 100644 --- a/lib/ain-contracts/build.rs +++ b/lib/ain-contracts/build.rs @@ -1,47 +1,54 @@ use anyhow::anyhow; -use ethers_solc::{Project, ProjectPathsConfig}; +use ethers_solc::{Project, ProjectPathsConfig, Solc}; +use std::env; use std::fs; use std::path::PathBuf; fn main() -> Result<(), Box> { // compile solidity project // configure `root` as our project root - let root = PathBuf::from("counter_contract"); - if !root.exists() { - return Err("Project root {root:?} does not exists!".into()); - } + let contracts = vec![("counter_contract", "Counter"), ("dst20", "DST20")]; - let paths = ProjectPathsConfig::builder() - .root(&root) - .sources(&root) - .build()?; - - let project = Project::builder() - .paths(paths) - .set_auto_detect(true) - .no_artifacts() - .build()?; - let output = project.compile().unwrap(); - let artifacts = output.into_artifacts(); - - for (id, artifact) in artifacts { - if id.name == "Counter" { - let abi = artifact.abi.ok_or_else(|| anyhow!("ABI not found"))?; - let bytecode = artifact.deployed_bytecode.expect("No bytecode found"); - - fs::create_dir_all("counter_contract/output/")?; - fs::write( - PathBuf::from("counter_contract/output/bytecode.json"), - serde_json::to_string(&bytecode).unwrap().as_bytes(), - )?; - fs::write( - PathBuf::from("counter_contract/output/abi.json"), - serde_json::to_string(&abi).unwrap().as_bytes(), - )?; + for (file_path, contract_name) in contracts { + let solc = Solc::new(env::var("SOLC_PATH")?); + let root = PathBuf::from(file_path); + if !root.exists() { + return Err("Project root {root:?} does not exists!".into()); } - } - project.rerun_if_sources_changed(); + let paths = ProjectPathsConfig::builder() + .root(&root) + .sources(&root) + .build()?; + + let project = Project::builder() + .solc(solc) + .paths(paths) + .set_auto_detect(true) + .no_artifacts() + .build()?; + let output = project.compile().unwrap(); + let artifacts = output.into_artifacts(); + + for (id, artifact) in artifacts { + if id.name == contract_name { + let abi = artifact.abi.ok_or_else(|| anyhow!("ABI not found"))?; + let bytecode = artifact.deployed_bytecode.expect("No bytecode found"); + + fs::create_dir_all(format!("{file_path}/output/"))?; + fs::write( + PathBuf::from(format!("{file_path}/output/bytecode.json")), + serde_json::to_string(&bytecode).unwrap().as_bytes(), + )?; + fs::write( + PathBuf::from(format!("{file_path}/output/abi.json")), + serde_json::to_string(&abi).unwrap().as_bytes(), + )?; + } + } + + project.rerun_if_sources_changed(); + } Ok(()) } diff --git a/lib/ain-contracts/counter_contract/Counter.sol b/lib/ain-contracts/counter_contract/Counter.sol index 9400207c37..4234d75bc5 100644 --- a/lib/ain-contracts/counter_contract/Counter.sol +++ b/lib/ain-contracts/counter_contract/Counter.sol @@ -7,4 +7,4 @@ pragma solidity >=0.8.2 <0.9.0; */ contract Counter { uint256 public number; -} \ No newline at end of file +} diff --git a/lib/ain-contracts/dst20/ERC20.sol b/lib/ain-contracts/dst20/ERC20.sol new file mode 100644 index 0000000000..b494acce4b --- /dev/null +++ b/lib/ain-contracts/dst20/ERC20.sol @@ -0,0 +1,515 @@ + +// File: @openzeppelin/contracts@4.9.2/utils/Context.sol + + +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } +} + +// File: @openzeppelin/contracts@4.9.2/token/ERC20/IERC20.sol + + +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.0; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} + +// File: @openzeppelin/contracts@4.9.2/token/ERC20/extensions/IERC20Metadata.sol + + +// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) + +pragma solidity ^0.8.0; + + +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + * + * _Available since v4.1._ + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} + +// File: @openzeppelin/contracts@4.9.2/token/ERC20/ERC20.sol + + +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.0; + + + + +/** + * @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.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead 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 Context, IERC20, IERC20Metadata { + mapping(address => uint256) private _balances; + + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @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) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, 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}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - the caller must have allowance for ``from``'s tokens of at least + * `amount`. + */ + function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, 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) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, allowance(owner, 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) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = allowance(owner, spender); + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This 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: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + */ + function _transfer(address from, address to, uint256 amount) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, 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: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(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 { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(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 { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require(currentAllowance >= amount, "ERC20: insufficient allowance"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} +} + +// File: oz.sol + + +pragma solidity ^0.8.9; + + +contract DST20 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} +} diff --git a/lib/ain-contracts/src/lib.rs b/lib/ain-contracts/src/lib.rs index 99446592e3..407b3df0ff 100644 --- a/lib/ain-contracts/src/lib.rs +++ b/lib/ain-contracts/src/lib.rs @@ -1,6 +1,8 @@ use anyhow::anyhow; use lazy_static::lazy_static; use primitive_types::{H160, H256, U256}; +use sha3::{Digest, Keccak256}; +use sp_core::{Blake2Hasher, Hasher}; use std::collections::HashMap; use std::error::Error; use std::str::FromStr; @@ -22,6 +24,25 @@ pub fn get_abi_encoded_string(input: &str) -> H256 { storage_value } +pub fn get_address_storage_index(address: H160) -> H256 { + // padded slot, slot for our contract is 0 + let slot = H256::zero(); + + // padded key + let key = H256::from(address); + + // keccak256(padded key + padded slot) + let mut hasher = Keccak256::new(); + hasher.update(key.as_fixed_bytes()); + hasher.update(slot.as_fixed_bytes()); + let hash_result = hasher.finalize(); + + let mut index_bytes = [0u8; 32]; + index_bytes.copy_from_slice(&hash_result); + + H256::from(index_bytes) +} + pub fn get_bytecode(input: &str) -> Result, Box> { let bytecode_json: serde_json::Value = serde_json::from_str(input)?; let bytecode_raw = bytecode_json["object"] @@ -35,6 +56,23 @@ pub fn get_counter_bytecode() -> Result, Box> { get_bytecode(include_str!("../counter_contract/output/bytecode.json")) } +pub fn get_dst20_bytecode() -> Result, Box> { + get_bytecode(include_str!("../dst20/output/bytecode.json")) +} + +pub fn get_dst20_codehash() -> Result> { + let bytecode = get_bytecode(include_str!("../dst20/output/bytecode.json"))?; + Ok(Blake2Hasher::hash(&bytecode)) +} + +pub fn dst20_address_from_token_id(token_id: &str) -> Result> { + let number_str = format!("{:x}", token_id.parse::()?); + let padded_number_str = format!("{:0>38}", number_str); + let final_str = format!("ff{}", padded_number_str); + + Ok(H160::from_str(&final_str)?) +} + #[derive(Debug, PartialEq, Eq, Hash)] pub enum Contracts { CounterContract, diff --git a/lib/ain-evm/src/core.rs b/lib/ain-evm/src/core.rs index 1bb32c9f56..61b2646c0e 100644 --- a/lib/ain-evm/src/core.rs +++ b/lib/ain-evm/src/core.rs @@ -7,7 +7,7 @@ use crate::receipt::ReceiptService; use crate::services::SERVICES; use crate::storage::traits::{BlockStorage, PersistentStateError}; use crate::storage::Storage; -use crate::transaction::bridge::{BalanceUpdate, BridgeTx}; +use crate::transaction::system::{BalanceUpdate, SystemTx}; use crate::trie::TrieDBStore; use crate::txqueue::{QueueError, QueueTx, TransactionQueueMap}; use crate::{ @@ -15,6 +15,7 @@ use crate::{ traits::{Executor, ExecutorContext}, transaction::SignedTx, }; +use primitive_types::H256; use ethereum::{AccessList, Account, Block, Log, PartialHeader, TransactionV2}; use ethereum_types::{Bloom, BloomInput, H160, U256}; @@ -286,7 +287,7 @@ impl EVMCoreService { amount: U256, hash: NativeTxHash, ) -> Result<(), EVMError> { - let queue_tx = QueueTx::BridgeTx(BridgeTx::EvmIn(BalanceUpdate { address, amount })); + let queue_tx = QueueTx::SystemTx(SystemTx::EvmIn(BalanceUpdate { address, amount })); self.tx_queues .queue_tx(queue_id, queue_tx, hash, U256::zero(), U256::zero())?; Ok(()) @@ -312,7 +313,7 @@ impl EVMCoreService { }) .into()) } else { - let queue_tx = QueueTx::BridgeTx(BridgeTx::EvmOut(BalanceUpdate { address, amount })); + let queue_tx = QueueTx::SystemTx(SystemTx::EvmOut(BalanceUpdate { address, amount })); self.tx_queues .queue_tx(queue_id, queue_tx, hash, U256::zero(), U256::zero())?; Ok(()) @@ -405,7 +406,7 @@ impl EVMCoreService { pub fn get_latest_contract_storage( &self, contract: H160, - storage_index: U256, + storage_index: H256, ) -> Result { let (_, block_number) = SERVICES .evm @@ -426,12 +427,8 @@ impl EVMCoreService { Vicinity::default(), )?; - // convert U256 to H256 - let tmp: &mut [u8; 32] = &mut [0; 32]; - storage_index.to_big_endian(tmp); - backend - .get_contract_storage(contract, tmp.as_slice()) + .get_contract_storage(contract, storage_index.as_bytes()) .map_err(|e| EVMError::TrieError(e.to_string())) } diff --git a/lib/ain-evm/src/evm.rs b/lib/ain-evm/src/evm.rs index f65f9bf81c..1e05034d56 100644 --- a/lib/ain-evm/src/evm.rs +++ b/lib/ain-evm/src/evm.rs @@ -9,7 +9,6 @@ use crate::receipt::ReceiptService; use crate::storage::traits::BlockStorage; use crate::storage::Storage; use crate::traits::Executor; -use crate::transaction::bridge::{BalanceUpdate, BridgeTx}; use crate::transaction::SignedTx; use crate::trie::GENESIS_STATE_ROOT; use crate::txqueue::QueueTx; @@ -19,6 +18,7 @@ use ethereum_types::{Bloom, H160, H64, U256}; use crate::bytes::Bytes; use crate::services::SERVICES; +use crate::transaction::system::{BalanceUpdate, DST20Data, DeployContractData, SystemTx}; use ain_contracts::{Contracts, CONTRACT_ADDRESSES}; use anyhow::anyhow; use hex::FromHex; @@ -44,12 +44,17 @@ pub struct FinalizedBlockInfo { pub total_priority_fees: U256, } -pub struct CounterContractInfo { +pub struct DeployContractInfo { pub address: H160, pub storage: Vec<(H256, H256)>, pub bytecode: Bytes, } +pub struct DST20BridgeInfo { + pub address: H160, + pub storage: Vec<(H256, H256)>, +} + impl EVMServices { /// Constructs a new Handlers instance. Depending on whether the defid -ethstartstate flag is set, /// it either revives the storage from a previously saved state or initializes new storage using input from a JSON file. @@ -153,15 +158,17 @@ impl EVMServices { let mut executor = AinExecutor::new(&mut backend); + // Ensure that state root changes by updating counter contract storage if current_block_number == U256::zero() { - let CounterContractInfo { + // Deploy contract on the first block + let DeployContractInfo { address, storage, bytecode, } = EVMServices::counter_contract()?; executor.deploy_contract(address, bytecode, storage)?; } else { - let CounterContractInfo { + let DeployContractInfo { address, storage, .. } = EVMServices::counter_contract()?; executor.update_storage(address, storage)?; @@ -203,7 +210,7 @@ impl EVMServices { EVMCoreService::logs_bloom(logs, &mut logs_bloom); receipts_v3.push(receipt); } - QueueTx::BridgeTx(BridgeTx::EvmIn(BalanceUpdate { address, amount })) => { + QueueTx::SystemTx(SystemTx::EvmIn(BalanceUpdate { address, amount })) => { debug!( "[finalize_block] EvmIn for address {:x?}, amount: {}, queue_id {}", address, amount, queue_id @@ -213,7 +220,7 @@ impl EVMServices { failed_transactions.push(hex::encode(queue_item.tx_hash)); } } - QueueTx::BridgeTx(BridgeTx::EvmOut(BalanceUpdate { address, amount })) => { + QueueTx::SystemTx(SystemTx::EvmOut(BalanceUpdate { address, amount })) => { debug!( "[finalize_block] EvmOut for address {}, amount: {}", address, amount @@ -224,6 +231,50 @@ impl EVMServices { failed_transactions.push(hex::encode(queue_item.tx_hash)); } } + QueueTx::SystemTx(SystemTx::DeployContract(DeployContractData { + name, + symbol, + address, + })) => { + debug!( + "[finalize_block] DeployContract for address {}, name {}, symbol {}", + address, name, symbol + ); + + let DeployContractInfo { + address, + bytecode, + storage, + } = EVMServices::dst20_contract(&mut executor, address, name, symbol)?; + + if let Err(e) = executor.deploy_contract(address, bytecode, storage) { + debug!("[finalize_block] EvmOut failed with {e}"); + } + } + QueueTx::SystemTx(SystemTx::DST20Bridge(DST20Data { + to, + contract, + amount, + out, + })) => { + debug!( + "[finalize_block] DST20Bridge for to {}, contract {}, amount {}, out {}", + to, contract, amount, out + ); + + match EVMServices::bridge_dst20(&mut executor, contract, to, amount, out) { + Ok(DST20BridgeInfo { address, storage }) => { + if let Err(e) = executor.update_storage(address, storage) { + debug!("[finalize_block] EvmOut failed with {e}"); + failed_transactions.push(hex::encode(queue_item.tx_hash)); + } + } + Err(e) => { + debug!("[finalize_block] EvmOut failed with {e}"); + failed_transactions.push(hex::encode(queue_item.tx_hash)); + } + } + } } executor.commit(); @@ -363,17 +414,17 @@ impl EVMServices { } /// Returns address, bytecode and storage with incremented count for the counter contract - pub fn counter_contract() -> Result> { + pub fn counter_contract() -> Result> { let address = *CONTRACT_ADDRESSES.get(&Contracts::CounterContract).unwrap(); let bytecode = ain_contracts::get_counter_bytecode()?; let count = SERVICES .evm .core - .get_latest_contract_storage(address, U256::one())?; + .get_latest_contract_storage(address, ain_contracts::u256_to_h256(U256::one()))?; debug!("Count: {:#x}", count + U256::one()); - Ok(CounterContractInfo { + Ok(DeployContractInfo { address, bytecode: Bytes::from(bytecode), storage: vec![( @@ -382,4 +433,67 @@ impl EVMServices { )], }) } + + pub fn dst20_contract( + executor: &mut AinExecutor, + address: H160, + name: String, + symbol: String, + ) -> Result> { + if executor.backend.get_account(&address).is_some() { + return Err(anyhow!("Token address is already in use").into()); + } + + let bytecode = ain_contracts::get_dst20_bytecode()?; + let storage = vec![ + ( + H256::from_low_u64_be(3), + ain_contracts::get_abi_encoded_string(name.as_str()), + ), + ( + H256::from_low_u64_be(4), + ain_contracts::get_abi_encoded_string(symbol.as_str()), + ), + ]; + + Ok(DeployContractInfo { + address, + bytecode: Bytes::from(bytecode), + storage, + }) + } + + pub fn bridge_dst20( + executor: &mut AinExecutor, + contract: H160, + to: H160, + amount: U256, + out: bool, + ) -> Result> { + // check if code of address matches DST20 bytecode + let account = executor + .backend + .get_account(&contract) + .ok_or_else(|| anyhow!("DST20 token address is not a contract"))?; + + if account.code_hash != ain_contracts::get_dst20_codehash()? { + return Err(anyhow!("DST20 token code is not valid").into()); + } + + let storage_index = ain_contracts::get_address_storage_index(to); + let balance = executor + .backend + .get_contract_storage(contract, storage_index.as_bytes())?; + + let new_balance = match out { + true => balance.checked_sub(amount), + false => balance.checked_add(amount), + } + .ok_or_else(|| anyhow!("Balance overflow/underflow"))?; + + Ok(DST20BridgeInfo { + address: contract, + storage: vec![(storage_index, ain_contracts::u256_to_h256(new_balance))], + }) + } } diff --git a/lib/ain-evm/src/executor.rs b/lib/ain-evm/src/executor.rs index 8d274b39f3..7e2b37b183 100644 --- a/lib/ain-evm/src/executor.rs +++ b/lib/ain-evm/src/executor.rs @@ -18,7 +18,7 @@ use log::trace; use primitive_types::{H160, H256}; pub struct AinExecutor<'backend> { - backend: &'backend mut EVMBackend, + pub backend: &'backend mut EVMBackend, } impl<'backend> AinExecutor<'backend> { diff --git a/lib/ain-evm/src/lib.rs b/lib/ain-evm/src/lib.rs index 4a49b436fb..048553e710 100644 --- a/lib/ain-evm/src/lib.rs +++ b/lib/ain-evm/src/lib.rs @@ -17,5 +17,5 @@ pub mod storage; pub mod traits; pub mod transaction; mod trie; -mod txqueue; +pub mod txqueue; pub mod weiamount; diff --git a/lib/ain-evm/src/transaction/bridge.rs b/lib/ain-evm/src/transaction/bridge.rs deleted file mode 100644 index 6ff4fb7d13..0000000000 --- a/lib/ain-evm/src/transaction/bridge.rs +++ /dev/null @@ -1,22 +0,0 @@ -use primitive_types::{H160, U256}; - -#[derive(Debug, Clone)] -pub struct BalanceUpdate { - pub address: H160, - pub amount: U256, -} - -#[derive(Debug, Clone)] -pub enum BridgeTx { - EvmIn(BalanceUpdate), - EvmOut(BalanceUpdate), -} - -impl BridgeTx { - pub fn sender(&self) -> H160 { - match self { - BridgeTx::EvmIn(tx) => tx.address, - BridgeTx::EvmOut(tx) => tx.address, - } - } -} diff --git a/lib/ain-evm/src/transaction/mod.rs b/lib/ain-evm/src/transaction/mod.rs index 5feb39a869..14146215a7 100644 --- a/lib/ain-evm/src/transaction/mod.rs +++ b/lib/ain-evm/src/transaction/mod.rs @@ -1,4 +1,4 @@ -pub mod bridge; +pub mod system; use crate::ecrecover::{public_key_to_address, recover_public_key}; use ethereum::{ diff --git a/lib/ain-evm/src/transaction/system.rs b/lib/ain-evm/src/transaction/system.rs new file mode 100644 index 0000000000..a5d0540181 --- /dev/null +++ b/lib/ain-evm/src/transaction/system.rs @@ -0,0 +1,40 @@ +use primitive_types::{H160, U256}; + +#[derive(Debug, Clone)] +pub struct DeployContractData { + pub name: String, + pub symbol: String, + pub address: H160, +} + +#[derive(Debug, Clone)] +pub struct DST20Data { + pub to: H160, + pub contract: H160, + pub amount: U256, + pub out: bool, +} + +#[derive(Debug, Clone)] +pub struct BalanceUpdate { + pub address: H160, + pub amount: U256, +} + +#[derive(Debug, Clone)] +pub enum SystemTx { + DeployContract(DeployContractData), + DST20Bridge(DST20Data), + EvmIn(BalanceUpdate), + EvmOut(BalanceUpdate), +} + +impl SystemTx { + pub fn sender(&self) -> Option { + match self { + SystemTx::EvmIn(tx) => Some(tx.address), + SystemTx::EvmOut(tx) => Some(tx.address), + _ => None, + } + } +} diff --git a/lib/ain-evm/src/txqueue.rs b/lib/ain-evm/src/txqueue.rs index 6ff986e85b..b765ffc115 100644 --- a/lib/ain-evm/src/txqueue.rs +++ b/lib/ain-evm/src/txqueue.rs @@ -5,11 +5,8 @@ use std::{ sync::{Mutex, RwLock}, }; -use crate::{ - core::NativeTxHash, - fee::calculate_gas_fee, - transaction::{bridge::BridgeTx, SignedTx}, -}; +use crate::transaction::system::SystemTx; +use crate::{core::NativeTxHash, fee::calculate_gas_fee, transaction::SignedTx}; #[derive(Debug)] pub struct TransactionQueueMap { @@ -170,7 +167,7 @@ impl TransactionQueueMap { #[derive(Debug, Clone)] pub enum QueueTx { SignedTx(Box), - BridgeTx(BridgeTx), + SystemTx(SystemTx), } #[derive(Debug, Clone)] @@ -272,6 +269,10 @@ impl TransactionQueue { self.data.lock().unwrap().transactions.len() } + pub fn is_empty(&self) -> bool { + self.data.lock().unwrap().transactions.is_empty() + } + pub fn remove_txs_by_sender(&self, sender: H160) { let mut data = self.data.lock().unwrap(); let mut fees_to_remove = U256::zero(); @@ -279,7 +280,7 @@ impl TransactionQueue { data.transactions.retain(|item| { let tx_sender = match &item.queue_tx { QueueTx::SignedTx(tx) => tx.sender, - QueueTx::BridgeTx(tx) => tx.sender(), + QueueTx::SystemTx(tx) => tx.sender().unwrap_or_default(), }; if tx_sender == sender { fees_to_remove += item.tx_fee; diff --git a/lib/ain-rs-exports/Cargo.toml b/lib/ain-rs-exports/Cargo.toml index 041445b9f2..4fcada7d87 100644 --- a/lib/ain-rs-exports/Cargo.toml +++ b/lib/ain-rs-exports/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["staticlib"] [dependencies] ain-evm = { path = "../ain-evm" } ain-grpc = { path = "../ain-grpc" } +ain-contracts = { path = "../ain-contracts" } ethereum = "0.14.0" rlp = "0.5.2" diff --git a/lib/ain-rs-exports/src/evm.rs b/lib/ain-rs-exports/src/evm.rs index b4d2d7d7e5..bf57b20dab 100644 --- a/lib/ain-rs-exports/src/evm.rs +++ b/lib/ain-rs-exports/src/evm.rs @@ -6,8 +6,11 @@ use ain_evm::{ transaction::{self, SignedTx}, weiamount::WeiAmount, }; +use std::error::Error; use ain_evm::storage::traits::BlockStorage; +use ain_evm::transaction::system::{DST20Data, DeployContractData, SystemTx}; +use ain_evm::txqueue::QueueTx; use ethereum::{EnvelopedEncodable, TransactionAction, TransactionSignature}; use log::debug; use primitive_types::{H160, H256, U256}; @@ -432,3 +435,78 @@ pub fn evm_try_get_block_number_by_hash( None => cross_boundary_error_return(result, "Invalid block hash"), } } + +pub fn evm_create_dst20( + result: &mut ffi::CrossBoundaryResult, + context: u64, + native_hash: [u8; 32], + name: &str, + symbol: &str, + token_id: &str, +) { + match create_dst20(context, native_hash, name, symbol, token_id) { + Ok(_) => cross_boundary_success(result), + Err(e) => cross_boundary_error_return(result, e.to_string()), + } +} + +pub fn evm_bridge_dst20( + result: &mut ffi::CrossBoundaryResult, + context: u64, + address: &str, + amount: [u8; 32], + native_tx_hash: [u8; 32], + token_id: &str, + out: bool, +) { + match bridge_to_dst20(context, address, amount, native_tx_hash, token_id, out) { + Ok(_) => cross_boundary_success(result), + Err(e) => cross_boundary_error_return(result, e.to_string()), + } +} + +fn create_dst20( + context: u64, + native_hash: [u8; 32], + name: &str, + symbol: &str, + token_id: &str, +) -> Result<(), Box> { + let address = ain_contracts::dst20_address_from_token_id(token_id)?; + debug!("Deploying to address {:#?}", address); + + let system_tx = QueueTx::SystemTx(SystemTx::DeployContract(DeployContractData { + name: String::from(name), + symbol: String::from(symbol), + address, + })); + SERVICES + .evm + .queue_tx(context, system_tx, native_hash, U256::zero())?; + + Ok(()) +} + +fn bridge_to_dst20( + context: u64, + address: &str, + amount: [u8; 32], + native_hash: [u8; 32], + token_id: &str, + out: bool, +) -> Result<(), Box> { + let address = address.parse()?; + let contract = ain_contracts::dst20_address_from_token_id(token_id)?; + + let system_tx = QueueTx::SystemTx(SystemTx::DST20Bridge(DST20Data { + to: address, + contract, + amount: amount.into(), + out, + })); + SERVICES + .evm + .queue_tx(context, system_tx, native_hash, U256::zero())?; + + Ok(()) +} diff --git a/lib/ain-rs-exports/src/lib.rs b/lib/ain-rs-exports/src/lib.rs index 6646cfa1bf..33283214c2 100644 --- a/lib/ain-rs-exports/src/lib.rs +++ b/lib/ain-rs-exports/src/lib.rs @@ -128,5 +128,22 @@ pub mod ffi { result: &mut CrossBoundaryResult, hash: [u8; 32], ) -> u64; + fn evm_create_dst20( + result: &mut CrossBoundaryResult, + context: u64, + native_hash: [u8; 32], + name: &str, + symbol: &str, + token_id: &str, + ); + fn evm_bridge_dst20( + result: &mut CrossBoundaryResult, + context: u64, + address: &str, + amount: [u8; 32], + native_tx_hash: [u8; 32], + token_id: &str, + out: bool, + ); } } diff --git a/src/masternodes/errors.h b/src/masternodes/errors.h index 38d53fdf6d..3c3cd63310 100644 --- a/src/masternodes/errors.h +++ b/src/masternodes/errors.h @@ -434,6 +434,26 @@ class DeFiErrors { return Res::Err("Source amount must be equal to destination amount"); } + static Res TransferDomainDifferentTokens() { + return Res::Err("Source token and destination token must be the same"); + } + + static Res TransferDomainDVMToEVMNativeTokenNotEnabled() { + return Res::Err("transferdomain for DFI from DVM to EVM is not enabled"); + } + + static Res TransferDomainEVMToDVMNativeTokenNotEnabled() { + return Res::Err("transferdomain for DFI from EVM to DVM is not enabled"); + } + + static Res TransferDomainDVMToEVMDATNotEnabled() { + return Res::Err("transferdomain for DST20 from DVM to EVM is not enabled"); + } + + static Res TransferDomainEVMToDVMDATNotEnabled() { + return Res::Err("transferdomain for DST20 from EVM to DVM is not enabled"); + } + static Res TransferDomainIncorrectToken() { return Res::Err("For transferdomain, only DFI token is currently supported"); } diff --git a/src/masternodes/govvariables/attributes.h b/src/masternodes/govvariables/attributes.h index 5b241f2fe1..0fb05b1511 100644 --- a/src/masternodes/govvariables/attributes.h +++ b/src/masternodes/govvariables/attributes.h @@ -87,28 +87,28 @@ enum EconomyKeys : uint8_t { }; enum DFIPKeys : uint8_t { - Active = 'a', - Premium = 'b', - MinSwap = 'c', - RewardPct = 'd', - BlockPeriod = 'e', - DUSDInterestBurn = 'g', - DUSDLoanBurn = 'h', - StartBlock = 'i', - GovUnset = 'j', - GovFoundation = 'k', - MNSetRewardAddress = 'l', - MNSetOperatorAddress = 'm', - MNSetOwnerAddress = 'n', - ConsortiumEnabled = 'o', - Members = 'p', - GovernanceEnabled = 'q', - CFPPayout = 'r', - EmissionUnusedFund = 's', - MintTokens = 't', - EVMEnabled = 'u', - ICXEnabled = 'v', - TransferDomain = 'w', + Active = 'a', + Premium = 'b', + MinSwap = 'c', + RewardPct = 'd', + BlockPeriod = 'e', + DUSDInterestBurn = 'g', + DUSDLoanBurn = 'h', + StartBlock = 'i', + GovUnset = 'j', + GovFoundation = 'k', + MNSetRewardAddress = 'l', + MNSetOperatorAddress = 'm', + MNSetOwnerAddress = 'n', + ConsortiumEnabled = 'o', + Members = 'p', + GovernanceEnabled = 'q', + CFPPayout = 'r', + EmissionUnusedFund = 's', + MintTokens = 't', + EVMEnabled = 'u', + ICXEnabled = 'v', + TransferDomain = 'w', }; enum GovernanceKeys : uint8_t { diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index d957ac4bab..11342a6551 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -1045,8 +1045,11 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor { CTokenImplementation token; static_cast(token) = obj; - token.symbol = trim_ws(token.symbol).substr(0, CToken::MAX_TOKEN_SYMBOL_LENGTH); - token.name = trim_ws(token.name).substr(0, CToken::MAX_TOKEN_NAME_LENGTH); + auto tokenSymbol = trim_ws(token.symbol).substr(0, CToken::MAX_TOKEN_SYMBOL_LENGTH); + auto tokenName = trim_ws(token.name).substr(0, CToken::MAX_TOKEN_NAME_LENGTH); + + token.symbol = tokenSymbol; + token.name = tokenName; token.creationTx = tx.GetHash(); token.creationHeight = height; @@ -1058,11 +1061,25 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor { if (static_cast(height) >= consensus.BayfrontHeight) { // formal compatibility if someone cheat and create // LPS token on the pre-bayfront node if (token.IsPoolShare()) { - return Res::Err("Cant't manually create 'Liquidity Pool Share' token; use poolpair creation"); + return Res::Err("Can't manually create 'Liquidity Pool Share' token; use poolpair creation"); + } + } + + auto tokenId = mnview.CreateToken(token, static_cast(height) < consensus.BayfrontHeight); + + if (tokenId && token.IsDAT() && IsEVMEnabled(height, mnview, consensus)) { + CrossBoundaryResult result; + evm_create_dst20(result, evmQueueId, tx.GetHash().GetByteArray(), + rust::string(tokenName.c_str()), + rust::string(tokenSymbol.c_str()), + tokenId->ToString()); + + if (!result.ok) { + return Res::Err("Error creating DST20 token: %s", result.reason); } } - return mnview.CreateToken(token, static_cast(height) < consensus.BayfrontHeight); + return tokenId; } Res operator()(const CUpdateTokenPreAMKMessage &obj) const { @@ -3884,9 +3901,23 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor { ExtractDestination(src.address, dest); const auto fromAddress = std::get(dest); arith_uint256 balanceIn = src.amount.nValue; + auto tokenId = dst.amount.nTokenId; balanceIn *= CAMOUNT_TO_GWEI * WEI_IN_GWEI; - if (!evm_sub_balance(evmQueueId, HexStr(fromAddress.begin(), fromAddress.end()), ArithToUint256(balanceIn).GetByteArray(), tx.GetHash().GetByteArray())) { - return DeFiErrors::TransferDomainNotEnoughBalance(EncodeDestination(dest)); + + if (tokenId == DCT_ID{0}) { + if (!evm_sub_balance(evmQueueId, HexStr(fromAddress.begin(), fromAddress.end()), + ArithToUint256(balanceIn).GetByteArray(), tx.GetHash().GetByteArray())) { + return DeFiErrors::TransferDomainNotEnoughBalance(EncodeDestination(dest)); + } + } + else { + CrossBoundaryResult result; + evm_bridge_dst20(result, evmQueueId, HexStr(fromAddress.begin(), fromAddress.end()), + ArithToUint256(balanceIn).GetByteArray(), tx.GetHash().GetByteArray(), tokenId.ToString(), true); + + if (!result.ok) { + return Res::Err("Error bridging DST20: %s", result.reason); + } } } if (dst.domain == static_cast(VMDomain::DVM)) { @@ -3902,8 +3933,21 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor { ExtractDestination(dst.address, dest); const auto toAddress = std::get(dest); arith_uint256 balanceIn = dst.amount.nValue; + auto tokenId = dst.amount.nTokenId; balanceIn *= CAMOUNT_TO_GWEI * WEI_IN_GWEI; - evm_add_balance(evmQueueId, HexStr(toAddress.begin(), toAddress.end()), ArithToUint256(balanceIn).GetByteArray(), tx.GetHash().GetByteArray()); + if (tokenId == DCT_ID{0}) { + evm_add_balance(evmQueueId, HexStr(toAddress.begin(), toAddress.end()), + ArithToUint256(balanceIn).GetByteArray(), tx.GetHash().GetByteArray()); + } + else { + CrossBoundaryResult result; + evm_bridge_dst20(result, evmQueueId, HexStr(toAddress.begin(), toAddress.end()), + ArithToUint256(balanceIn).GetByteArray(), tx.GetHash().GetByteArray(), tokenId.ToString(), false); + + if (!result.ok) { + return Res::Err("Error bridging DST20: %s", result.reason); + } + } } if (src.data.size() > MAX_TRANSFERDOMAIN_EVM_DATA_LEN || dst.data.size() > MAX_TRANSFERDOMAIN_EVM_DATA_LEN) { @@ -4067,9 +4111,20 @@ Res ValidateTransferDomainEdge(const CTransaction &tx, if (src.amount.nValue != dst.amount.nValue) return DeFiErrors::TransferDomainUnequalAmount(); - // Restrict only for use with DFI token for now. Will be enabled later. - if (src.amount.nTokenId != DCT_ID{0} || dst.amount.nTokenId != DCT_ID{0}) - return DeFiErrors::TransferDomainIncorrectToken(); + if (src.amount.nTokenId != dst.amount.nTokenId) + return DeFiErrors::TransferDomainDifferentTokens(); + + if (src.amount.nTokenId == DCT_ID{0} && !config.dvmToEvmNativeTokenEnabled) + return DeFiErrors::TransferDomainDVMToEVMNativeTokenNotEnabled(); + + if (dst.amount.nTokenId == DCT_ID{0} && !config.evmToDvmNativeTokenEnabled) + return DeFiErrors::TransferDomainEVMToDVMNativeTokenNotEnabled(); + + if (src.amount.nTokenId != DCT_ID{0} && !config.dvmToEvmDatEnabled) + return DeFiErrors::TransferDomainDVMToEVMDATNotEnabled(); + + if (dst.amount.nTokenId != DCT_ID{0} && !config.evmToDvmDatEnabled) + return DeFiErrors::TransferDomainEVMToDVMDATNotEnabled(); if (src.domain == static_cast(VMDomain::DVM) && dst.domain == static_cast(VMDomain::EVM)) { if (!config.dvmToEvmEnabled) { @@ -4123,15 +4178,15 @@ TransferDomainLiveConfig GetTransferDomainConfig(CCustomCSView &mnview) { TransferDomainLiveConfig config{ attributes->GetValue(dvm_to_evm_enabled, true), attributes->GetValue(evm_to_dvm_enabled, true), - attributes->GetValue(dvm_to_evm_src_formats, XVmAddressFormatItems { + attributes->GetValue(dvm_to_evm_src_formats, XVmAddressFormatItems { XVmAddressFormatTypes::Bech32, XVmAddressFormatTypes::PkHash }), - attributes->GetValue(dvm_to_evm_dest_formats, XVmAddressFormatItems { + attributes->GetValue(dvm_to_evm_dest_formats, XVmAddressFormatItems { XVmAddressFormatTypes::Erc55 }), - attributes->GetValue(evm_to_dvm_dest_formats, XVmAddressFormatItems { + attributes->GetValue(evm_to_dvm_dest_formats, XVmAddressFormatItems { XVmAddressFormatTypes::Bech32, XVmAddressFormatTypes::PkHash }), - attributes->GetValue(evm_to_dvm_src_formats, XVmAddressFormatItems { + attributes->GetValue(evm_to_dvm_src_formats, XVmAddressFormatItems { XVmAddressFormatTypes::Erc55 }), - attributes->GetValue(evm_to_dvm_auth_formats, XVmAddressFormatItems { + attributes->GetValue(evm_to_dvm_auth_formats, XVmAddressFormatItems { XVmAddressFormatTypes::Bech32ProxyErc55, XVmAddressFormatTypes::PkHashProxyErc55 }), attributes->GetValue(dvm_to_evm_native_enabled, true), attributes->GetValue(evm_to_dvm_native_enabled, true), diff --git a/test/functional/feature_dst20.py b/test/functional/feature_dst20.py new file mode 100644 index 0000000000..5e01683d71 --- /dev/null +++ b/test/functional/feature_dst20.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) DeFi Blockchain Developers +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. +"""Test EVM behaviour""" + +import math +from decimal import Decimal +from web3 import Web3 + +from test_framework.evm_key_pair import KeyPair +from test_framework.test_framework import DefiTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error + + +class DST20(DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [ + [ + "-txordering=2", + "-dummypos=0", + "-txnotokens=0", + "-amkheight=50", + "-bayfrontheight=51", + "-eunosheight=80", + "-fortcanningheight=82", + "-fortcanninghillheight=84", + "-fortcanningroadheight=86", + "-fortcanningcrunchheight=88", + "-fortcanningspringheight=90", + "-fortcanninggreatworldheight=94", + "-fortcanningepilogueheight=96", + "-grandcentralheight=101", + "-nextnetworkupgradeheight=105", + "-changiintermediateheight=105", + "-changiintermediate3height=105", + "-subsidytest=1", + "-txindex=1", + ] + ] + + def test_deploy_token(self): + # should have no code on contract address + assert_equal( + Web3.to_hex(self.web3.eth.get_code(self.contract_address_btc)), "0x" + ) + + self.node.createtoken( + { + "symbol": "BTC", + "name": "BTC token", + "isDAT": True, + "collateralAddress": self.address, + } + ) + self.nodes[0].generate(1) + + # should have code on contract address + assert Web3.to_hex(self.web3.eth.get_code(self.contract_address_btc)) != "0x" + + # check contract variables + self.btc = self.web3.eth.contract( + address=self.contract_address_btc, abi=self.abi + ) + assert_equal(self.btc.functions.name().call(), "BTC token") + assert_equal(self.btc.functions.symbol().call(), "BTC") + + def test_deploy_multiple_tokens(self): + # should have no code on contract addresses + assert_equal( + Web3.to_hex(self.web3.eth.get_code(self.contract_address_eth)), "0x" + ) + assert_equal( + Web3.to_hex(self.web3.eth.get_code(self.contract_address_dusd)), "0x" + ) + + self.node.createtoken( + { + "symbol": "ETH", + "name": "ETH token", + "isDAT": True, + "collateralAddress": self.address, + } + ) + self.node.createtoken( + { + "symbol": "DUSD", + "name": "DUSD token", + "isDAT": True, + "collateralAddress": self.address, + } + ) + self.node.generate(1) + + # should have code on contract address + assert Web3.to_hex(self.web3.eth.get_code(self.contract_address_eth)) != "0x" + assert Web3.to_hex(self.web3.eth.get_code(self.contract_address_dusd)) != "0x" + + # check contract variables + self.eth = self.web3.eth.contract( + address=self.contract_address_eth, abi=self.abi + ) + assert_equal(self.eth.functions.name().call(), "ETH token") + assert_equal(self.eth.functions.symbol().call(), "ETH") + + self.dusd = self.web3.eth.contract( + address=self.contract_address_dusd, abi=self.abi + ) + assert_equal(self.dusd.functions.name().call(), "DUSD token") + assert_equal(self.dusd.functions.symbol().call(), "DUSD") + + def test_dst20_dvm_to_evm_bridge(self): + self.node.transferdomain( + [ + { + "src": {"address": self.address, "amount": "1@BTC", "domain": 2}, + "dst": { + "address": self.key_pair.address, + "amount": "1@BTC", + "domain": 3, + }, + } + ] + ) + self.node.generate(1) + + assert_equal( + self.btc.functions.balanceOf(self.key_pair.address).call() + / math.pow(10, self.btc.functions.decimals().call()), + Decimal(1), + ) + + [amountBTC] = [x for x in self.node.getaccount(self.address) if "BTC" in x] + assert_equal(amountBTC, "9.00000000@BTC") + + # transfer again to check if state is updated instead of being overwritten by new value + self.nodes[0].transferdomain( + [ + { + "src": {"address": self.address, "amount": "1@BTC", "domain": 2}, + "dst": { + "address": self.key_pair.address, + "amount": "1@BTC", + "domain": 3, + }, + } + ] + ) + self.node.generate(1) + + assert_equal( + self.btc.functions.balanceOf(self.key_pair.address).call() + / math.pow(10, self.btc.functions.decimals().call()), + Decimal(2), + ) + [amountBTC] = [x for x in self.node.getaccount(self.address) if "BTC" in x] + assert_equal(amountBTC, "8.00000000@BTC") + + def test_dst20_evm_to_dvm_bridge(self): + self.node.transferdomain( + [ + { + "dst": {"address": self.address, "amount": "1.5@BTC", "domain": 2}, + "src": { + "address": self.key_pair.address, + "amount": "1.5@BTC", + "domain": 3, + }, + } + ] + ) + self.node.generate(1) + + assert_equal( + self.btc.functions.balanceOf(self.key_pair.address).call() + / math.pow(10, self.btc.functions.decimals().call()), + Decimal(0.5), + ) + [amountBTC] = [x for x in self.node.getaccount(self.address) if "BTC" in x] + assert_equal(amountBTC, "9.50000000@BTC") + + def test_multiple_dvm_evm_bridge(self): + self.nodes[0].transferdomain( + [ + { + "src": {"address": self.address, "amount": "1@BTC", "domain": 2}, + "dst": { + "address": self.key_pair.address, + "amount": "1@BTC", + "domain": 3, + }, + } + ] + ) + self.nodes[0].transferdomain( + [ + { + "src": {"address": self.address, "amount": "2@BTC", "domain": 2}, + "dst": { + "address": self.key_pair.address, + "amount": "2@BTC", + "domain": 3, + }, + } + ] + ) + self.node.generate(1) + + assert_equal( + self.btc.functions.balanceOf(self.key_pair.address).call() + / math.pow(10, self.btc.functions.decimals().call()), + Decimal(3.5), + ) + [amountBTC] = [x for x in self.node.getaccount(self.address) if "BTC" in x] + assert_equal(amountBTC, "6.50000000@BTC") + + def test_conflicting_bridge(self): + [beforeAmount] = [x for x in self.node.getaccount(self.address) if "BTC" in x] + + # should be processed in order so balance is bridged to and back in the same block + self.nodes[0].transferdomain( + [ + { + "src": {"address": self.address, "amount": "1@BTC", "domain": 2}, + "dst": { + "address": self.key_pair2.address, + "amount": "1@BTC", + "domain": 3, + }, + } + ] + ) + self.nodes[0].transferdomain( + [ + { + "src": { + "address": self.key_pair2.address, + "amount": "1@BTC", + "domain": 3, + }, + "dst": {"address": self.address, "amount": "1@BTC", "domain": 2}, + } + ] + ) + self.node.generate(1) + + [afterAmount] = [x for x in self.node.getaccount(self.address) if "BTC" in x] + assert_equal( + self.btc.functions.balanceOf(self.key_pair2.address).call(), Decimal(0) + ) + assert_equal(beforeAmount, afterAmount) + + def test_bridge_when_no_balance(self): + assert_equal( + self.btc.functions.balanceOf(self.key_pair2.address).call(), Decimal(0) + ) + [beforeAmount] = [x for x in self.node.getaccount(self.address) if "BTC" in x] + + self.nodes[0].transferdomain( + [ + { + "src": { + "address": self.key_pair2.address, + "amount": "1@BTC", + "domain": 3, + }, + "dst": {"address": self.address, "amount": "1@BTC", "domain": 2}, + } + ] + ) + self.node.generate(1) + + assert_equal( + self.btc.functions.balanceOf(self.key_pair2.address).call(), Decimal(0) + ) + [afterAmount] = [x for x in self.node.getaccount(self.address) if "BTC" in x] + assert_equal(beforeAmount, afterAmount) + + def test_invalid_token(self): + # DVM to EVM + assert_raises_rpc_error( + 0, + "Invalid Defi token: XYZ", + self.nodes[0].transferdomain, + [ + { + "src": {"address": self.address, "amount": "1@XYZ", "domain": 2}, + "dst": { + "address": self.key_pair.address, + "amount": "1@XYZ", + "domain": 3, + }, + } + ], + ) + + # EVM to DVM + assert_raises_rpc_error( + 0, + "Invalid Defi token: XYZ", + self.nodes[0].transferdomain, + [ + { + "src": { + "address": self.key_pair.address, + "amount": "1@XYZ", + "domain": 3, + }, + "dst": {"address": self.address, "amount": "1@XYZ", "domain": 2}, + } + ], + ) + + def test_transfer_to_token_address(self): + self.nodes[0].transferdomain( + [ + { + "src": {"address": self.address, "amount": "2@BTC", "domain": 2}, + "dst": { + "address": self.contract_address_btc, + "amount": "2@BTC", + "domain": 3, + }, + } + ] + ) + self.node.generate(1) + + assert_equal( + self.btc.functions.balanceOf(self.contract_address_btc).call() + / math.pow(10, self.btc.functions.decimals().call()), + Decimal(2), + ) + + def test_negative_transfer(self): + assert_raises_rpc_error(-3, "Amount out of range", self.nodes[0].transferdomain, + [ + { + "src": {"address": self.address, "amount": "-1@BTC", "domain": 2}, + "dst": { + "address": self.contract_address_btc, + "amount": "-1@BTC", + "domain": 3, + }, + } + ] + ) + + def test_different_tokens(self): + assert_raises_rpc_error(-32600, "Source token and destination token must be the same", self.nodes[0].transferdomain, + [ + { + "src": {"address": self.address, "amount": "1@BTC", "domain": 2}, + "dst": { + "address": self.contract_address_btc, + "amount": "1@ETH", + "domain": 3, + }, + } + ] + ) + + + def run_test(self): + self.node = self.nodes[0] + self.address = self.node.get_genesis_keys().ownerAuthAddress + self.web3 = Web3(Web3.HTTPProvider(self.node.get_evm_rpc())) + + # Contract addresses + self.contract_address_btc = "0xff00000000000000000000000000000000000001" + self.contract_address_eth = "0xff00000000000000000000000000000000000002" + self.contract_address_dusd = Web3.to_checksum_address( + "0xff00000000000000000000000000000000000003" + ) + + # Contract ABI + self.abi = open("./lib/ain-contracts/dst20/output/abi.json", "r", encoding="utf8").read() + + # Generate chain + self.node.generate(105) + self.nodes[0].utxostoaccount({self.address: "100@DFI"}) + self.nodes[0].setgov({"ATTRIBUTES": {"v0/params/feature/evm": "true"}}) + self.nodes[0].generate(1) + + self.test_deploy_token() + self.test_deploy_multiple_tokens() + + self.key_pair = KeyPair.from_node(self.node) + self.key_pair2 = KeyPair.from_node(self.node) + self.node.minttokens("10@BTC") + self.node.generate(1) + + self.test_dst20_dvm_to_evm_bridge() + self.test_dst20_evm_to_dvm_bridge() + self.test_multiple_dvm_evm_bridge() + self.test_conflicting_bridge() + self.test_invalid_token() + self.test_transfer_to_token_address() + self.test_bridge_when_no_balance() + self.test_negative_transfer() + self.test_different_tokens() + +if __name__ == "__main__": + DST20().main() diff --git a/test/functional/feature_evm_transferdomain.py b/test/functional/feature_evm_transferdomain.py index 290773b1eb..b71357fcf8 100755 --- a/test/functional/feature_evm_transferdomain.py +++ b/test/functional/feature_evm_transferdomain.py @@ -111,8 +111,6 @@ def invalid_values_dvm_evm(self): assert_raises_rpc_error(-32600, "Dst address must be an ERC55 address in case of \"EVM\" domain", self.nodes[0].transferdomain, [{"src": {"address":self.address, "amount":"100@DFI", "domain": 2}, "dst":{"address":self.address, "amount":"100@DFI", "domain": 3}}]) assert_raises_rpc_error(-32600, "Cannot transfer inside same domain", self.nodes[0].transferdomain, [{"src": {"address":self.address, "amount":"100@DFI", "domain": 2}, "dst":{"address":self.eth_address, "amount":"100@DFI", "domain": 2}}]) assert_raises_rpc_error(-32600, "Source amount must be equal to destination amount", self.nodes[0].transferdomain, [{"src": {"address":self.address, "amount":"100@DFI", "domain": 2}, "dst":{"address":self.eth_address, "amount":"101@DFI", "domain": 3}}]) - assert_raises_rpc_error(-32600, "For transferdomain, only DFI token is currently supported", self.nodes[0].transferdomain, [{"src": {"address":self.address, "amount":"100@BTC", "domain": 2}, "dst":{"address":self.eth_address, "amount":"100@DFI", "domain": 3}}]) - assert_raises_rpc_error(-32600, "For transferdomain, only DFI token is currently supported", self.nodes[0].transferdomain, [{"src": {"address":self.address, "amount":"100@DFI", "domain": 2}, "dst":{"address":self.eth_address, "amount":"100@BTC", "domain": 3}}]) assert_raises_rpc_error(-32600, "Excess data set, maximum allow is 0", self.nodes[0].transferdomain, [{"src": {"address":self.address, "amount":"100@DFI", "domain": 2, "data": "1"}, "dst":{"address":self.eth_address, "amount":"100@DFI", "domain": 3}}]) assert_raises_rpc_error(-32600, "Excess data set, maximum allow is 0", self.nodes[0].transferdomain, [{"src": {"address":self.address, "amount":"100@DFI", "domain": 2}, "dst":{"address":self.eth_address, "amount":"100@DFI", "domain": 3, "data": "1"}}]) @@ -147,8 +145,6 @@ def invalid_values_evm_dvm(self): assert_raises_rpc_error(-32600, "Dst address must be a legacy or Bech32 address in case of \"DVM\" domain", self.nodes[0].transferdomain, [{"src": {"address":self.eth_address, "amount":"100@DFI", "domain": 3}, "dst":{"address":self.eth_address, "amount":"100@DFI", "domain": 2}}]) assert_raises_rpc_error(-32600, "Cannot transfer inside same domain", self.nodes[0].transferdomain, [{"src": {"address":self.eth_address, "amount":"100@DFI", "domain": 3}, "dst":{"address":self.address, "amount":"100@DFI", "domain": 3}}]) assert_raises_rpc_error(-32600, "Source amount must be equal to destination amount", self.nodes[0].transferdomain, [{"src": {"address":self.eth_address, "amount":"100@DFI", "domain": 3}, "dst":{"address":self.address, "amount":"101@DFI", "domain": 2}}]) - assert_raises_rpc_error(-32600, "For transferdomain, only DFI token is currently supported", self.nodes[0].transferdomain, [{"src": {"address":self.eth_address, "amount":"100@BTC", "domain": 3}, "dst":{"address":self.address, "amount":"100@DFI", "domain": 2}}]) - assert_raises_rpc_error(-32600, "For transferdomain, only DFI token is currently supported", self.nodes[0].transferdomain, [{"src": {"address":self.eth_address, "amount":"100@DFI", "domain": 3}, "dst":{"address":self.address, "amount":"100@BTC", "domain": 2}}]) assert_raises_rpc_error(-32600, "TransferDomain currently only supports a single transfer per transaction", self.nodes[0].transferdomain, [{"src": {"address":self.eth_address1, "amount":"10@DFI", "domain": 3}, "dst":{"address":self.address, "amount":"10@DFI", "domain": 2}}, {"src": {"address":self.eth_address1, "amount":"10@DFI", "domain": 3}, "dst":{"address":self.address_2nd, "amount":"10@DFI", "domain": 2}}]) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 2dcf70c051..6ec79b4ea2 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -301,6 +301,7 @@ 'feature_evm_smart_contract.py', 'feature_evm_transferdomain.py', 'feature_evm.py', + 'feature_dst20.py', 'feature_evm_state_root_change.py', 'feature_loan_low_interest.py', 'feature_loan_estimatecollateral.py',