Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Commit

Permalink
DOT App V2 (paritytech#309)
Browse files Browse the repository at this point in the history
  • Loading branch information
vgeddes authored Mar 10, 2021
1 parent 503042c commit f6b3b45
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 104 deletions.
67 changes: 8 additions & 59 deletions ethereum/contracts/DOTApp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -37,7 +25,6 @@ contract DOTApp is AccessControl {
constructor(
string memory _name,
string memory _symbol,
uint256 _decimals,
Channel memory _basic,
Channel memory _incentivized
) {
Expand All @@ -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)
Expand All @@ -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);
}
}
1 change: 0 additions & 1 deletion ethereum/migrations/2_next.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 3 additions & 12 deletions ethereum/test/test_dot_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
46 changes: 36 additions & 10 deletions parachain/pallets/dot-app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -43,6 +47,8 @@ pub trait Config: system::Config {
type CallOrigin: EnsureOrigin<Self::Origin, Success=H160>;

type ModuleId: Get<ModuleId>;

type Decimals: Get<u32>;
}

decl_storage! {
Expand Down Expand Up @@ -77,17 +83,32 @@ 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<T>) -> DispatchResult {
let who = ensure_signed(origin)?;

T::Currency::transfer(&who, &Self::account_id(), amount, AllowDeath)?;

let amount_wrapped = match wrap::<T>(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::<u128>(),
amount: amount_wrapped,
};

T::OutboundRouter::submit(channel_id, &who, Address::get(), &message.encode())?;
Expand All @@ -97,15 +118,20 @@ decl_module! {

#[weight = 0]
#[transactional]
pub fn unlock(origin, sender: H160, recipient: <T::Lookup as StaticLookup>::Source, amount: BalanceOf<T>) -> DispatchResult {
pub fn unlock(origin, sender: H160, recipient: <T::Lookup as StaticLookup>::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::<T>(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(())
}
}
Expand Down
2 changes: 2 additions & 0 deletions parachain/pallets/dot-app/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -114,6 +115,7 @@ impl dot_app::Config for Test {
type OutboundRouter = MockOutboundRouter<Self::AccountId>;
type CallOrigin = artemis_dispatch::EnsureEthereumAccount;
type ModuleId = DotModuleId;
type Decimals = Decimals;
}

pub fn new_tester() -> sp_io::TestExternalities {
Expand Down
8 changes: 4 additions & 4 deletions parachain/pallets/dot-app/src/payload.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use sp_core::RuntimeDebug;
use sp_core::{U256, RuntimeDebug};
use sp_std::prelude::*;
use codec::Encode;

Expand All @@ -10,7 +10,7 @@ use artemis_ethereum::H160;
pub struct OutboundPayload<AccountId: Encode> {
pub sender: AccountId,
pub recipient: H160,
pub amount: u128,
pub amount: U256,
}

impl<AccountId: Encode> OutboundPayload<AccountId> {
Expand All @@ -20,8 +20,8 @@ impl<AccountId: Encode> OutboundPayload<AccountId> {
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())
}
}
81 changes: 81 additions & 0 deletions parachain/pallets/dot-app/src/primitives.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use sp_core::U256;
use sp_runtime::traits::CheckedConversion;

use crate::{Config, BalanceOf};

pub fn unwrap<T: Config>(value: U256, decimals: u32) -> Option<BalanceOf<T>> {
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<T: Config>(value: BalanceOf<T>, decimals: u32) -> Option<U256> {
let granularity = match granularity(decimals) {
Some(value) => value,
None => return None,
};

let value_u256 = match value.checked_into::<u128>() {
Some(value) => U256::from(value),
None => return None,
};

value_u256.checked_mul(granularity)
}

fn granularity(decimals: u32) -> Option<U256> {
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::<Test>(max_possible_amount, min_possible_decimals),
None
);

// smallest possible value
let min_possible_amount = 1;
let max_possible_decimals = 18;
assert_ne!(
wrap::<Test>(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::<Test>(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::<Test>(min_possible_amount, max_possible_decimals),
None
)
}

}
Loading

0 comments on commit f6b3b45

Please sign in to comment.