Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix send_transfer #2

Merged
merged 14 commits into from
Sep 11, 2023
10 changes: 10 additions & 0 deletions module-system/module-implementations/sov-bank/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,16 @@ impl<C: sov_modules_api::Context> Bank<C> {
.get(&token_address, working_set)
.and_then(|token| token.balances.get(&user_address, working_set))
}

/// Get the name of a token by address
pub fn get_token_name(
&self,
token_address: &C::Address,
working_set: &mut WorkingSet<C::Storage>,
) -> Option<String> {
let token = self.tokens.get(token_address, working_set);
token.map(|token| token.name)
}
}

/// Creates a new prefix from an already existing prefix `parent_prefix` and a `token_address`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn initial_and_deployed_token() {
let token_address = get_token_address::<C>(&token_name, sender_address.as_ref(), salt);
let create_token_message = CallMessage::CreateToken::<C> {
salt,
token_name,
token_name: token_name.clone(),
initial_balance,
minter_address,
authorized_minters: vec![minter_address],
Expand All @@ -38,6 +38,11 @@ fn initial_and_deployed_token() {
let sender_balance = bank.get_balance_of(sender_address, token_address, &mut working_set);
assert!(sender_balance.is_none());

let observed_token_name = bank
.get_token_name(&token_address, &mut working_set)
.expect("Token is missing its name");
assert_eq!(&token_name, &observed_token_name);

let minter_balance = bank.get_balance_of(minter_address, token_address, &mut working_set);

assert_eq!(Some(initial_balance), minter_balance);
Expand Down
131 changes: 131 additions & 0 deletions module-system/module-implementations/sov-ibc-transfer/src/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use std::cell::RefCell;
use std::rc::Rc;

use anyhow::Result;
use ibc::applications::transfer::context::TokenTransferExecutionContext;
use ibc::applications::transfer::msgs::transfer::MsgTransfer;
use ibc::applications::transfer::packet::PacketData;
use ibc::applications::transfer::{send_transfer, Memo, PrefixedCoin};
use ibc::core::ics04_channel::timeout::TimeoutHeight;
use ibc::core::ics24_host::identifier::{ChannelId, PortId};
use ibc::core::timestamp::Timestamp;
use ibc::core::ExecutionContext;
use ibc::Signer;
use sov_state::WorkingSet;

use crate::context::EscrowExtraData;
use crate::Transfer;

#[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq)]
pub struct SDKTokenTransfer<C: sov_modules_api::Context> {
/// the port on which the packet will be sent
pub port_id_on_a: PortId,
/// the channel by which the packet will be sent
pub chan_id_on_a: ChannelId,
/// Timeout height relative to the current block height.
/// The timeout is disabled when set to None.
pub timeout_height_on_b: TimeoutHeight,
/// Timeout timestamp relative to the current block timestamp.
/// The timeout is disabled when set to 0.
pub timeout_timestamp_on_b: Timestamp,
/// The address of the token to be sent
pub token_address: C::Address,
/// The amount of tokens sent
pub amount: sov_bank::Amount,
/// The address of the token sender
pub sender: Signer,
/// The address of the token receiver on the counterparty chain
pub receiver: Signer,
/// Additional note associated with the message
pub memo: Memo,
}

impl<C> Transfer<C>
where
C: sov_modules_api::Context,
{
pub fn transfer(
&self,
sdk_token_transfer: SDKTokenTransfer<C>,
execution_context: &mut impl ExecutionContext,
token_ctx: &mut impl TokenTransferExecutionContext<EscrowExtraData<C>>,
working_set: Rc<RefCell<&mut WorkingSet<C::Storage>>>,
) -> Result<sov_modules_api::CallResponse> {
let msg_transfer: MsgTransfer = {
let denom = {
let token_name = self
.bank
.get_token_name(
&sdk_token_transfer.token_address,
&mut working_set.borrow_mut(),
)
.ok_or(anyhow::anyhow!(
"Token with address {} doesn't exist",
sdk_token_transfer.token_address
))?;

if self.token_was_created_by_ibc(
&token_name,
&sdk_token_transfer.token_address,
&mut working_set.borrow_mut(),
) {
// The token was created by the IBC module, and the ICS-20
// denom was stored in the token name. Hence, we need to use
// the token name as denom.
token_name
} else {
// This applies to all other tokens created on this
// sovereign SDK chain. The token name is not guaranteed to
// be unique, and hence we must use the token address (which
// is guaranteed to be unique) as the ICS-20 denom to ensure
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am seeing, when transferring tokens between two Sovereign chains, we exclusively use the token address as the denom, disregarding the token's name.
Whether the token lives on the source chain, in which case the denom is based on the token address, or if the IBC mints the token on the source chain (which represents a token on the destination chain). In latter, we use a prefixed denom, where the base denom comes from a MsgRecvPacket, which was earlier crafted by relayers based on an emitted TransferEvent from the destination chain...Where the denom is also set as the token address!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when transferring tokens between two Sovereign chains

In the initial version, we will only support sending tokens to Tendermint-based chains, as we will only have a tendermint light client installed.

In latter, we use a prefixed denom, where the base denom comes from a MsgRecvPacket, which was earlier crafted by relayers based on an emitted TransferEvent from the destination chain...Where the denom is also set as the token address!

However, if we were to support Sovereign <-> Sovereign transfers, then the denom of tokens we receive would indeed represent the address of the token on the other chain. Note that the token we mint on our side would have a different address, but its denom would be set to the string representation of the address of the token on the other chain. However, for received tokens, we treat the token name as a black box; we never try to interpret it in any way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the denom of tokens we receive would indeed represent the address of the token on the other chain

Exactly. And looks like it holds true for bunch of other non-Cosmos chains, which makes string reps of token addresses the most common denom.

By the way, the transfer module stores the "denom → token_address" in the minted_token field. This allows us to map, e.g. the channel-1/port-1/0xb79…4f5ea received from MsgRecvPacket to the corresponding token address on the source chain. So, I'm still a bit puzzled about why can't we work with the MsgTransfer and include the string reps of the token's address as denom? What required us to define SDKTokenTransfer on top of that?

Anyway, as chatted earlier, Let's dive deeper into these details when writing tests.

// uniqueness.
sdk_token_transfer.token_address.to_string()
}
Farhad-Shabani marked this conversation as resolved.
Show resolved Hide resolved
};

MsgTransfer {
port_id_on_a: sdk_token_transfer.port_id_on_a,
chan_id_on_a: sdk_token_transfer.chan_id_on_a,
packet_data: PacketData {
token: PrefixedCoin {
denom: denom
.parse()
.map_err(|_err| anyhow::anyhow!("Failed to parse denom {denom}"))?,
amount: sdk_token_transfer.amount.into(),
},
sender: sdk_token_transfer.sender,
receiver: sdk_token_transfer.receiver,
memo: sdk_token_transfer.memo,
},
timeout_height_on_b: sdk_token_transfer.timeout_height_on_b,
timeout_timestamp_on_b: sdk_token_transfer.timeout_timestamp_on_b,
}
};

send_transfer(
execution_context,
token_ctx,
msg_transfer,
&EscrowExtraData {
token_address: sdk_token_transfer.token_address,
},
)?;

todo!()
}

/// This function returns true if the token to be sent was created by IBC.
/// This only occurs for tokens that are native to and received from other
/// chains; i.e. for tokens for which this chain isn't the source.
fn token_was_created_by_ibc(
&self,
token_name: &str,
token_address: &C::Address,
working_set: &mut WorkingSet<C::Storage>,
) -> bool {
match self.minted_tokens.get(token_name, working_set) {
Some(minted_token_address) => minted_token_address == *token_address,
None => false,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,9 @@ where
coin: &PrefixedCoin,
) -> Result<(), TokenTransferError> {
let token_address = {
let mut hasher = <C::Hasher as Digest>::new();
hasher.update(coin.denom.to_string());
let denom_hash = hasher.finalize().to_vec();

self.transfer_mod
.minted_tokens
.get(&denom_hash, &mut self.working_set.borrow_mut())
.get(&coin.denom.to_string(), &mut self.working_set.borrow_mut())
.ok_or(TokenTransferError::InvalidCoin {
coin: coin.to_string(),
})?
Expand Down Expand Up @@ -246,13 +242,9 @@ where
// ensure that escrow account has enough balance
let escrow_balance: transfer::Amount = {
let token_address = {
let mut hasher = <C::Hasher as Digest>::new();
hasher.update(coin.denom.to_string());
let denom_hash = hasher.finalize().to_vec();

self.transfer_mod
.escrowed_tokens
.get(&denom_hash, &mut self.working_set.borrow_mut())
.get(&coin.denom.to_string(), &mut self.working_set.borrow_mut())
.ok_or(TokenTransferError::InvalidCoin {
coin: coin.to_string(),
})?
Expand Down Expand Up @@ -295,17 +287,14 @@ where
account: &Self::AccountId,
coin: &PrefixedCoin,
) -> Result<(), TokenTransferError> {
let denom = coin.denom.to_string();

// 1. if token address doesn't exist in `minted_tokens`, then create a new token and store in `minted_tokens`
let token_address: C::Address = {
// TODO: Put this in a function
let mut hasher = <C::Hasher as Digest>::new();
hasher.update(coin.denom.to_string());
let denom_hash = hasher.finalize().to_vec();

let maybe_token_address = self
.transfer_mod
.minted_tokens
.get(&denom_hash, &mut self.working_set.borrow_mut());
.get(&denom, &mut self.working_set.borrow_mut());

match maybe_token_address {
Some(token_address) => token_address,
Expand Down Expand Up @@ -334,7 +323,7 @@ where

// Store the new address in `minted_tokens`
self.transfer_mod.minted_tokens.set(
&denom_hash,
&denom,
&new_token_addr,
&mut self.working_set.borrow_mut(),
);
Expand Down Expand Up @@ -374,13 +363,9 @@ where
coin: &PrefixedCoin,
) -> Result<(), TokenTransferError> {
let token_address = {
let mut hasher = <C::Hasher as Digest>::new();
hasher.update(coin.denom.to_string());
let denom_hash = hasher.finalize().to_vec();

self.transfer_mod
.minted_tokens
.get(&denom_hash, &mut self.working_set.borrow_mut())
.get(&coin.denom.to_string(), &mut self.working_set.borrow_mut())
.ok_or(TokenTransferError::InvalidCoin {
coin: coin.to_string(),
})?
Expand Down Expand Up @@ -417,17 +402,11 @@ where
) -> Result<(), TokenTransferError> {
// 1. ensure that token exists in `self.escrowed_tokens` map, which is
// necessary information when unescrowing tokens
{
let mut hasher = <C::Hasher as Digest>::new();
hasher.update(coin.denom.to_string());
let denom_hash = hasher.finalize().to_vec();

self.transfer_mod.escrowed_tokens.set(
&denom_hash,
&extra.token_address,
&mut self.working_set.borrow_mut(),
);
}
self.transfer_mod.escrowed_tokens.set(
&coin.denom.to_string(),
&extra.token_address,
&mut self.working_set.borrow_mut(),
);

// 2. transfer coins to escrow account
{
Expand All @@ -454,18 +433,13 @@ where
to_account: &Self::AccountId,
coin: &PrefixedCoin,
) -> Result<(), TokenTransferError> {
let token_address = {
let mut hasher = <C::Hasher as Digest>::new();
hasher.update(coin.denom.to_string());
let denom_hash = hasher.finalize().to_vec();

self.transfer_mod
.escrowed_tokens
.get(&denom_hash, &mut self.working_set.borrow_mut())
.ok_or(TokenTransferError::InvalidCoin {
coin: coin.to_string(),
})?
};
let token_address = self
.transfer_mod
.escrowed_tokens
.get(&coin.denom.to_string(), &mut self.working_set.borrow_mut())
.ok_or(TokenTransferError::InvalidCoin {
coin: coin.to_string(),
})?;

// transfer coins out of escrow account to `to_account`
{
Expand Down
19 changes: 7 additions & 12 deletions module-system/module-implementations/sov-ibc-transfer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod call;
pub mod context;
mod genesis;

Expand Down Expand Up @@ -26,28 +27,22 @@ pub struct Transfer<C: sov_modules_api::Context> {
#[module]
pub(crate) bank: sov_bank::Bank<C>,

/// Keeps track of the address of each token we minted.
/// The index is the hash of the token denom (using the hasher `C::Hasher`).
/// Note: we use `Vec<u8>` instead of `Output<C::Hasher>` because `C::Hasher`
/// is not cloneable, and we currently need our module to be cloneable
/// Keeps track of the address of each token we minted by token denom.
#[state]
pub(crate) minted_tokens: sov_state::StateMap<Vec<u8>, C::Address>,
pub(crate) minted_tokens: sov_state::StateMap<String, C::Address>,

/// Keeps track of the address of each token we escrowed as a function of
/// the (hash of) the token denom. We need this map because we have the
/// token address information when escrowing the tokens (i.e. when someone
/// calls a `send_transfer()`), but not when unescrowing tokens (i.e in a
/// the token denom. We need this map because we have the token address
/// information when escrowing the tokens (i.e. when someone calls a
/// `send_transfer()`), but not when unescrowing tokens (i.e in a
/// `recv_packet`), in which case the only information we have is the ICS 20
/// denom, and amount. Given that every token that is unescrowed has been
/// previously escrowed, our strategy to get the token address associated
/// with a denom is
/// 1. when tokens are escrowed, save the mapping `denom -> token address`
/// 2. when tokens are unescrowed, lookup the token address by `denom`
///
/// Note: Even though we could store the `denom: String` as a key, we prefer
/// to hash it to the key a constant size.
#[state]
pub(crate) escrowed_tokens: sov_state::StateMap<Vec<u8>, C::Address>,
pub(crate) escrowed_tokens: sov_state::StateMap<String, C::Address>,
}

impl<C: sov_modules_api::Context> sov_modules_api::Module for Transfer<C> {
Expand Down
Loading