diff --git a/ethereum/contracts/DOTApp.sol b/ethereum/contracts/DOTApp.sol index 9904e6da11a62..6303e45bff560 100644 --- a/ethereum/contracts/DOTApp.sol +++ b/ethereum/contracts/DOTApp.sol @@ -2,31 +2,19 @@ pragma solidity >=0.7.6; pragma experimental ABIEncoderV2; -import "@openzeppelin/contracts/access/AccessControl.sol"; import "./WrappedToken.sol"; import "./ScaleCodec.sol"; import "./OutboundChannel.sol"; enum ChannelId {Basic, Incentivized} -contract DOTApp is AccessControl { - using ScaleCodec for uint128; - - bytes32 public constant FEE_BURNER_ROLE = keccak256("FEE_BURNER_ROLE"); +contract DOTApp { + using ScaleCodec for uint256; mapping(ChannelId => Channel) public channels; bytes2 constant UNLOCK_CALL = 0x0e01; - /* - * Smallest part of DOT/KSM/ROC that is not divisible when increasing - * precision to 18 decimal places. - * - * This is used for converting between native and wrapped - * representations of DOT/KSM/ROC. - */ - uint256 private granularity; - WrappedToken public token; struct Channel { @@ -37,7 +25,6 @@ contract DOTApp is AccessControl { constructor( string memory _name, string memory _symbol, - uint256 _decimals, Channel memory _basic, Channel memory _incentivized ) { @@ -51,41 +38,28 @@ contract DOTApp is AccessControl { Channel storage c2 = channels[ChannelId.Incentivized]; c2.inbound = _incentivized.inbound; c2.outbound = _incentivized.outbound; - - _setupRole(FEE_BURNER_ROLE, _incentivized.outbound); - - granularity = 10 ** (18 - _decimals); } - function burn(bytes32 _recipient, uint256 _amount, ChannelId _channelId) public { + function burn(bytes32 _recipient, uint256 _amount, ChannelId _channelId) external { require( _channelId == ChannelId.Basic || _channelId == ChannelId.Incentivized, "Invalid channel ID" ); - require(_amount % granularity == 0, "Invalid Granularity"); - token.burn(msg.sender, _amount, abi.encodePacked(_recipient)); OutboundChannel channel = OutboundChannel(channels[_channelId].outbound); - bytes memory call = encodeCall(msg.sender, _recipient, unwrap(_amount)); + bytes memory call = encodeCall(msg.sender, _recipient, _amount); channel.submit(msg.sender, call); } - function mint(bytes32 _sender, address _recipient, uint128 _amount) public { + function mint(bytes32 _sender, address _recipient, uint256 _amount) external { // TODO: Ensure message sender is a known inbound channel - token.mint(_recipient, wrap(_amount), abi.encodePacked(_sender)); + token.mint(_recipient, _amount, abi.encodePacked(_sender)); } - function burnFee(address _account, uint256 _amount) external returns (uint128) { - require(hasRole(FEE_BURNER_ROLE, msg.sender), "ACCESS_FORBIDDEN"); - require(_amount % granularity == 0, "INVALID_GRANULARITY"); - token.burn(_account, _amount, ""); - return unwrap(_amount); - } - - function encodeCall(address _sender, bytes32 _recipient, uint128 _amount) + function encodeCall(address _sender, bytes32 _recipient, uint256 _amount) private pure returns (bytes memory) @@ -96,33 +70,8 @@ contract DOTApp is AccessControl { _sender, byte(0x00), // Encoding recipient as MultiAddress::Id _recipient, - _amount.encode128() + _amount.encode256() ); } - /* - * Convert native DOT/KSM/ROC to the wrapped equivalent. - * - * SAFETY: No need for SafeMath.mul as overflow is not possible for - * 0 <= granularity <= 10 ^ 8. - * - * Can verify in Rust using this snippet: - * - * let granularity = U256::from(100000000u64); - * U256::from(u128::MAX).checked_mul(granularity).unwrap(); - * - */ - function wrap(uint128 _value) view internal returns (uint256) { - return uint256(_value) * granularity; - } - - /* - * Convert wrapped DOT/KSM/ROC to its native equivalent. - * - * SAFETY: No need for SafeMath.div since granularity is - * configured to be non-zero. - */ - function unwrap(uint256 _value) view internal returns (uint128) { - return uint128(_value / granularity); - } } diff --git a/ethereum/migrations/2_next.js b/ethereum/migrations/2_next.js index 392b59820269b..f88678a994ae1 100644 --- a/ethereum/migrations/2_next.js +++ b/ethereum/migrations/2_next.js @@ -80,7 +80,6 @@ module.exports = function(deployer, network, accounts) { DOTApp, "Snowfork DOT", "SnowDOT", - 12, // On Kusama and Rococo, KSM/ROC tokens have 12 decimal places { inbound: channels.basic.inbound.instance.address, outbound: channels.basic.outbound.instance.address, diff --git a/ethereum/test/test_dot_app.js b/ethereum/test/test_dot_app.js index 4f0acf1d53300..dde650404c68d 100644 --- a/ethereum/test/test_dot_app.js +++ b/ethereum/test/test_dot_app.js @@ -54,7 +54,7 @@ contract("DOTApp", function (accounts) { describe("minting", function () { beforeEach(async function () { this.erc1820 = await singletons.ERC1820Registry(owner); - [this.channels, this.app] = await deployAppContractWithChannels(DOTApp, "Snowfork DOT", "SnowDOT", 10); + [this.channels, this.app] = await deployAppContractWithChannels(DOTApp, "Snowfork DOT", "SnowDOT"); this.token = await Token.at(await this.app.token()); }); @@ -67,7 +67,7 @@ contract("DOTApp", function (accounts) { let tx = await this.app.mint( addressBytes(POLKADOT_ADDRESS), user, - amountNative.toString(), + amountWrapped.toString(), { from: owner, value: 0 @@ -94,7 +94,7 @@ contract("DOTApp", function (accounts) { describe("burning", function () { beforeEach(async function () { this.erc1820 = await singletons.ERC1820Registry(owner); - [this.channels, this.app] = await deployAppContractWithChannels(DOTApp, "Snowfork DOT", "SnowDOT", 10); + [this.channels, this.app] = await deployAppContractWithChannels(DOTApp, "Snowfork DOT", "SnowDOT"); this.token = await Token.at(await this.app.token()); // Mint 2 wrapped DOT @@ -134,15 +134,6 @@ contract("DOTApp", function (accounts) { beforeUserBalance.minus(afterUserBalance).should.be.bignumber.equal(amountWrapped); }); - it("should revert on bad granularity", async function () { - const amount = BigNumber("1"); - - const err = await burnTokens(this.app, user, POLKADOT_ADDRESS, amount, ChannelId.Basic) - .should.be.rejected; - - err.reason.should.be.equal("Invalid Granularity"); - }); - it("should send payload to the basic outbound channel", async function () { const amountWrapped = wrapped(BigNumber("10000000000")); let { receipt } = await burnTokens(this.app, user, POLKADOT_ADDRESS, amountWrapped, ChannelId.Basic).should.be.fulfilled; diff --git a/parachain/pallets/dot-app/src/lib.rs b/parachain/pallets/dot-app/src/lib.rs index 7822b318f668d..338c9c8aa7f6c 100644 --- a/parachain/pallets/dot-app/src/lib.rs +++ b/parachain/pallets/dot-app/src/lib.rs @@ -11,20 +11,24 @@ use frame_support::{ ExistenceRequirement::{KeepAlive, AllowDeath}, } }; -use sp_runtime::traits::StaticLookup; -use sp_std::prelude::*; -use sp_core::H160; +use sp_std::{ + prelude::*, +}; +use sp_core::{H160, U256}; use sp_runtime::{ ModuleId, - traits::AccountIdConversion, - SaturatedConversion, + traits::{StaticLookup, AccountIdConversion}, }; use artemis_core::{ChannelId, OutboundRouter}; -mod payload; +use primitives::{wrap, unwrap}; + use payload::OutboundPayload; +mod payload; +mod primitives; + #[cfg(test)] mod mock; @@ -43,6 +47,8 @@ pub trait Config: system::Config { type CallOrigin: EnsureOrigin; type ModuleId: Get; + + type Decimals: Get; } decl_storage! { @@ -77,6 +83,16 @@ decl_module! { fn deposit_event() = default; + fn integrity_test() { + sp_io::TestExternalities::new_empty().execute_with(|| { + let allowed_decimals: &[u32] = &[10, 12]; + let decimals = T::Decimals::get(); + assert!( + allowed_decimals.contains(&decimals) + ) + }); + } + #[weight = 0] #[transactional] pub fn lock(origin, channel_id: ChannelId, recipient: H160, amount: BalanceOf) -> DispatchResult { @@ -84,10 +100,15 @@ decl_module! { T::Currency::transfer(&who, &Self::account_id(), amount, AllowDeath)?; + let amount_wrapped = match wrap::(amount, T::Decimals::get()) { + Some(value) => value, + None => panic!("Runtime is misconfigured"), + }; + let message = OutboundPayload { sender: who.clone(), recipient: recipient.clone(), - amount: amount.saturated_into::(), + amount: amount_wrapped, }; T::OutboundRouter::submit(channel_id, &who, Address::get(), &message.encode())?; @@ -97,15 +118,20 @@ decl_module! { #[weight = 0] #[transactional] - pub fn unlock(origin, sender: H160, recipient: ::Source, amount: BalanceOf) -> DispatchResult { + pub fn unlock(origin, sender: H160, recipient: ::Source, amount: U256) -> DispatchResult { let who = T::CallOrigin::ensure_origin(origin)?; if who != Address::get() { return Err(DispatchError::BadOrigin.into()); } + let amount_unwrapped = match unwrap::(amount, T::Decimals::get()) { + Some(value) => value, + None => panic!("Runtime is misconfigured"), + }; + let recipient = T::Lookup::lookup(recipient)?; - T::Currency::transfer(&Self::account_id(), &recipient, amount, KeepAlive)?; - Self::deposit_event(RawEvent::Unlocked(sender, recipient, amount)); + T::Currency::transfer(&Self::account_id(), &recipient, amount_unwrapped, KeepAlive)?; + Self::deposit_event(RawEvent::Unlocked(sender, recipient, amount_unwrapped)); Ok(()) } } diff --git a/parachain/pallets/dot-app/src/mock.rs b/parachain/pallets/dot-app/src/mock.rs index 6fd06d21e28ea..e8b14fb54b643 100644 --- a/parachain/pallets/dot-app/src/mock.rs +++ b/parachain/pallets/dot-app/src/mock.rs @@ -106,6 +106,7 @@ impl pallet_balances::Config for Test { parameter_types! { pub const DotModuleId: ModuleId = ModuleId(*b"s/dotapp"); + pub const Decimals: u32 = 12; } impl dot_app::Config for Test { @@ -114,6 +115,7 @@ impl dot_app::Config for Test { type OutboundRouter = MockOutboundRouter; type CallOrigin = artemis_dispatch::EnsureEthereumAccount; type ModuleId = DotModuleId; + type Decimals = Decimals; } pub fn new_tester() -> sp_io::TestExternalities { diff --git a/parachain/pallets/dot-app/src/payload.rs b/parachain/pallets/dot-app/src/payload.rs index d34dca061d77d..c984b8bd0db5c 100644 --- a/parachain/pallets/dot-app/src/payload.rs +++ b/parachain/pallets/dot-app/src/payload.rs @@ -1,4 +1,4 @@ -use sp_core::RuntimeDebug; +use sp_core::{U256, RuntimeDebug}; use sp_std::prelude::*; use codec::Encode; @@ -10,7 +10,7 @@ use artemis_ethereum::H160; pub struct OutboundPayload { pub sender: AccountId, pub recipient: H160, - pub amount: u128, + pub amount: U256, } impl OutboundPayload { @@ -20,8 +20,8 @@ impl OutboundPayload { let tokens = vec![ Token::FixedBytes(self.sender.encode()), Token::Address(self.recipient), - Token::Uint(self.amount.into()) + Token::Uint(self.amount) ]; - ethabi::encode_function("mint(bytes32,address,uint128)", tokens.as_ref()) + ethabi::encode_function("mint(bytes32,address,uint256)", tokens.as_ref()) } } diff --git a/parachain/pallets/dot-app/src/primitives.rs b/parachain/pallets/dot-app/src/primitives.rs new file mode 100644 index 0000000000000..987495848c0e1 --- /dev/null +++ b/parachain/pallets/dot-app/src/primitives.rs @@ -0,0 +1,81 @@ +use sp_core::U256; +use sp_runtime::traits::CheckedConversion; + +use crate::{Config, BalanceOf}; + +pub fn unwrap(value: U256, decimals: u32) -> Option> { + let granularity = match granularity(decimals) { + Some(value) => value, + None => return None, + }; + + let unwrapped = match value.checked_div(granularity) { + Some(value) => value, + None => return None, + }; + + unwrapped.low_u128().checked_into() +} + +pub fn wrap(value: BalanceOf, decimals: u32) -> Option { + let granularity = match granularity(decimals) { + Some(value) => value, + None => return None, + }; + + let value_u256 = match value.checked_into::() { + Some(value) => U256::from(value), + None => return None, + }; + + value_u256.checked_mul(granularity) +} + +fn granularity(decimals: u32) -> Option { + Some(U256::from(u64::checked_pow(10, 18 - decimals)?)) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::mock::{Test, Balance}; + #[test] + fn should_wrap_without_overflow() { + // largest possible value + let max_possible_amount = Balance::MAX; + let min_possible_decimals = 0; + assert_ne!( + wrap::(max_possible_amount, min_possible_decimals), + None + ); + + // smallest possible value + let min_possible_amount = 1; + let max_possible_decimals = 18; + assert_ne!( + wrap::(min_possible_amount, max_possible_decimals), + None + ) + } + + #[test] + fn should_unwrap_without_overflow() { + // largest possible value + let max_possible_amount = U256::from(Balance::MAX); + let min_possible_decimals = 0; + assert_ne!( + unwrap::(max_possible_amount, min_possible_decimals), + None + ); + + // smallest possible value + let min_possible_amount = U256::from(1); + let max_possible_decimals = 18; + assert_ne!( + unwrap::(min_possible_amount, max_possible_decimals), + None + ) + } + +} diff --git a/parachain/pallets/dot-app/src/tests.rs b/parachain/pallets/dot-app/src/tests.rs index 0b93e4af9f3d7..544c15b7f83b8 100644 --- a/parachain/pallets/dot-app/src/tests.rs +++ b/parachain/pallets/dot-app/src/tests.rs @@ -1,12 +1,13 @@ +use crate::Config; use crate::mock::{Test, AccountId, Balances, DOTApp, Event, Origin, System, new_tester}; use frame_support::{assert_noop, assert_ok, dispatch::{ DispatchError, }, - traits::Currency + traits::Currency, }; use sp_keyring::AccountKeyring as Keyring; -use sp_core::{H160, U256}; +use sp_core::H160; use artemis_core::ChannelId; fn last_event() -> Event { @@ -44,8 +45,9 @@ fn should_unlock() { let peer_contract = H160::repeat_byte(1); let sender = H160::repeat_byte(7); let recipient: AccountId = Keyring::Bob.into(); - let amount = 100; let balance = 500; + let amount = 100; + let amount_wrapped = crate::primitives::wrap::(amount, ::Decimals::get()).unwrap(); let _ = Balances::deposit_creating(&DOTApp::account_id(), balance); @@ -54,14 +56,14 @@ fn should_unlock() { artemis_dispatch::Origin(peer_contract).into(), sender, recipient.clone(), - amount + amount_wrapped, ) ); assert_eq!(Balances::total_balance(&recipient), amount); assert_eq!(Balances::total_balance(&DOTApp::account_id()), balance - amount); assert_eq!( - Event::dot_app(crate::Event::::Unlocked(sender, recipient, amount.into())), + Event::dot_app(crate::Event::::Unlocked(sender, recipient, amount)), last_event() ); }); @@ -73,8 +75,9 @@ fn should_not_unlock_on_bad_origin_failure() { let unknown_peer_contract = H160::repeat_byte(64); let sender = H160::repeat_byte(7); let recipient: AccountId = Keyring::Bob.into(); - let amount = 100; let balance = 500; + let amount = 100; + let amount_wrapped = crate::primitives::wrap::(amount, ::Decimals::get()).unwrap(); let _ = Balances::deposit_creating(&DOTApp::account_id(), balance); @@ -83,7 +86,7 @@ fn should_not_unlock_on_bad_origin_failure() { artemis_dispatch::Origin(unknown_peer_contract).into(), sender, recipient.clone(), - amount + amount_wrapped, ), DispatchError::BadOrigin ); @@ -93,12 +96,10 @@ fn should_not_unlock_on_bad_origin_failure() { Origin::signed(Keyring::Alice.into()), sender, recipient.clone(), - amount + amount_wrapped, ), DispatchError::BadOrigin ); - - assert_eq!(Balances::total_balance(&DOTApp::account_id()), balance); }); } @@ -116,15 +117,9 @@ fn should_not_lock_on_add_commitment_failure() { Origin::signed(sender.clone()), ChannelId::Basic, recipient.clone(), - amount), + amount.into() + ), DispatchError::Other("some error!") ); }); } - -// Used to prove safety of conversion from DOT to wrapped DOT (See BaseDOTApp.sol) -#[test] -fn should_max_dot_convert_to_wrapped_dot() { - let granularity = U256::from(100000000u64); // 10 ** 8 - U256::from(u128::MAX).checked_mul(granularity).unwrap(); -} diff --git a/parachain/runtime/src/lib.rs b/parachain/runtime/src/lib.rs index c1ac478c5277b..177c1d8d71fb3 100644 --- a/parachain/runtime/src/lib.rs +++ b/parachain/runtime/src/lib.rs @@ -477,6 +477,7 @@ impl erc20_app::Config for Runtime { parameter_types! { pub const DotModuleId: ModuleId = ModuleId(*b"s/dotapp"); + pub const Decimals: u32 = 12; } impl dot_app::Config for Runtime { @@ -485,6 +486,7 @@ impl dot_app::Config for Runtime { type OutboundRouter = SimpleOutboundRouter; type CallOrigin = EnsureEthereumAccount; type ModuleId = DotModuleId; + type Decimals = Decimals; } construct_runtime!(