git.delta.rocks / unique-network / refs/commits / 6c48590c156a

difftreelog

Merge pull request #483 from UniqueNetwork/feature/mint-for-fungible-token

Yaroslav Bolyukin2022-08-30parents: #5e4ac16 #42824e2.patch.diff
in: master

12 files changed

modifiedCargo.lockdiffbeforeafterboth
--- 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",
modifiedcrates/evm-coder/CHANGELOG.mddiffbeforeafterboth
--- 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.
+
 <!-- bureaucrate goes here -->
 ## [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
modifiedcrates/evm-coder/Cargo.tomldiffbeforeafterboth
--- 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"
 
modifiedcrates/evm-coder/src/abi.rsdiffbeforeafterboth
--- 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<u8>`] at current position, then advance
 	pub fn bytes(&mut self) -> Result<Vec<u8>> {
-		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<AbiReader<'i>> {
-		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<usize>) -> Result<AbiReader<'i>> {
+		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<T> {
 	/// Read item from current position, advanding decoder
 	fn abi_read(&mut self) -> Result<T>;
+
+	/// 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<u8>, 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<u8>, 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<u8>` is restricted, `bytes` should be used instead
@@ -355,7 +382,7 @@
 	Self: AbiRead<R>,
 {
 	fn abi_read(&mut self) -> Result<Vec<R>> {
-		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(<Self as AbiRead<($($ident,)+)>>::size()) } else { None };
+				let mut subresult = self.subresult(size)?;
 				Ok((
 					$(<Self as AbiRead<$ident>>::abi_read(&mut subresult)?,)+
 				))
 			}
+
+			fn size() -> usize {
+				0 $(+ <AbiReader<'_> 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 =
+			<AbiReader<'_> as AbiRead<Vec<(address, uint256)>>>::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])
+				),
+			]
+		);
+	}
 }
modifiedpallets/fungible/CHANGELOG.mddiffbeforeafterboth
--- 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
modifiedpallets/fungible/Cargo.tomldiffbeforeafterboth
--- 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"
 
modifiedpallets/fungible/src/erc.rsdiffbeforeafterboth
--- a/pallets/fungible/src/erc.rs
+++ b/pallets/fungible/src/erc.rs
@@ -129,8 +129,32 @@
 	}
 }
 
