From 6c48590c156acb2bbd89baee8576ceb23c6ca24b Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Tue, 30 Aug 2022 08:06:16 +0000 Subject: [PATCH] Merge pull request #483 from UniqueNetwork/feature/mint-for-fungible-token --- --- a/Cargo.lock +++ b/Cargo.lock @@ -2169,7 +2169,7 @@ [[package]] name = "evm-coder" -version = "0.1.1" +version = "0.1.3" dependencies = [ "ethereum", "evm-coder-procedural", @@ -2178,6 +2178,7 @@ "hex-literal", "impl-trait-for-tuples", "primitive-types", + "sp-std", ] [[package]] @@ -5748,7 +5749,7 @@ [[package]] name = "pallet-fungible" -version = "0.1.4" +version = "0.1.5" dependencies = [ "ethereum", "evm-coder", --- a/crates/evm-coder/CHANGELOG.md +++ b/crates/evm-coder/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [0.1.3] - 2022-08-29 + +### Fixed + + - Parsing simple values. + ## [v0.1.2] 2022-08-19 @@ -21,4 +27,4 @@ - build: Upgrade polkadot to v0.9.26 85515e54c4ca1b82a2630034e55dcc804c643bf8 -- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b \ No newline at end of file +- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b --- a/crates/evm-coder/Cargo.toml +++ b/crates/evm-coder/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "evm-coder" -version = "0.1.1" +version = "0.1.3" license = "GPLv3" edition = "2021" @@ -11,8 +11,9 @@ primitive-types = { version = "0.11.1", default-features = false } # Evm doesn't have reexports for log and others ethereum = { version = "0.12.0", default-features = false } +sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.27" } # Error types for execution -evm-core = { default-features = false, git = "https://github.com/uniquenetwork/evm", branch = "unique-polkadot-v0.9.27" } +evm-core = { default-features = false , git = "https://github.com/uniquenetwork/evm", branch = "unique-polkadot-v0.9.27" } # We have tuple-heavy code in solidity.rs impl-trait-for-tuples = "0.2.2" --- a/crates/evm-coder/src/abi.rs +++ b/crates/evm-coder/src/abi.rs @@ -31,6 +31,10 @@ const ABI_ALIGNMENT: usize = 32; +trait TypeHelper { + fn is_dynamic() -> bool; +} + /// View into RLP data, which provides method to read typed items from it #[derive(Clone)] pub struct AbiReader<'i> { @@ -77,8 +81,8 @@ return Err(Error::Error(ExitError::OutOfOffset)); } let mut block = [0; S]; - // Verify padding is empty - if !buf[pad_start..pad_size].iter().all(|&v| v == 0) { + let is_pad_zeroed = buf[pad_start..pad_size].iter().all(|&v| v == 0); + if !is_pad_zeroed { return Err(Error::Error(ExitError::InvalidRange)); } block.copy_from_slice(&buf[block_start..block_size]); @@ -133,7 +137,7 @@ /// Read [`Vec`] at current position, then advance pub fn bytes(&mut self) -> Result> { - let mut subresult = self.subresult()?; + let mut subresult = self.subresult(None)?; let length = subresult.uint32()? as usize; if subresult.buf.len() < subresult.offset + length { return Err(Error::Error(ExitError::OutOfOffset)); @@ -179,15 +183,26 @@ } /// Slice recursive buffer, advance one word for buffer offset - fn subresult(&mut self) -> Result> { - let offset = self.uint32()? as usize; + /// If `size` is [`None`] then [`Self::offset`] and [`Self::subresult_offset`] evals from [`Self::buf`]. + fn subresult(&mut self, size: Option) -> Result> { + let subresult_offset = self.subresult_offset; + let offset = if let Some(size) = size { + self.offset += size; + self.subresult_offset += size; + 0 + } else { + self.uint32()? as usize + }; + if offset + self.subresult_offset > self.buf.len() { return Err(Error::Error(ExitError::InvalidRange)); } + + let new_offset = offset + subresult_offset; Ok(AbiReader { buf: self.buf, - subresult_offset: offset + self.subresult_offset, - offset: offset + self.subresult_offset, + subresult_offset: new_offset, + offset: new_offset, }) } @@ -318,28 +333,40 @@ pub trait AbiRead { /// Read item from current position, advanding decoder fn abi_read(&mut self) -> Result; + + /// Size for type aligned to [`ABI_ALIGNMENT`]. + fn size() -> usize; } macro_rules! impl_abi_readable { - ($ty:ty, $method:ident) => { + ($ty:ty, $method:ident, $dynamic:literal) => { + impl TypeHelper for $ty { + fn is_dynamic() -> bool { + $dynamic + } + } impl AbiRead<$ty> for AbiReader<'_> { fn abi_read(&mut self) -> Result<$ty> { self.$method() } + + fn size() -> usize { + ABI_ALIGNMENT + } } }; } -impl_abi_readable!(u8, uint8); -impl_abi_readable!(u32, uint32); -impl_abi_readable!(u64, uint64); -impl_abi_readable!(u128, uint128); -impl_abi_readable!(U256, uint256); -impl_abi_readable!([u8; 4], bytes4); -impl_abi_readable!(H160, address); -impl_abi_readable!(Vec, bytes); -impl_abi_readable!(bool, bool); -impl_abi_readable!(string, string); +impl_abi_readable!(u8, uint8, false); +impl_abi_readable!(u32, uint32, false); +impl_abi_readable!(u64, uint64, false); +impl_abi_readable!(u128, uint128, false); +impl_abi_readable!(U256, uint256, false); +impl_abi_readable!([u8; 4], bytes4, false); +impl_abi_readable!(H160, address, false); +impl_abi_readable!(Vec, bytes, true); +impl_abi_readable!(bool, bool, true); +impl_abi_readable!(string, string, true); mod sealed { /// Not all types can be placed in vec, i.e `Vec` is restricted, `bytes` should be used instead @@ -355,7 +382,7 @@ Self: AbiRead, { fn abi_read(&mut self) -> Result> { - let mut sub = self.subresult()?; + let mut sub = self.subresult(None)?; let size = sub.uint32()? as usize; sub.subresult_offset = sub.offset; let mut out = Vec::with_capacity(size); @@ -364,21 +391,41 @@ } Ok(out) } + + fn size() -> usize { + ABI_ALIGNMENT + } } macro_rules! impl_tuples { ($($ident:ident)+) => { + impl<$($ident: TypeHelper,)+> TypeHelper for ($($ident,)+) { + fn is_dynamic() -> bool { + false + $( + || <$ident>::is_dynamic() + )* + } + } impl<$($ident),+> sealed::CanBePlacedInVec for ($($ident,)+) {} impl<$($ident),+> AbiRead<($($ident,)+)> for AbiReader<'_> where - $(Self: AbiRead<$ident>),+ + $( + Self: AbiRead<$ident>, + )+ + ($($ident,)+): TypeHelper, { fn abi_read(&mut self) -> Result<($($ident,)+)> { - let mut subresult = self.subresult()?; + let size = if !<($($ident,)+)>::is_dynamic() { Some(>::size()) } else { None }; + let mut subresult = self.subresult(size)?; Ok(( $(>::abi_read(&mut subresult)?,)+ )) } + + fn size() -> usize { + 0 $(+ as AbiRead<$ident>>::size())+ + } } #[allow(non_snake_case)] impl<$($ident),+> AbiWrite for &($($ident,)+) @@ -535,7 +582,7 @@ assert_eq!(encoded, alternative_encoded); let mut decoder = AbiReader::new(&encoded); - assert_eq!(decoder.bool().unwrap(), true); + assert!(decoder.bool().unwrap()); assert_eq!(decoder.string().unwrap(), "test"); } @@ -604,4 +651,49 @@ ] ); } + + #[test] + fn parse_vec_with_simple_type() { + use crate::types::address; + use primitive_types::{H160, U256}; + + let (call, mut decoder) = AbiReader::new_call(&hex!( + " + 1ACF2D55 + 0000000000000000000000000000000000000000000000000000000000000020 // offset of (address, uint256)[] + 0000000000000000000000000000000000000000000000000000000000000003 // length of (address, uint256)[] + + 0000000000000000000000002D2FF76104B7BACB2E8F6731D5BFC184EBECDDBC // address + 000000000000000000000000000000000000000000000000000000000000000A // uint256 + + 000000000000000000000000AB8E3D9134955566483B11E6825C9223B6737B10 // address + 0000000000000000000000000000000000000000000000000000000000000014 // uint256 + + 0000000000000000000000008C582BDF2953046705FC56F189385255EFC1BE18 // address + 000000000000000000000000000000000000000000000000000000000000001E // uint256 + " + )) + .unwrap(); + assert_eq!(call, u32::to_be_bytes(0x1ACF2D55)); + let data = + as AbiRead>>::abi_read(&mut decoder).unwrap(); + assert_eq!(data.len(), 3); + assert_eq!( + data, + vec![ + ( + H160(hex!("2D2FF76104B7BACB2E8F6731D5BFC184EBECDDBC")), + U256([10, 0, 0, 0]) + ), + ( + H160(hex!("AB8E3D9134955566483B11E6825C9223B6737B10")), + U256([20, 0, 0, 0]) + ), + ( + H160(hex!("8C582BDF2953046705FC56F189385255EFC1BE18")), + U256([30, 0, 0, 0]) + ), + ] + ); + } } --- a/pallets/fungible/CHANGELOG.md +++ b/pallets/fungible/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. + +## [0.1.5] - 2022-08-29 + +### Added + + - Implementation of `mint` and `mint_bulk` methods for ERC20 API. + ## [v0.1.4] - 2022-08-24 ### Change --- a/pallets/fungible/Cargo.toml +++ b/pallets/fungible/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-fungible" -version = "0.1.4" +version = "0.1.5" license = "GPLv3" edition = "2021" --- a/pallets/fungible/src/erc.rs +++ b/pallets/fungible/src/erc.rs @@ -129,8 +129,32 @@ } } +#[solidity_interface(name = ERC20Mintable)] +impl FungibleHandle { + /// Mint tokens for `to` account. + /// @param to account that will receive minted tokens + /// @param amount amount of tokens to mint + #[weight(>::create_item())] + fn mint(&mut self, caller: caller, to: address, amount: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let to = T::CrossAccountId::from_eth(to); + let amount = amount.try_into().map_err(|_| "amount overflow")?; + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + >::create_item(&self, &caller, (to, amount), &budget) + .map_err(dispatch_to_evm::)?; + Ok(true) + } +} + #[solidity_interface(name = ERC20UniqueExtensions)] impl FungibleHandle { + /// Burn tokens from account + /// @dev Function that burns an `amount` of the tokens of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. #[weight(>::burn_from())] fn burn_from(&mut self, caller: caller, from: address, amount: uint256) -> Result { let caller = T::CrossAccountId::from_eth(caller); @@ -144,12 +168,36 @@ .map_err(dispatch_to_evm::)?; Ok(true) } + + /// Mint tokens for multiple accounts. + /// @param amounts array of pairs of account address and amount + #[weight(>::create_multiple_items_ex(amounts.len() as u32))] + fn mint_bulk(&mut self, caller: caller, amounts: Vec<(address, uint256)>) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + let amounts = amounts + .into_iter() + .map(|(to, amount)| { + Ok(( + T::CrossAccountId::from_eth(to), + amount.try_into().map_err(|_| "amount overflow")?, + )) + }) + .collect::>()?; + + >::create_multiple_items(&self, &caller, amounts, &budget) + .map_err(dispatch_to_evm::)?; + Ok(true) + } } #[solidity_interface( name = UniqueFungible, is( ERC20, + ERC20Mintable, ERC20UniqueExtensions, Collection(common_mut, CollectionHandle), ) --- a/pallets/fungible/src/stubs/UniqueFungible.sol +++ b/pallets/fungible/src/stubs/UniqueFungible.sol @@ -350,19 +350,51 @@ } } +/// @dev the ERC-165 identifier for this interface is 0x63034ac5 +contract ERC20UniqueExtensions is Dummy, ERC165 { + /// Burn tokens from account + /// @dev Function that burns an `amount` of the tokens of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) + function burnFrom(address from, uint256 amount) public returns (bool) { + require(false, stub_error); + from; + amount; + dummy = 0; + return false; + } + + /// Mint tokens for multiple accounts. + /// @param amounts array of pairs of account address and amount + /// @dev EVM selector for this function is: 0x1acf2d55, + /// or in textual repr: mintBulk((address,uint256)[]) + function mintBulk(Tuple6[] memory amounts) public returns (bool) { + require(false, stub_error); + amounts; + dummy = 0; + return false; + } +} + /// @dev anonymous struct struct Tuple6 { address field_0; uint256 field_1; } -/// @dev the ERC-165 identifier for this interface is 0x79cc6790 -contract ERC20UniqueExtensions is Dummy, ERC165 { - /// @dev EVM selector for this function is: 0x79cc6790, - /// or in textual repr: burnFrom(address,uint256) - function burnFrom(address from, uint256 amount) public returns (bool) { +/// @dev the ERC-165 identifier for this interface is 0x40c10f19 +contract ERC20Mintable is Dummy, ERC165 { + /// Mint tokens for `to` account. + /// @param to account that will receive minted tokens + /// @param amount amount of tokens to mint + /// @dev EVM selector for this function is: 0x40c10f19, + /// or in textual repr: mint(address,uint256) + function mint(address to, uint256 amount) public returns (bool) { require(false, stub_error); - from; + to; amount; dummy = 0; return false; @@ -476,6 +508,7 @@ Dummy, ERC165, ERC20, + ERC20Mintable, ERC20UniqueExtensions, Collection {} --- a/tests/src/eth/api/UniqueFungible.sol +++ b/tests/src/eth/api/UniqueFungible.sol @@ -225,17 +225,38 @@ function setOwnerSubstrate(uint256 newOwner) external; } +/// @dev the ERC-165 identifier for this interface is 0x63034ac5 +interface ERC20UniqueExtensions is Dummy, ERC165 { + /// Burn tokens from account + /// @dev Function that burns an `amount` of the tokens of a given account, + /// deducting from the sender's allowance for said account. + /// @param from The account whose tokens will be burnt. + /// @param amount The amount that will be burnt. + /// @dev EVM selector for this function is: 0x79cc6790, + /// or in textual repr: burnFrom(address,uint256) + function burnFrom(address from, uint256 amount) external returns (bool); + + /// Mint tokens for multiple accounts. + /// @param amounts array of pairs of account address and amount + /// @dev EVM selector for this function is: 0x1acf2d55, + /// or in textual repr: mintBulk((address,uint256)[]) + function mintBulk(Tuple6[] memory amounts) external returns (bool); +} + /// @dev anonymous struct struct Tuple6 { address field_0; uint256 field_1; } -/// @dev the ERC-165 identifier for this interface is 0x79cc6790 -interface ERC20UniqueExtensions is Dummy, ERC165 { - /// @dev EVM selector for this function is: 0x79cc6790, - /// or in textual repr: burnFrom(address,uint256) - function burnFrom(address from, uint256 amount) external returns (bool); +/// @dev the ERC-165 identifier for this interface is 0x40c10f19 +interface ERC20Mintable is Dummy, ERC165 { + /// Mint tokens for `to` account. + /// @param to account that will receive minted tokens + /// @param amount amount of tokens to mint + /// @dev EVM selector for this function is: 0x40c10f19, + /// or in textual repr: mint(address,uint256) + function mint(address to, uint256 amount) external returns (bool); } /// @dev inlined interface @@ -298,6 +319,7 @@ Dummy, ERC165, ERC20, + ERC20Mintable, ERC20UniqueExtensions, Collection {} --- a/tests/src/eth/fungible.test.ts +++ b/tests/src/eth/fungible.test.ts @@ -14,10 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {approveExpectSuccess, createCollectionExpectSuccess, createFungibleItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE} from '../util/helpers'; -import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, transferBalanceToEth} from './util/helpers'; +import {approveExpectSuccess, createCollection, createCollectionExpectSuccess, createFungibleItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE} from '../util/helpers'; +import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, transferBalanceToEth} from './util/helpers'; import fungibleAbi from './fungibleAbi.json'; import {expect} from 'chai'; +import {submitTransactionAsync} from '../substrate/substrate-api'; describe('Fungible: Information getting', () => { itWeb3('totalSupply', async ({api, web3, privateKeyWrapper}) => { @@ -58,6 +59,128 @@ }); describe('Fungible: Plain calls', () => { + itWeb3('Can perform mint()', async ({web3, api, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + const collection = await createCollection(api, alice, { + name: 'token name', + mode: {type: 'Fungible', decimalPoints: 0}, + }); + + const receiver = createEthAccount(web3); + + const collectionIdAddress = collectionIdToAddress(collection.collectionId); + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const changeAdminTx = api.tx.unique.addCollectionAdmin(collection.collectionId, {Ethereum: owner}); + await submitTransactionAsync(alice, changeAdminTx); + + const collectionContract = evmCollection(web3, owner, collectionIdAddress, {type: 'Fungible', decimalPoints: 0}); + const result = await collectionContract.methods.mint(receiver, 100).send(); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + value: '100', + }, + }, + ]); + }); + + itWeb3('Can perform mintBulk()', async ({web3, api, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + const collection = await createCollection(api, alice, { + name: 'token name', + mode: {type: 'Fungible', decimalPoints: 0}, + }); + + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const receiver1 = createEthAccount(web3); + const receiver2 = createEthAccount(web3); + const receiver3 = createEthAccount(web3); + + const collectionIdAddress = collectionIdToAddress(collection.collectionId); + const changeAdminTx = api.tx.unique.addCollectionAdmin(collection.collectionId, {Ethereum: owner}); + await submitTransactionAsync(alice, changeAdminTx); + + const collectionContract = evmCollection(web3, owner, collectionIdAddress, {type: 'Fungible', decimalPoints: 0}); + const result = await collectionContract.methods.mintBulk([ + [receiver1, 10], + [receiver2, 20], + [receiver3, 30], + ]).send(); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.contain({ + address:collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver1, + value: '10', + }, + }); + + expect(events).to.be.deep.contain({ + address:collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver2, + value: '20', + }, + }); + + expect(events).to.be.deep.contain({ + address:collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver3, + value: '30', + }, + }); + }); + + itWeb3('Can perform burn()', async ({web3, api, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + const collection = await createCollection(api, alice, { + name: 'token name', + mode: {type: 'Fungible', decimalPoints: 0}, + }); + + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const changeAdminTx = api.tx.unique.addCollectionAdmin(collection.collectionId, {Ethereum: owner}); + await submitTransactionAsync(alice, changeAdminTx); + const receiver = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + + const collectionIdAddress = collectionIdToAddress(collection.collectionId); + const collectionContract = evmCollection(web3, owner, collectionIdAddress, {type: 'Fungible', decimalPoints: 0}); + await collectionContract.methods.mint(receiver, 100).send(); + + const result = await collectionContract.methods.burnFrom(receiver, 49).send({from: receiver}); + + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: receiver, + to: '0x0000000000000000000000000000000000000000', + value: '49', + }, + }, + ]); + + const balance = await collectionContract.methods.balanceOf(receiver).call(); + expect(balance).to.equal('51'); + }); + itWeb3('Can perform approve()', async ({web3, api, privateKeyWrapper}) => { const collection = await createCollectionExpectSuccess({ name: 'token name', --- a/tests/src/eth/fungibleAbi.json +++ b/tests/src/eth/fungibleAbi.json @@ -193,6 +193,33 @@ "type": "function" }, { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "mint", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple6[]", + "name": "amounts", + "type": "tuple[]" + } + ], + "name": "mintBulk", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "name", "outputs": [{ "internalType": "string", "name": "", "type": "string" }], -- gitstuff