+#[solidity_interface(name = ERC20Mintable)]
+impl<T: Config> FungibleHandle<T> {
+	/// Mint tokens for `to` account.
+	/// @param to account that will receive minted tokens
+	/// @param amount amount of tokens to mint
+	#[weight(<SelfWeightOf<T>>::create_item())]
+	fn mint(&mut self, caller: caller, to: address, amount: uint256) -> Result<bool> {
+		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(<StructureWeight<T>>::find_parent());
+		<Pallet<T>>::create_item(&self, &caller, (to, amount), &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
+}
+
 #[solidity_interface(name = ERC20UniqueExtensions)]
 impl<T: Config> FungibleHandle<T> {
+	/// 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(<SelfWeightOf<T>>::burn_from())]
 	fn burn_from(&mut self, caller: caller, from: address, amount: uint256) -> Result<bool> {
 		let caller = T::CrossAccountId::from_eth(caller);
@@ -144,12 +168,36 @@
 			.map_err(dispatch_to_evm::<T>)?;
 		Ok(true)
 	}
+
+	/// Mint tokens for multiple accounts.
+	/// @param amounts array of pairs of account address and amount
+	#[weight(<SelfWeightOf<T>>::create_multiple_items_ex(amounts.len() as u32))]
+	fn mint_bulk(&mut self, caller: caller, amounts: Vec<(address, uint256)>) -> Result<bool> {
+		let caller = T::CrossAccountId::from_eth(caller);
+		let budget = self
+			.recorder
+			.weight_calls_budget(<StructureWeight<T>>::find_parent());
+		let amounts = amounts
+			.into_iter()
+			.map(|(to, amount)| {
+				Ok((
+					T::CrossAccountId::from_eth(to),
+					amount.try_into().map_err(|_| "amount overflow")?,
+				))
+			})
+			.collect::<Result<_>>()?;
+
+		<Pallet<T>>::create_multiple_items(&self, &caller, amounts, &budget)
+			.map_err(dispatch_to_evm::<T>)?;
+		Ok(true)
+	}
 }
 
 #[solidity_interface(
 	name = UniqueFungible,
 	is(
 		ERC20,
+		ERC20Mintable,
 		ERC20UniqueExtensions,
 		Collection(common_mut, CollectionHandle<T>),
 	)
modifiedpallets/fungible/src/stubs/UniqueFungible.rawdiffbeforeafterboth

binary blob — no preview

modifiedpallets/fungible/src/stubs/UniqueFungible.soldiffbeforeafterboth
before · pallets/fungible/src/stubs/UniqueFungible.sol
1// SPDX-License-Identifier: OTHER2// This code is automatically generated34pragma solidity >=0.8.0 <0.9.0;56/// @dev common stubs holder7contract Dummy {8	uint8 dummy;9	string stub_error = "this contract is implemented in native";10}1112contract ERC165 is Dummy {13	function supportsInterface(bytes4 interfaceID)14		external15		view16		returns (bool)17	{18		require(false, stub_error);19		interfaceID;20		return true;21	}22}2324/// @title A contract that allows you to work with collections.25/// @dev the ERC-165 identifier for this interface is 0xe54be64026contract Collection is Dummy, ERC165 {27	/// Set collection property.28	///29	/// @param key Property key.30	/// @param value Propery value.31	/// @dev EVM selector for this function is: 0x2f073f66,32	///  or in textual repr: setCollectionProperty(string,bytes)33	function setCollectionProperty(string memory key, bytes memory value)34		public35	{36		require(false, stub_error);37		key;38		value;39		dummy = 0;40	}4142	/// Delete collection property.43	///44	/// @param key Property key.45	/// @dev EVM selector for this function is: 0x7b7debce,46	///  or in textual repr: deleteCollectionProperty(string)47	function deleteCollectionProperty(string memory key) public {48		require(false, stub_error);49		key;50		dummy = 0;51	}5253	/// Get collection property.54	///55	/// @dev Throws error if key not found.56	///57	/// @param key Property key.58	/// @return bytes The property corresponding to the key.59	/// @dev EVM selector for this function is: 0xcf24fd6d,60	///  or in textual repr: collectionProperty(string)61	function collectionProperty(string memory key)62		public63		view64		returns (bytes memory)65	{66		require(false, stub_error);67		key;68		dummy;69		return hex"";70	}7172	/// Set the sponsor of the collection.73	///74	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.75	///76	/// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.77	/// @dev EVM selector for this function is: 0x7623402e,78	///  or in textual repr: setCollectionSponsor(address)79	function setCollectionSponsor(address sponsor) public {80		require(false, stub_error);81		sponsor;82		dummy = 0;83	}8485	/// Set the substrate sponsor of the collection.86	///87	/// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.88	///89	/// @param sponsor Substrate address of the sponsor from whose account funds will be debited for operations with the contract.90	/// @dev EVM selector for this function is: 0xc74d6751,91	///  or in textual repr: setCollectionSponsorSubstrate(uint256)92	function setCollectionSponsorSubstrate(uint256 sponsor) public {93		require(false, stub_error);94		sponsor;95		dummy = 0;96	}9798	/// @dev EVM selector for this function is: 0x058ac185,99	///  or in textual repr: hasCollectionPendingSponsor()100	function hasCollectionPendingSponsor() public view returns (bool) {101		require(false, stub_error);102		dummy;103		return false;104	}105106	/// Collection sponsorship confirmation.107	///108	/// @dev After setting the sponsor for the collection, it must be confirmed with this function.109	/// @dev EVM selector for this function is: 0x3c50e97a,110	///  or in textual repr: confirmCollectionSponsorship()111	function confirmCollectionSponsorship() public {112		require(false, stub_error);113		dummy = 0;114	}115116	/// Remove collection sponsor.117	/// @dev EVM selector for this function is: 0x6e0326a3,118	///  or in textual repr: removeCollectionSponsor()119	function removeCollectionSponsor() public {120		require(false, stub_error);121		dummy = 0;122	}123124	/// Get current sponsor.125	///126	/// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.127	/// @dev EVM selector for this function is: 0xb66bbc14,128	///  or in textual repr: getCollectionSponsor()129	function getCollectionSponsor() public view returns (Tuple6 memory) {130		require(false, stub_error);131		dummy;132		return Tuple6(0x0000000000000000000000000000000000000000, 0);133	}134135	/// Set limits for the collection.136	/// @dev Throws error if limit not found.137	/// @param limit Name of the limit. Valid names:138	/// 	"accountTokenOwnershipLimit",139	/// 	"sponsoredDataSize",140	/// 	"sponsoredDataRateLimit",141	/// 	"tokenLimit",142	/// 	"sponsorTransferTimeout",143	/// 	"sponsorApproveTimeout"144	/// @param value Value of the limit.145	/// @dev EVM selector for this function is: 0x6a3841db,146	///  or in textual repr: setCollectionLimit(string,uint32)147	function setCollectionLimit(string memory limit, uint32 value) public {148		require(false, stub_error);149		limit;150		value;151		dummy = 0;152	}153154	/// Set limits for the collection.155	/// @dev Throws error if limit not found.156	/// @param limit Name of the limit. Valid names:157	/// 	"ownerCanTransfer",158	/// 	"ownerCanDestroy",159	/// 	"transfersEnabled"160	/// @param value Value of the limit.161	/// @dev EVM selector for this function is: 0x993b7fba,162	///  or in textual repr: setCollectionLimit(string,bool)163	function setCollectionLimit(string memory limit, bool value) public {164		require(false, stub_error);165		limit;166		value;167		dummy = 0;168	}169170	/// Get contract address.171	/// @dev EVM selector for this function is: 0xf6b4dfb4,172	///  or in textual repr: contractAddress()173	function contractAddress() public view returns (address) {174		require(false, stub_error);175		dummy;176		return 0x0000000000000000000000000000000000000000;177	}178179	/// Add collection admin by substrate address.180	/// @param newAdmin Substrate administrator address.181	/// @dev EVM selector for this function is: 0x5730062b,182	///  or in textual repr: addCollectionAdminSubstrate(uint256)183	function addCollectionAdminSubstrate(uint256 newAdmin) public {184		require(false, stub_error);185		newAdmin;186		dummy = 0;187	}188189	/// Remove collection admin by substrate address.190	/// @param admin Substrate administrator address.191	/// @dev EVM selector for this function is: 0x4048fcf9,192	///  or in textual repr: removeCollectionAdminSubstrate(uint256)193	function removeCollectionAdminSubstrate(uint256 admin) public {194		require(false, stub_error);195		admin;196		dummy = 0;197	}198199	/// Add collection admin.200	/// @param newAdmin Address of the added administrator.201	/// @dev EVM selector for this function is: 0x92e462c7,202	///  or in textual repr: addCollectionAdmin(address)203	function addCollectionAdmin(address newAdmin) public {204		require(false, stub_error);205		newAdmin;206		dummy = 0;207	}208209	/// Remove collection admin.210	///211	/// @param admin Address of the removed administrator.212	/// @dev EVM selector for this function is: 0xfafd7b42,213	///  or in textual repr: removeCollectionAdmin(address)214	function removeCollectionAdmin(address admin) public {215		require(false, stub_error);216		admin;217		dummy = 0;218	}219220	/// Toggle accessibility of collection nesting.221	///222	/// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'223	/// @dev EVM selector for this function is: 0x112d4586,224	///  or in textual repr: setCollectionNesting(bool)225	function setCollectionNesting(bool enable) public {226		require(false, stub_error);227		enable;228		dummy = 0;229	}230231	/// Toggle accessibility of collection nesting.232	///233	/// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'234	/// @param collections Addresses of collections that will be available for nesting.235	/// @dev EVM selector for this function is: 0x64872396,236	///  or in textual repr: setCollectionNesting(bool,address[])237	function setCollectionNesting(bool enable, address[] memory collections)238		public239	{240		require(false, stub_error);241		enable;242		collections;243		dummy = 0;244	}245246	/// Set the collection access method.247	/// @param mode Access mode248	/// 	0 for Normal249	/// 	1 for AllowList250	/// @dev EVM selector for this function is: 0x41835d4c,251	///  or in textual repr: setCollectionAccess(uint8)252	function setCollectionAccess(uint8 mode) public {253		require(false, stub_error);254		mode;255		dummy = 0;256	}257258	/// Add the user to the allowed list.259	///260	/// @param user Address of a trusted user.261	/// @dev EVM selector for this function is: 0x67844fe6,262	///  or in textual repr: addToCollectionAllowList(address)263	function addToCollectionAllowList(address user) public {264		require(false, stub_error);265		user;266		dummy = 0;267	}268269	/// Remove the user from the allowed list.270	///271	/// @param user Address of a removed user.272	/// @dev EVM selector for this function is: 0x85c51acb,273	///  or in textual repr: removeFromCollectionAllowList(address)274	function removeFromCollectionAllowList(address user) public {275		require(false, stub_error);276		user;277		dummy = 0;278	}279280	/// Switch permission for minting.281	///282	/// @param mode Enable if "true".283	/// @dev EVM selector for this function is: 0x00018e84,284	///  or in textual repr: setCollectionMintMode(bool)285	function setCollectionMintMode(bool mode) public {286		require(false, stub_error);287		mode;288		dummy = 0;289	}290291	/// Check that account is the owner or admin of the collection292	///293	/// @param user account to verify294	/// @return "true" if account is the owner or admin295	/// @dev EVM selector for this function is: 0x9811b0c7,296	///  or in textual repr: isOwnerOrAdmin(address)297	function isOwnerOrAdmin(address user) public view returns (bool) {298		require(false, stub_error);299		user;300		dummy;301		return false;302	}303304	/// Check that substrate account is the owner or admin of the collection305	///306	/// @param user account to verify307	/// @return "true" if account is the owner or admin308	/// @dev EVM selector for this function is: 0x68910e00,309	///  or in textual repr: isOwnerOrAdminSubstrate(uint256)310	function isOwnerOrAdminSubstrate(uint256 user) public view returns (bool) {311		require(false, stub_error);312		user;313		dummy;314		return false;315	}316317	/// Returns collection type318	///319	/// @return `Fungible` or `NFT` or `ReFungible`320	/// @dev EVM selector for this function is: 0xd34b55b8,321	///  or in textual repr: uniqueCollectionType()322	function uniqueCollectionType() public returns (string memory) {323		require(false, stub_error);324		dummy = 0;325		return "";326	}327328	/// Changes collection owner to another account329	///330	/// @dev Owner can be changed only by current owner331	/// @param newOwner new owner account332	/// @dev EVM selector for this function is: 0x13af4035,333	///  or in textual repr: setOwner(address)334	function setOwner(address newOwner) public {335		require(false, stub_error);336		newOwner;337		dummy = 0;338	}339340	/// Changes collection owner to another substrate account341	///342	/// @dev Owner can be changed only by current owner343	/// @param newOwner new owner substrate account344	/// @dev EVM selector for this function is: 0xb212138f,345	///  or in textual repr: setOwnerSubstrate(uint256)346	function setOwnerSubstrate(uint256 newOwner) public {347		require(false, stub_error);348		newOwner;349		dummy = 0;350	}351}352353/// @dev anonymous struct354struct Tuple6 {355	address field_0;356	uint256 field_1;357}358359/// @dev the ERC-165 identifier for this interface is 0x79cc6790360contract ERC20UniqueExtensions is Dummy, ERC165 {361	/// @dev EVM selector for this function is: 0x79cc6790,362	///  or in textual repr: burnFrom(address,uint256)363	function burnFrom(address from, uint256 amount) public returns (bool) {364		require(false, stub_error);365		from;366		amount;367		dummy = 0;368		return false;369	}370}371372/// @dev inlined interface373contract ERC20Events {374	event Transfer(address indexed from, address indexed to, uint256 value);375	event Approval(376		address indexed owner,377		address indexed spender,378		uint256 value379	);380}381382/// @dev the ERC-165 identifier for this interface is 0x942e8b22383contract ERC20 is Dummy, ERC165, ERC20Events {384	/// @dev EVM selector for this function is: 0x06fdde03,385	///  or in textual repr: name()386	function name() public view returns (string memory) {387		require(false, stub_error);388		dummy;389		return "";390	}391392	/// @dev EVM selector for this function is: 0x95d89b41,393	///  or in textual repr: symbol()394	function symbol() public view returns (string memory) {395		require(false, stub_error);396		dummy;397		return "";398	}399400	/// @dev EVM selector for this function is: 0x18160ddd,401	///  or in textual repr: totalSupply()402	function totalSupply() public view returns (uint256) {403		require(false, stub_error);404		dummy;405		return 0;406	}407408	/// @dev EVM selector for this function is: 0x313ce567,409	///  or in textual repr: decimals()410	function decimals() public view returns (uint8) {411		require(false, stub_error);412		dummy;413		return 0;414	}415416	/// @dev EVM selector for this function is: 0x70a08231,417	///  or in textual repr: balanceOf(address)418	function balanceOf(address owner) public view returns (uint256) {419		require(false, stub_error);420		owner;421		dummy;422		return 0;423	}424425	/// @dev EVM selector for this function is: 0xa9059cbb,426	///  or in textual repr: transfer(address,uint256)427	function transfer(address to, uint256 amount) public returns (bool) {428		require(false, stub_error);429		to;430		amount;431		dummy = 0;432		return false;433	}434435	/// @dev EVM selector for this function is: 0x23b872dd,436	///  or in textual repr: transferFrom(address,address,uint256)437	function transferFrom(438		address from,439		address to,440		uint256 amount441	) public returns (bool) {442		require(false, stub_error);443		from;444		to;445		amount;446		dummy = 0;447		return false;448	}449450	/// @dev EVM selector for this function is: 0x095ea7b3,451	///  or in textual repr: approve(address,uint256)452	function approve(address spender, uint256 amount) public returns (bool) {453		require(false, stub_error);454		spender;455		amount;456		dummy = 0;457		return false;458	}459460	/// @dev EVM selector for this function is: 0xdd62ed3e,461	///  or in textual repr: allowance(address,address)462	function allowance(address owner, address spender)463		public464		view465		returns (uint256)466	{467		require(false, stub_error);468		owner;469		spender;470		dummy;471		return 0;472	}473}474475contract UniqueFungible is476	Dummy,477	ERC165,478	ERC20,479	ERC20UniqueExtensions,480	Collection481{}
modifiedtests/src/eth/api/UniqueFungible.soldiffbeforeafterboth
--- 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
 {}
modifiedtests/src/eth/fungible.test.tsdiffbeforeafterboth
--- 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 <http://www.gnu.org/licenses/>.
 
-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',
modifiedtests/src/eth/fungibleAbi.jsondiffbeforeafterboth
--- 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" }],