From 9ec1833f20b1319ff916ddecf0dd86d5b21f706c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 11 Jul 2023 10:15:26 +0100 Subject: [PATCH 001/103] Add Eth whitelist storage keys --- core/src/ledger/eth_bridge/storage/mod.rs | 1 + .../ledger/eth_bridge/storage/whitelist.rs | 117 ++++++++++++++++++ .../src/storage/eth_bridge_queries.rs | 51 +++++++- 3 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 core/src/ledger/eth_bridge/storage/whitelist.rs diff --git a/core/src/ledger/eth_bridge/storage/mod.rs b/core/src/ledger/eth_bridge/storage/mod.rs index 0906be3d5d..b777caa265 100644 --- a/core/src/ledger/eth_bridge/storage/mod.rs +++ b/core/src/ledger/eth_bridge/storage/mod.rs @@ -1,5 +1,6 @@ //! Functionality for accessing the storage subspace pub mod bridge_pool; +pub mod whitelist; pub mod wrapped_erc20s; use super::ADDRESS; diff --git a/core/src/ledger/eth_bridge/storage/whitelist.rs b/core/src/ledger/eth_bridge/storage/whitelist.rs new file mode 100644 index 0000000000..15ebddfa8b --- /dev/null +++ b/core/src/ledger/eth_bridge/storage/whitelist.rs @@ -0,0 +1,117 @@ +//! ERC20 token whitelist storage data. +//! +//! These storage keys should only ever be written to by governance, +//! or `InitChain`. + +use std::str::FromStr; + +use super::super::ADDRESS as BRIDGE_ADDRESS; +use super::{prefix as ethbridge_key_prefix, wrapped_erc20s}; +use crate::types::ethereum_events::EthAddress; +use crate::types::storage; +use crate::types::storage::DbKeySeg; +use crate::types::token::{denom_key, minted_balance_key}; + +mod segments { + //! Storage key segments under the token whitelist. + use namada_macros::StorageKeys; + + use crate::types::address::Address; + use crate::types::storage::{DbKeySeg, Key}; + + /// The name of the main storage segment. + pub(super) const MAIN_SEGMENT: &str = "whitelist"; + + /// Storage key segments under the token whitelist. + #[derive(StorageKeys)] + pub(super) struct Segments { + /// Whether an ERC20 asset is whitelisted or not. + pub whitelisted: &'static str, + /// The token cap of an ERC20 asset. + pub cap: &'static str, + } + + /// All the values of the generated [`Segments`]. + pub(super) const VALUES: Segments = Segments::VALUES; + + /// Listing of each of the generated [`Segments`]. + pub(super) const ALL: &[&str] = Segments::ALL; +} + +/// Represents the type of a key relating to whitelisted ERC20. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +pub enum KeyType { + /// Whether an ERC20 asset is whitelisted or not. + Whitelisted, + /// The token cap of an ERC20 asset. + Cap, + /// The current supply of a wrapped ERC20 asset, + /// circulating in Namada. + WrappedSupply, + /// The denomination of the ERC20 asset. + Denomination, +} + +/// Whitelisted ERC20 token storage sub-space. +pub struct Key { + /// The specific ERC20 as identified by its Ethereum address. + pub asset: EthAddress, + /// The type of this key. + pub suffix: KeyType, +} + +/// Return the whitelist storage key sub-space prefix. +fn whitelist_prefix(asset: &EthAddress) -> storage::Key { + ethbridge_key_prefix() + .push(&segments::MAIN_SEGMENT.to_owned()) + .expect("Should be able to push a storage key segment") + .push(&asset.to_canonical()) + .expect("Should be able to push a storage key segment") +} + +impl From for storage::Key { + #[inline] + fn from(key: Key) -> Self { + (&key).into() + } +} + +impl From<&Key> for storage::Key { + fn from(key: &Key) -> Self { + match &key.suffix { + KeyType::Whitelisted => whitelist_prefix(&key.asset) + .push(&segments::VALUES.whitelisted.to_owned()) + .expect("Should be able to push a storage key segment"), + KeyType::Cap => whitelist_prefix(&key.asset) + .push(&segments::VALUES.cap.to_owned()) + .expect("Should be able to push a storage key segment"), + KeyType::WrappedSupply => { + let token = wrapped_erc20s::token(&key.asset); + minted_balance_key(&token) + } + KeyType::Denomination => { + let token = wrapped_erc20s::token(&key.asset); + denom_key(&token) + } + } + } +} + +/// Check if some [`storage::Key`] is an Ethereum bridge whitelist key +/// of type [`KeyType::Cap`] or [`KeyType::Whitelisted`]. +pub fn is_cap_or_whitelisted_key(key: &storage::Key) -> bool { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(s1), + DbKeySeg::StringSeg(s2), + DbKeySeg::StringSeg(s3), + DbKeySeg::StringSeg(s4), + ] => { + s1 == &BRIDGE_ADDRESS + && s2 == segments::MAIN_SEGMENT + && EthAddress::from_str(s3).is_ok() + && segments::ALL.binary_search(&s4.as_str()).is_ok() + } + _ => false, + } +} diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 827745def0..ae380c8433 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -1,9 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::hints; -use namada_core::ledger::eth_bridge::storage::active_key; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_nonce_key, get_signed_root_key, }; +use namada_core::ledger::eth_bridge::storage::{active_key, whitelist}; use namada_core::ledger::storage; use namada_core::ledger::storage::{StoreType, WlStorage}; use namada_core::ledger::storage_api::StorageRead; @@ -392,6 +392,55 @@ where voting_powers_map, ) } + + /// Check if the token at the given [`EthAddress`] is whitelisted. + pub fn is_token_whitelisted(self, &token: &EthAddress) -> bool { + let key = whitelist::Key { + asset: token, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + + self.wl_storage + .read(&key) + .expect("Reading from storage should not fail") + .unwrap_or(false) + } + + /// Fetch the token cap of the asset associated with the given + /// [`EthAddress`]. + /// + /// If the asset has never been whitelisted, return [`None`]. + pub fn get_token_cap(self, &token: &EthAddress) -> Option { + let key = whitelist::Key { + asset: token, + suffix: whitelist::KeyType::Cap, + } + .into(); + + self.wl_storage + .read(&key) + .expect("Reading from storage should not fail") + } + + /// Fetch the token supply of the asset associated with the given + /// [`EthAddress`]. + /// + /// If the asset has never been minted, return [`None`]. + pub fn get_token_supply( + self, + &token: &EthAddress, + ) -> Option { + let key = whitelist::Key { + asset: token, + suffix: whitelist::KeyType::WrappedSupply, + } + .into(); + + self.wl_storage + .read(&key) + .expect("Reading from storage should not fail") + } } /// A handle to the Ethereum addresses of the set of consensus From ed2afb257fa50197983dd166e7c2d1a2aec3da7f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 11 Jul 2023 10:23:16 +0100 Subject: [PATCH 002/103] Add Eth whitelist key tests --- .../ledger/eth_bridge/storage/whitelist.rs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/core/src/ledger/eth_bridge/storage/whitelist.rs b/core/src/ledger/eth_bridge/storage/whitelist.rs index 15ebddfa8b..77b0860a8c 100644 --- a/core/src/ledger/eth_bridge/storage/whitelist.rs +++ b/core/src/ledger/eth_bridge/storage/whitelist.rs @@ -115,3 +115,50 @@ pub fn is_cap_or_whitelisted_key(key: &storage::Key) -> bool { _ => false, } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; + + /// Test that storage key serialization yields the expected value. + #[test] + fn test_keys_whitelisted_to_string() { + let key: storage::Key = Key { + asset: DAI_ERC20_ETH_ADDRESS, + suffix: KeyType::Whitelisted, + } + .into(); + let expected = "#atest1v9hx7w36g42ysgzzwf5kgem9ypqkgerjv4ehxgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpq8f99ew/whitelist/0x6b175474e89094c44da98b954eedeac495271d0f/whitelisted"; + assert_eq!(expected, key.to_string()); + } + + /// Test that checking if a key is of type "cap" or "whitelisted" works. + #[test] + fn test_cap_or_whitelisted_key() { + let whitelisted_key: storage::Key = Key { + asset: DAI_ERC20_ETH_ADDRESS, + suffix: KeyType::Whitelisted, + } + .into(); + assert!(is_cap_or_whitelisted_key(&whitelisted_key)); + + let cap_key: storage::Key = Key { + asset: DAI_ERC20_ETH_ADDRESS, + suffix: KeyType::Cap, + } + .into(); + assert!(is_cap_or_whitelisted_key(&cap_key)); + + let unexpected_key = { + let mut k: storage::Key = Key { + asset: DAI_ERC20_ETH_ADDRESS, + suffix: KeyType::Cap, + } + .into(); + k.segments[3] = DbKeySeg::StringSeg("abc".to_owned()); + k + }; + assert!(!is_cap_or_whitelisted_key(&unexpected_key)); + } +} From 6ca25fd08f9afde306d20ac7238d74aa783a5727 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 11 Jul 2023 11:44:18 +0100 Subject: [PATCH 003/103] Add non-usable tokens for Ethereum bridge protocol bookkeeping --- .../eth_bridge/storage/wrapped_erc20s.rs | 5 +++ core/src/types/address.rs | 33 ++++++++++++++++--- shared/src/ledger/protocol/mod.rs | 3 +- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs b/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs index 0062dd50c9..36ce04141b 100644 --- a/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs +++ b/core/src/ledger/eth_bridge/storage/wrapped_erc20s.rs @@ -14,6 +14,11 @@ pub fn token(address: &EthAddress) -> Address { Address::Internal(InternalAddress::Erc20(*address)) } +/// Construct a NUT token address from an ERC20 address. +pub fn nut(address: &EthAddress) -> Address { + Address::Internal(InternalAddress::Nut(*address)) +} + /// Represents the type of a key relating to a wrapped ERC20 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] pub enum KeyType { diff --git a/core/src/types/address.rs b/core/src/types/address.rs index d587d20280..4f300c76ed 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -99,6 +99,8 @@ const PREFIX_INTERNAL: &str = "ano"; const PREFIX_IBC: &str = "ibc"; /// Fixed-length address strings prefix for Ethereum addresses. const PREFIX_ETH: &str = "eth"; +/// Fixed-length address strings prefix for Non-Usable-Token addresses. +const PREFIX_NUT: &str = "nut"; #[allow(missing_docs)] #[derive(Error, Debug)] @@ -234,6 +236,11 @@ impl Address { eth_addr.to_canonical().replace("0x", ""); format!("{}::{}", PREFIX_ETH, eth_addr) } + InternalAddress::Nut(eth_addr) => { + let eth_addr = + eth_addr.to_canonical().replace("0x", ""); + format!("{PREFIX_NUT}::{eth_addr}") + } InternalAddress::ReplayProtection => { internal::REPLAY_PROTECTION.to_string() } @@ -330,12 +337,18 @@ impl Address { "Invalid IBC internal address", )), }, - Some((PREFIX_ETH, raw)) => match string { + Some((prefix @ (PREFIX_ETH | PREFIX_NUT), raw)) => match string { _ if raw.len() == HASH_HEX_LEN => { match EthAddress::from_str(&format!("0x{}", raw)) { - Ok(eth_addr) => Ok(Address::Internal( - InternalAddress::Erc20(eth_addr), - )), + Ok(eth_addr) => Ok(match prefix { + PREFIX_ETH => Address::Internal( + InternalAddress::Erc20(eth_addr), + ), + PREFIX_NUT => Address::Internal( + InternalAddress::Nut(eth_addr), + ), + _ => unreachable!(), + }), Err(e) => Err(Error::new( ErrorKind::InvalidData, e.to_string(), @@ -543,6 +556,8 @@ pub enum InternalAddress { EthBridgePool, /// ERC20 token for Ethereum bridge Erc20(EthAddress), + /// Non-usable ERC20 tokens + Nut(EthAddress), /// Replay protection contains transactions' hash ReplayProtection, /// Multitoken @@ -566,6 +581,7 @@ impl Display for InternalAddress { Self::EthBridge => "EthBridge".to_string(), Self::EthBridgePool => "EthBridgePool".to_string(), Self::Erc20(eth_addr) => format!("Erc20: {}", eth_addr), + Self::Nut(eth_addr) => format!("Non-usable token: {eth_addr}"), Self::ReplayProtection => "ReplayProtection".to_string(), Self::Multitoken => "Multitoken".to_string(), Self::Pgf => "PublicGoodFundings".to_string(), @@ -861,6 +877,7 @@ pub mod testing { InternalAddress::EthBridge => {} InternalAddress::EthBridgePool => {} InternalAddress::Erc20(_) => {} + InternalAddress::Nut(_) => {} InternalAddress::ReplayProtection => {} InternalAddress::Pgf => {} InternalAddress::Multitoken => {} /* Add new addresses in the @@ -876,6 +893,7 @@ pub mod testing { Just(InternalAddress::EthBridge), Just(InternalAddress::EthBridgePool), Just(arb_erc20()), + Just(arb_nut()), Just(InternalAddress::ReplayProtection), Just(InternalAddress::Multitoken), Just(InternalAddress::Pgf), @@ -900,6 +918,13 @@ pub mod testing { fn arb_erc20() -> InternalAddress { use crate::types::ethereum_events::testing::arbitrary_eth_address; + // TODO: generate random erc20 addr data InternalAddress::Erc20(arbitrary_eth_address()) } + + fn arb_nut() -> InternalAddress { + use crate::types::ethereum_events::testing::arbitrary_eth_address; + // TODO: generate random erc20 addr data + InternalAddress::Nut(arbitrary_eth_address()) + } } diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 7a0244932a..eabe47ddca 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -586,7 +586,8 @@ where result } InternalAddress::IbcToken(_) - | InternalAddress::Erc20(_) => { + | InternalAddress::Erc20(_) + | InternalAddress::Nut(_) => { // The address should be a part of a multitoken key gas_meter = ctx.gas_meter.into_inner(); Ok(verifiers.contains(&Address::Internal( From 5208f3d815d849bdb08871ab9f26f1df9dfba501 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 11 Jul 2023 16:37:22 +0100 Subject: [PATCH 004/103] Add a VP for NUT transfers --- .../ledger/native_vp/ethereum_bridge/mod.rs | 1 + .../ledger/native_vp/ethereum_bridge/nut.rs | 119 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 shared/src/ledger/native_vp/ethereum_bridge/nut.rs diff --git a/shared/src/ledger/native_vp/ethereum_bridge/mod.rs b/shared/src/ledger/native_vp/ethereum_bridge/mod.rs index 85df785e79..250d51d1b5 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/mod.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/mod.rs @@ -3,4 +3,5 @@ //! pool. pub mod bridge_pool_vp; +pub mod nut; pub mod vp; diff --git a/shared/src/ledger/native_vp/ethereum_bridge/nut.rs b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs new file mode 100644 index 0000000000..dec56adbad --- /dev/null +++ b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs @@ -0,0 +1,119 @@ +//! Validity predicate for Non Usable Tokens (NUTs). + +use std::collections::BTreeSet; + +use eyre::WrapErr; +use namada_core::ledger::storage as ledger_storage; +use namada_core::ledger::storage::traits::StorageHasher; +use namada_core::types::address::{Address, InternalAddress}; +use namada_core::types::storage::Key; +use namada_core::types::token::Amount; + +use crate::ledger::native_vp::{Ctx, NativeVp, VpEnv}; +use crate::proto::Tx; +use crate::types::token::is_any_token_balance_key; +use crate::vm::WasmCacheAccess; + +/// Generic error that may be returned by the validity predicate +#[derive(thiserror::Error, Debug)] +#[error(transparent)] +pub struct Error(#[from] eyre::Report); + +/// Validity predicate for non-usable tokens. +/// +/// All this VP does is reject NUT transfers whose destination +/// address is not the Bridge pool escrow address. +pub struct NonUsableTokens<'ctx, DB, H, CA> +where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: StorageHasher, + CA: 'static + WasmCacheAccess, +{ + /// Context to interact with the host structures. + pub ctx: Ctx<'ctx, DB, H, CA>, +} + +impl<'a, DB, H, CA> NativeVp for NonUsableTokens<'a, DB, H, CA> +where + DB: 'static + ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: 'static + StorageHasher, + CA: 'static + WasmCacheAccess, +{ + type Error = Error; + + fn validate_tx( + &self, + _: &Tx, + keys_changed: &BTreeSet, + verifiers: &BTreeSet
, + ) -> Result { + tracing::debug!( + keys_changed_len = keys_changed.len(), + verifiers_len = verifiers.len(), + "Non usable tokens VP triggered", + ); + + let is_multitoken = + verifiers.contains(&Address::Internal(InternalAddress::Multitoken)); + if !is_multitoken { + tracing::debug!("Rejecting non-multitoken transfer tx"); + return Ok(false); + } + + let nut_owners = + keys_changed.iter().filter_map( + |key| match is_any_token_balance_key(key) { + Some( + [Address::Internal(InternalAddress::Nut(_)), owner], + ) => Some((key, owner)), + _ => None, + }, + ); + + for (changed_key, token_owner) in nut_owners { + let pre: Amount = self + .ctx + .read_pre(changed_key) + .context("Reading pre amount failed") + .map_err(Error)? + .unwrap_or_default(); + let post: Amount = self + .ctx + .read_post(changed_key) + .context("Reading post amount failed") + .map_err(Error)? + .unwrap_or_default(); + + match token_owner { + // the NUT balance of the bridge pool should increase + Address::Internal(InternalAddress::EthBridgePool) => { + if post < pre { + tracing::debug!( + %changed_key, + pre_amount = ?pre, + post_amount = ?post, + "Bridge pool balance should have increased" + ); + return Ok(false); + } + } + // arbitrary addresses should have their balance decrease + _addr => { + if post > pre { + tracing::debug!( + %changed_key, + pre_amount = ?pre, + post_amount = ?post, + "Balance should have decreased" + ); + return Ok(false); + } + } + } + } + + Ok(true) + } +} + +// TODO: add tests From 9c14b65de3c6eaf60828525f97330e9313645f86 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 10:32:38 +0100 Subject: [PATCH 005/103] Test NUT VP --- .../ledger/native_vp/ethereum_bridge/nut.rs | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/nut.rs b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs index dec56adbad..afea1da1d4 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/nut.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs @@ -116,4 +116,124 @@ where } } -// TODO: add tests +#[cfg(test)] +mod test_nuts { + use std::env::temp_dir; + + use assert_matches::assert_matches; + use borsh::BorshSerialize; + use namada_core::ledger::storage::testing::TestWlStorage; + use namada_core::ledger::storage_api::StorageWrite; + use namada_core::types::address::testing::arb_non_internal_address; + use namada_core::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; + use namada_core::types::storage::TxIndex; + use namada_core::types::token::balance_key; + use namada_core::types::transaction::TxType; + use namada_ethereum_bridge::storage::wrapped_erc20s; + use proptest::prelude::*; + + use super::*; + use crate::ledger::gas::VpGasMeter; + use crate::vm::wasm::VpCache; + use crate::vm::WasmCacheRwAccess; + + /// Run a VP check on a NUT transfer between the two provided addresses. + fn check_nut_transfer(src: Address, dst: Address) -> Option { + let nut = wrapped_erc20s::nut(&DAI_ERC20_ETH_ADDRESS); + let src_balance_key = balance_key(&nut, &src); + let dst_balance_key = balance_key(&nut, &dst); + + let wl_storage = { + let mut wl = TestWlStorage::default(); + + // write initial balances + wl.write(&src_balance_key, Amount::from(200_u64)) + .expect("Test failed"); + wl.write(&dst_balance_key, Amount::from(100_u64)) + .expect("Test failed"); + wl.commit_block().expect("Test failed"); + + // write the updated balances + wl.write_log + .write( + &src_balance_key, + Amount::from(100_u64).try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); + wl.write_log + .write( + &dst_balance_key, + Amount::from(200_u64).try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); + + wl + }; + + let keys_changed = { + let mut keys = BTreeSet::new(); + keys.insert(src_balance_key); + keys.insert(dst_balance_key); + keys + }; + let verifiers = { + let mut v = BTreeSet::new(); + v.insert(Address::Internal(InternalAddress::Multitoken)); + v + }; + + let tx = Tx::from_type(TxType::Raw); + let ctx = Ctx::<_, _, WasmCacheRwAccess>::new( + &Address::Internal(InternalAddress::Nut(DAI_ERC20_ETH_ADDRESS)), + &wl_storage.storage, + &wl_storage.write_log, + &tx, + &TxIndex(0), + VpGasMeter::new(0u64), + &keys_changed, + &verifiers, + VpCache::new(temp_dir(), 100usize), + ); + let vp = NonUsableTokens { ctx }; + + // print debug info in case we run into failures + for key in &keys_changed { + let pre: Amount = vp + .ctx + .read_pre(key) + .expect("Test failed") + .unwrap_or_default(); + let post: Amount = vp + .ctx + .read_post(key) + .expect("Test failed") + .unwrap_or_default(); + println!("{key}: PRE={pre:?} POST={post:?}"); + } + + vp.validate_tx(&tx, &keys_changed, &verifiers).ok() + } + + proptest! { + /// Test that transferring NUTs between two arbitrary addresses + /// will always fail. + #[test] + fn test_nut_transfer_rejected( + (src, dst) in (arb_non_internal_address(), arb_non_internal_address()) + ) { + let status = check_nut_transfer(src, dst); + assert_matches!(status, Some(false)); + } + + /// Test that transferring NUTs from an arbitrary address to the + /// Bridge pool address passes. + #[test] + fn test_nut_transfer_passes(src in arb_non_internal_address()) { + let status = check_nut_transfer( + src, + Address::Internal(InternalAddress::EthBridgePool), + ); + assert_matches!(status, Some(true)); + } + } +} From 4901296831b129eea78d192f0850ffcdb5016271 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 10:39:26 +0100 Subject: [PATCH 006/103] Trigger the NUT native VP --- shared/src/ledger/protocol/mod.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index eabe47ddca..f3002ca723 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -11,6 +11,7 @@ use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter}; use crate::ledger::governance::GovernanceVp; use crate::ledger::ibc::vp::Ibc; use crate::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; +use crate::ledger::native_vp::ethereum_bridge::nut::NonUsableTokens; use crate::ledger::native_vp::ethereum_bridge::vp::EthBridge; use crate::ledger::native_vp::multitoken::MultitokenVp; use crate::ledger::native_vp::parameters::{self, ParametersVp}; @@ -72,6 +73,8 @@ pub enum Error { ReplayProtectionNativeVpError( crate::ledger::native_vp::replay_protection::Error, ), + #[error("Non usable tokens native VP error: {0}")] + NutNativeVpError(native_vp::ethereum_bridge::nut::Error), #[error("Access to an internal address {0} is forbidden")] AccessForbidden(InternalAddress), } @@ -585,9 +588,17 @@ where gas_meter = pgf_vp.ctx.gas_meter.into_inner(); result } + InternalAddress::Nut(_) => { + let non_usable_tokens = NonUsableTokens { ctx }; + let result = non_usable_tokens + .validate_tx(tx, &keys_changed, &verifiers) + .map_err(Error::NutNativeVpError); + gas_meter = + non_usable_tokens.ctx.gas_meter.into_inner(); + result + } InternalAddress::IbcToken(_) - | InternalAddress::Erc20(_) - | InternalAddress::Nut(_) => { + | InternalAddress::Erc20(_) => { // The address should be a part of a multitoken key gas_meter = ctx.gas_meter.into_inner(); Ok(verifiers.contains(&Address::Internal( From 9b6df799b1dab7e37fcd21e0b0346a5d7a1e9eac Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 13:12:50 +0100 Subject: [PATCH 007/103] Update ethbridge-rs to v0.19.0 --- Cargo.lock | 24 ++++++++++++------------ apps/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- shared/Cargo.toml | 4 ++-- wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a069b0956..4359495965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,8 +2021,8 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2032,8 +2032,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2043,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2054,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethers", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 992abbef33..a5c4dec197 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -87,9 +87,9 @@ derivative.workspace = true directories.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} eyre.workspace = true fd-lock.workspace = true ferveo-common.workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index 7c5addad7c..5da1f1f6d6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0" } +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0" } eyre.workspace = true ferveo = {optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 28711a0329..d6c64d6e01 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -100,8 +100,8 @@ clru.workspace = true data-encoding.workspace = true derivation-path.workspace = true derivative.workspace = true -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} ethers.workspace = true eyre.workspace = true futures.workspace = true diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index be65562b6d..2e8816fd54 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 77b2082a4e..95d016ca57 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.19.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" dependencies = [ "ethabi", "ethers", From b9328901218bf418c842ffff6e24b2539ac27e20 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 13:41:09 +0100 Subject: [PATCH 008/103] Add transfer to Ethereum kinds (ERC20 or NUT) --- .../lib/node/ledger/ethereum_oracle/events.rs | 13 ++++- .../lib/node/ledger/ethereum_oracle/mod.rs | 6 +- .../lib/node/ledger/shell/finalize_block.rs | 3 +- .../shell/vote_extensions/eth_events.rs | 10 +++- .../ledger/eth_bridge/storage/bridge_pool.rs | 21 ++++++- core/src/types/eth_bridge_pool.rs | 5 ++ core/src/types/ethereum_events.rs | 56 +++++++++++++++++++ .../transactions/ethereum_events/events.rs | 4 ++ shared/src/ledger/eth_bridge/bridge_pool.rs | 6 +- .../ethereum_bridge/bridge_pool_vp.rs | 12 +++- shared/src/ledger/queries/shell/eth_bridge.rs | 10 +++- tests/src/native_vp/eth_bridge_pool.rs | 5 +- 12 files changed, 142 insertions(+), 9 deletions(-) diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 28cd61f743..10837829de 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -15,7 +15,7 @@ pub mod eth_events { use namada::types::address::Address; use namada::types::ethereum_events::{ EthAddress, EthereumEvent, TokenWhitelist, TransferToEthereum, - TransferToNamada, Uint, + TransferToEthereumKind, TransferToNamada, Uint, }; use namada::types::keccak::KeccakHash; use namada::types::token::Amount; @@ -180,6 +180,7 @@ pub mod eth_events { /// Trait to add parsing methods to foreign types. trait Parse: Sized { + parse_method! { parse_eth_transfer_kind -> TransferToEthereumKind } parse_method! { parse_eth_address -> EthAddress } parse_method! { parse_address -> Address } parse_method! { parse_amount -> Amount } @@ -198,6 +199,13 @@ pub mod eth_events { parse_method! { parse_transfer_to_eth -> TransferToEthereum } } + impl Parse for u8 { + fn parse_eth_transfer_kind(self) -> Result { + self.try_into() + .map_err(|err| Error::Decode(format!("{:?}", err))) + } + } + impl Parse for ethabi::Address { fn parse_eth_address(self) -> Result { Ok(EthAddress(self.0)) @@ -296,6 +304,7 @@ pub mod eth_events { impl Parse for ethereum_structs::Erc20Transfer { fn parse_transfer_to_eth(self) -> Result { + let kind = self.kind.parse_eth_transfer_kind()?; let asset = self.from.parse_eth_address()?; let receiver = self.to.parse_eth_address()?; let sender = self.sender.parse_address()?; @@ -303,6 +312,7 @@ pub mod eth_events { let gas_payer = self.fee_from.parse_address()?; let gas_amount = self.fee.parse_amount()?; Ok(TransferToEthereum { + kind, asset, amount, sender, @@ -524,6 +534,7 @@ pub mod eth_events { let eth_transfers = TransferToErcFilter { transfers: vec![ ethereum_structs::Erc20Transfer { + kind: TransferToEthereumKind::Erc20 as u8, from: H160([1; 20]), to: H160([2; 20]), sender: address.clone(), diff --git a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index 1e967b12b4..05486aa11e 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -569,7 +569,9 @@ mod test_oracle { use namada::eth_bridge::ethers::types::H160; use namada::eth_bridge::structs::Erc20Transfer; use namada::types::address::testing::gen_established_address; - use namada::types::ethereum_events::{EthAddress, TransferToEthereum}; + use namada::types::ethereum_events::{ + EthAddress, TransferToEthereum, TransferToEthereumKind, + }; use tokio::sync::oneshot::channel; use tokio::time::timeout; @@ -826,6 +828,7 @@ mod test_oracle { let gas_payer = gen_established_address(); let second_event = TransferToErcFilter { transfers: vec![Erc20Transfer { + kind: TransferToEthereumKind::Erc20 as u8, amount: 0.into(), from: H160([0; 20]), sender: gas_payer.to_string(), @@ -895,6 +898,7 @@ mod test_oracle { assert_eq!( transfer, TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: Default::default(), asset: EthAddress([0; 20]), sender: gas_payer.clone(), diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 8af2d1a746..f8fb4fb0a4 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1039,7 +1039,7 @@ mod test_finalize_block { use namada::proto::{Code, Data, Section, Signature}; use namada::types::dec::POS_DECIMAL_PRECISION; use namada::types::ethereum_events::{ - EthAddress, TransferToEthereum, Uint as ethUint, + EthAddress, TransferToEthereum, TransferToEthereumKind, Uint as ethUint, }; use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; @@ -1697,6 +1697,7 @@ mod test_finalize_block { let transfer = { use namada::core::types::eth_bridge_pool::PendingTransfer; let transfer = TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 10u64.into(), asset, receiver, diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 891a403f90..a7006abecc 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -466,7 +466,8 @@ mod test_vote_extensions { #[cfg(feature = "abcipp")] use namada::types::eth_abi::Encode; use namada::types::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, Uint, + EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, + Uint, }; #[cfg(feature = "abcipp")] use namada::types::keccak::keccak_hash; @@ -598,6 +599,7 @@ mod test_vote_extensions { let event_1 = EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), sender: gen_established_address(), @@ -611,6 +613,7 @@ mod test_vote_extensions { let event_2 = EthereumEvent::TransfersToEthereum { nonce: 1.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), sender: gen_established_address(), @@ -662,6 +665,7 @@ mod test_vote_extensions { let event_1 = EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), sender: gen_established_address(), @@ -724,6 +728,7 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), sender: gen_established_address(), asset: EthAddress([1; 20]), @@ -818,6 +823,7 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), sender: gen_established_address(), asset: EthAddress([1; 20]), @@ -895,6 +901,7 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), sender: gen_established_address(), asset: EthAddress([1; 20]), @@ -977,6 +984,7 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { + kind: TransferToEthereumKind::Erc20, amount: 100.into(), sender: gen_established_address(), asset: EthAddress([1; 20]), diff --git a/core/src/ledger/eth_bridge/storage/bridge_pool.rs b/core/src/ledger/eth_bridge/storage/bridge_pool.rs index 5134094f3f..0c20c50ff7 100644 --- a/core/src/ledger/eth_bridge/storage/bridge_pool.rs +++ b/core/src/ledger/eth_bridge/storage/bridge_pool.rs @@ -415,7 +415,9 @@ mod test_bridge_pool_tree { use proptest::prelude::*; use super::*; - use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; + use crate::types::eth_bridge_pool::{ + GasFee, TransferToEthereum, TransferToEthereumKind, + }; use crate::types::ethereum_events::EthAddress; /// An established user address for testing & development @@ -432,6 +434,7 @@ mod test_bridge_pool_tree { assert_eq!(tree.root().0, [0; 32]); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -458,6 +461,7 @@ mod test_bridge_pool_tree { for i in 0..2 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -485,6 +489,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -522,6 +527,7 @@ mod test_bridge_pool_tree { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -549,6 +555,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -579,6 +586,7 @@ mod test_bridge_pool_tree { fn test_parse_key() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -602,6 +610,7 @@ mod test_bridge_pool_tree { fn test_key_multiple_segments() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -637,6 +646,7 @@ mod test_bridge_pool_tree { let mut tree = BridgePoolTree::default(); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), @@ -655,6 +665,7 @@ mod test_bridge_pool_tree { ); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([0; 20]), @@ -686,6 +697,7 @@ mod test_bridge_pool_tree { fn test_single_leaf() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), sender: bertha_address(), recipient: EthAddress([0; 20]), @@ -714,6 +726,7 @@ mod test_bridge_pool_tree { for i in 0..2 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -743,6 +756,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -772,6 +786,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -799,6 +814,7 @@ mod test_bridge_pool_tree { for i in 0..2 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -826,6 +842,7 @@ mod test_bridge_pool_tree { for i in 0..3 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -853,6 +870,7 @@ mod test_bridge_pool_tree { for i in 0..5 { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([i; 20]), sender: bertha_address(), recipient: EthAddress([i + 1; 20]), @@ -884,6 +902,7 @@ mod test_bridge_pool_tree { .into_iter() .map(|addr| PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress(addr), sender: bertha_address(), recipient: EthAddress(addr), diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index d70c55ab78..aa471ce7d1 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::types::address::Address; use crate::types::eth_abi::Encode; +pub use crate::types::ethereum_events::TransferToEthereumKind; use crate::types::ethereum_events::{ EthAddress, TransferToEthereum as TransferToEthereumEvent, }; @@ -33,6 +34,8 @@ const NAMESPACE: &str = "transfer"; BorshSchema, )] pub struct TransferToEthereum { + /// The kind of transfer to Ethereum. + pub kind: TransferToEthereumKind, /// The type of token pub asset: EthAddress, /// The recipient address @@ -70,6 +73,7 @@ pub struct PendingTransfer { impl From for ethbridge_structs::Erc20Transfer { fn from(pending: PendingTransfer) -> Self { Self { + kind: pending.transfer.kind as u8, from: pending.transfer.asset.0.into(), to: pending.transfer.recipient.0.into(), amount: pending.transfer.amount.into(), @@ -98,6 +102,7 @@ impl Encode<8> for PendingTransfer { impl From<&TransferToEthereumEvent> for PendingTransfer { fn from(event: &TransferToEthereumEvent) -> Self { let transfer = TransferToEthereum { + kind: event.kind, asset: event.asset, recipient: event.receiver, sender: event.sender.clone(), diff --git a/core/src/types/ethereum_events.rs b/core/src/types/ethereum_events.rs index bab4d46bd2..75bfc5f5cc 100644 --- a/core/src/types/ethereum_events.rs +++ b/core/src/types/ethereum_events.rs @@ -376,6 +376,60 @@ pub struct TransferToNamada { pub receiver: Address, } +/// Transfer to Ethereum kinds. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +#[repr(u8)] +pub enum TransferToEthereumKind { + /// Transfer ERC20 assets from Namada to Ethereum. + /// + /// These transfers burn wrapped ERC20 assets in Namada, once + /// they have been confirmed. + Erc20 = Self::KIND_ERC20, + /// Refund non-usable tokens. + /// + /// These Bridge pool transfers should be crafted for assets + /// that have been transferred to Namada, that had either not + /// been whitelisted or whose token caps had been exceeded in + /// Namada at the time of the transfer. + Nut = Self::KIND_NUT, +} + +// XXX: keep these values in sync with the smart contracts +impl TransferToEthereumKind { + const KIND_ERC20: u8 = 0; + const KIND_NUT: u8 = 1; +} + +impl TryFrom for TransferToEthereumKind { + type Error = eyre::Error; + + fn try_from(kind: u8) -> Result { + match kind { + Self::KIND_ERC20 => Ok(Self::Erc20), + Self::KIND_NUT => Ok(Self::Nut), + _ => Err(eyre!( + "Only valid kinds are {} (ERC20) and {} (NUT)", + Self::KIND_ERC20, + Self::KIND_NUT + )), + } + } +} + /// An event transferring some kind of value from Namada to Ethereum #[derive( Clone, @@ -392,6 +446,8 @@ pub struct TransferToNamada { Deserialize, )] pub struct TransferToEthereum { + /// The kind of transfer to Ethereum. + pub kind: TransferToEthereumKind, /// Quantity of wrapped Asset in the transfer pub amount: Amount, /// Address of the smart contract issuing the token diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 0052fb01b1..1fa1347b73 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -596,6 +596,7 @@ mod tests { sender: sender.clone(), recipient: EthAddress([i as u8 + 1; 20]), amount: Amount::from(10), + kind: eth_bridge_pool::TransferToEthereumKind::Erc20, }, gas_fee: GasFee { amount: Amount::from(1), @@ -822,6 +823,7 @@ mod tests { let mut transfers = vec![]; for transfer in pending_transfers { let transfer_to_eth = TransferToEthereum { + kind: transfer.transfer.kind, amount: transfer.transfer.amount, asset: transfer.transfer.asset, receiver: transfer.transfer.recipient, @@ -912,6 +914,7 @@ mod tests { sender: address::testing::established_address_1(), recipient: EthAddress([5; 20]), amount: Amount::from(10), + kind: eth_bridge_pool::TransferToEthereumKind::Erc20, }, gas_fee: GasFee { amount: Amount::from(1), @@ -1077,6 +1080,7 @@ mod tests { .into_iter() .map(|transfer| { let transfer_to_eth = TransferToEthereum { + kind: transfer.transfer.kind, amount: transfer.transfer.amount, asset: transfer.transfer.asset, receiver: transfer.transfer.recipient, diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 5b8a8eaa5e..596538bb29 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -28,7 +28,7 @@ use crate::types::control_flow::{ }; use crate::types::eth_abi::Encode; use crate::types::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, + GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, }; use crate::types::keccak::KeccakHash; use crate::types::token::{Amount, DenominatedAmount}; @@ -60,6 +60,9 @@ pub async fn build_bridge_pool_tx( recipient, sender: sender.clone(), amount, + // TODO: dispatch on the transfer kind, based + // on CLI arguments + kind: TransferToEthereumKind::Erc20, }, gas_fee: GasFee { amount: fee_amount, @@ -672,6 +675,7 @@ mod recommendations { pub fn transfer(gas_amount: u64) -> PendingTransfer { PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), recipient: EthAddress([2; 20]), sender: bertha_address(), diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 323633b563..df5696e60a 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -357,7 +357,9 @@ mod test_bridge_pool_vp { use crate::ledger::storage_api::StorageWrite; use crate::types::address::{nam, wnam}; use crate::types::chain::ChainId; - use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; + use crate::types::eth_bridge_pool::{ + GasFee, TransferToEthereum, TransferToEthereumKind, + }; use crate::types::hash::Hash; use crate::types::storage::TxIndex; use crate::types::transaction::TxType; @@ -406,6 +408,7 @@ mod test_bridge_pool_vp { fn initial_pool() -> PendingTransfer { PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, sender: bertha_address(), recipient: EthAddress([0; 20]), @@ -570,6 +573,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, sender: bertha_address(), recipient: EthAddress([1; 20]), @@ -826,6 +830,7 @@ mod test_bridge_pool_vp { |transfer, log| { let t = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), sender: bertha_address(), recipient: EthAddress([11; 20]), @@ -856,6 +861,7 @@ mod test_bridge_pool_vp { |transfer, log| { let t = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), sender: bertha_address(), recipient: EthAddress([11; 20]), @@ -977,6 +983,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, sender: bertha_address(), recipient: EthAddress([1; 20]), @@ -1043,6 +1050,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), sender: bertha_address(), recipient: EthAddress([1; 20]), @@ -1130,6 +1138,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), sender: bertha_address(), recipient: EthAddress([1; 20]), @@ -1236,6 +1245,7 @@ mod test_bridge_pool_vp { // the transfer to be added to the pool let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), sender: bertha_address(), recipient: EthAddress([1; 20]), diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index d7d0ed249e..f7c0731c70 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -588,7 +588,7 @@ mod test_ethbridge_router { use crate::ledger::queries::RPC; use crate::types::eth_abi::Encode; use crate::types::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, + GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, }; use crate::types::ethereum_events::EthAddress; @@ -788,6 +788,7 @@ mod test_ethbridge_router { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -829,6 +830,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -889,6 +891,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -998,6 +1001,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -1088,6 +1092,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -1159,6 +1164,7 @@ mod test_ethbridge_router { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -1183,6 +1189,7 @@ mod test_ethbridge_router { let event_transfer = namada_core::types::ethereum_events::TransferToEthereum { + kind: transfer.transfer.kind, asset: transfer.transfer.asset, receiver: transfer.transfer.recipient, amount: transfer.transfer.amount, @@ -1269,6 +1276,7 @@ mod test_ethbridge_router { test_utils::init_default_storage(&mut client.wl_storage); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 544889b2d1..ee3c659591 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -12,7 +12,7 @@ mod test_bridge_pool_vp { use namada::types::address::{nam, wnam}; use namada::types::chain::ChainId; use namada::types::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, + GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, }; use namada::types::ethereum_events::EthAddress; use namada::types::key::{common, ed25519, SecretKey}; @@ -119,6 +119,7 @@ mod test_bridge_pool_vp { fn validate_erc20_tx() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -136,6 +137,7 @@ mod test_bridge_pool_vp { fn validate_mint_wnam_tx() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), recipient: EthAddress([0; 20]), sender: bertha_address(), @@ -153,6 +155,7 @@ mod test_bridge_pool_vp { fn validate_mint_wnam_different_sender_tx() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), recipient: EthAddress([0; 20]), sender: bertha_address(), From 8297842597fd0aab5431dfbb87e5fca42b2d24db Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 15:20:22 +0100 Subject: [PATCH 009/103] Mint NUTs upon receiving non-whitelisted assets or exceeding token caps --- .../transactions/ethereum_events/events.rs | 119 +++++++++++------- .../src/storage/eth_bridge_queries.rs | 57 +++++++++ 2 files changed, 132 insertions(+), 44 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 1fa1347b73..05f374c64b 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -30,7 +30,7 @@ use namada_core::types::token::{balance_key, minted_balance_key}; use crate::parameters::read_native_erc20_address; use crate::protocol::transactions::update; -use crate::storage::eth_bridge_queries::EthBridgeQueries; +use crate::storage::eth_bridge_queries::{EthAssetMint, EthBridgeQueries}; /// Updates storage based on the given confirmed `event`. For example, for a /// confirmed [`EthereumEvent::TransfersToNamada`], mint the corresponding @@ -140,15 +140,25 @@ where receiver, } = transfer; let mut changed = if asset != &wrapped_native_erc20 { - let changed = - mint_wrapped_erc20s(wl_storage, asset, receiver, amount)?; + let (asset_count, changed) = + mint_eth_assets(wl_storage, asset, receiver, amount)?; // TODO: query denomination of the whitelisted token from storage, // and print this amount with the proper formatting; for now, use // NAM's formatting - tracing::info!( - "Minted wrapped ERC20s - (receiver - {receiver}, amount - {})", - amount.to_string_native(), - ); + if !asset_count.erc20_amount.is_zero() { + tracing::info!( + "Minted wrapped ERC20s - (asset - {asset}, receiver - \ + {receiver}, amount - {})", + asset_count.erc20_amount.to_string_native(), + ); + } + if !asset_count.nut_amount.is_zero() { + tracing::info!( + "Minted NUTs - (asset - {asset}, receiver - {receiver}, \ + amount - {})", + asset_count.nut_amount.to_string_native(), + ); + } changed } else { redeem_native_token(wl_storage, receiver, amount)? @@ -220,55 +230,76 @@ where ])) } +/// Helper function to mint assets originating from Ethereum +/// on Namada. +/// /// Mints `amount` of a wrapped ERC20 `asset` for `receiver`. -fn mint_wrapped_erc20s( +/// If the given asset is not whitelisted or has exceeded the +/// token caps, mint NUTs, too. +fn mint_eth_assets( wl_storage: &mut WlStorage, asset: &EthAddress, receiver: &Address, - amount: &token::Amount, -) -> Result> + &amount: &token::Amount, +) -> Result<(EthAssetMint, BTreeSet)> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { let mut changed_keys = BTreeSet::default(); - let token = wrapped_erc20s::token(asset); - let balance_key = balance_key(&token, receiver); - update::amount(wl_storage, &balance_key, |balance| { - tracing::debug!( - %balance_key, - ?balance, - "Existing value found", - ); - balance.receive(amount); - tracing::debug!( - %balance_key, - ?balance, - "New value calculated", - ); - })?; - _ = changed_keys.insert(balance_key); - let supply_key = minted_balance_key(&token); - update::amount(wl_storage, &supply_key, |supply| { - tracing::debug!( - %supply_key, - ?supply, - "Existing value found", - ); - supply.receive(amount); - tracing::debug!( - %supply_key, - ?supply, - "New value calculated", - ); - })?; - _ = changed_keys.insert(supply_key); + let asset_count = wl_storage + .ethbridge_queries() + .get_eth_assets_to_mint(asset, amount); + + let assets_to_mint = [ + // check if we should mint nuts + (!asset_count.nut_amount.is_zero()) + .then(|| (wrapped_erc20s::nut(asset), asset_count.nut_amount)), + // check if we should mint erc20s + (!asset_count.erc20_amount.is_zero()) + .then(|| (wrapped_erc20s::token(asset), asset_count.erc20_amount)), + ] + .into_iter() + // remove assets that do not need to be + // minted from the iterator + .flatten(); + + for (token, ref amount) in assets_to_mint { + let balance_key = balance_key(&token, receiver); + update::amount(wl_storage, &balance_key, |balance| { + tracing::debug!( + %balance_key, + ?balance, + "Existing value found", + ); + balance.receive(amount); + tracing::debug!( + %balance_key, + ?balance, + "New value calculated", + ); + })?; + _ = changed_keys.insert(balance_key); - // mint the token without a minter because a protocol tx doesn't need to - // trigger a VP + let supply_key = minted_balance_key(&token); + update::amount(wl_storage, &supply_key, |supply| { + tracing::debug!( + %supply_key, + ?supply, + "Existing value found", + ); + supply.receive(amount); + tracing::debug!( + %supply_key, + ?supply, + "New value calculated", + ); + })?; + _ = changed_keys.insert(supply_key); + } - Ok(changed_keys) + Ok((asset_count, changed_keys)) } fn act_on_transfers_to_eth( diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index ae380c8433..0a738d96a4 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -441,6 +441,63 @@ where .read(&key) .expect("Reading from storage should not fail") } + + /// Return the number of ERC20 and NUT assets to be minted, + /// after receiving a "transfer to Namada" Ethereum event. + /// + /// NUTs are minted when: + /// + /// 1. `token` is not whitelisted. + /// 2. `token` has exceeded the configured token caps, + /// after minting `amount_to_mint`. + pub fn get_eth_assets_to_mint( + self, + token: &EthAddress, + amount_to_mint: token::Amount, + ) -> EthAssetMint { + if !self.is_token_whitelisted(token) { + return EthAssetMint { + nut_amount: amount_to_mint, + erc20_amount: token::Amount::zero(), + }; + } + + let supply = self.get_token_supply(token).unwrap_or_default(); + let cap = self.get_token_cap(token).unwrap_or_default(); + + if hints::unlikely(cap < supply) { + panic!( + "Namada's state is faulty! The Ethereum ERC20 asset {token} \ + has a higher minted supply than the configured token cap: \ + cap:{cap:?} < supply:{supply:?}" + ); + } + + if amount_to_mint + supply > cap { + let erc20_amount = cap - supply; + let nut_amount = amount_to_mint - erc20_amount; + + return EthAssetMint { + nut_amount, + erc20_amount, + }; + } + + EthAssetMint { + erc20_amount: amount_to_mint, + nut_amount: token::Amount::zero(), + } + } +} + +/// Number of tokens to mint after receiving a "transfer +/// to Namada" Ethereum event. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct EthAssetMint { + /// Amount of NUTs to mint. + pub nut_amount: token::Amount, + /// Amount of wrapped ERC20s to mint. + pub erc20_amount: token::Amount, } /// A handle to the Ethereum addresses of the set of consensus From 82320aa3673d5b36962d824f387d152b1676504f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 16:28:29 +0100 Subject: [PATCH 010/103] Add helper fn to whitelist tokens in tests --- ethereum_bridge/src/test_utils.rs | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index e74c4803e0..f2a1ee0b8c 100644 --- a/ethereum_bridge/src/test_utils.rs +++ b/ethereum_bridge/src/test_utils.rs @@ -5,6 +5,7 @@ use std::num::NonZeroU64; use borsh::BorshSerialize; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; +use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::mockdb::MockDBWriteBatch; use namada_core::ledger::storage::testing::{TestStorage, TestWlStorage}; use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; @@ -114,6 +115,45 @@ pub fn bootstrap_ethereum_bridge( config } +/// Whitelist metadata to pass to [`whitelist_tokens`]. +pub struct WhitelistMeta { + /// Token cap. + pub cap: token::Amount, + /// Token denomination. + pub denom: u8, +} + +/// Whitelist the given Ethereum tokens. +pub fn whitelist_tokens(wl_storage: &mut TestWlStorage, token_list: L) +where + L: Into>, +{ + for (asset, WhitelistMeta { cap, denom }) in token_list.into() { + let cap_key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Cap, + } + .into(); + wl_storage.write(&cap_key, cap).expect("Test failed"); + + let whitelisted_key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + wl_storage + .write(&whitelisted_key, true) + .expect("Test failed"); + + let denom_key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Denomination, + } + .into(); + wl_storage.write(&denom_key, denom).expect("Test failed"); + } +} + /// Returns the number of keys in `storage` which have values present. pub fn stored_keys_count(wl_storage: &TestWlStorage) -> usize { let root = Key { segments: vec![] }; From 1a70c4fc05fa9de94bf663131d533eda0d3176fa Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 16:35:22 +0100 Subject: [PATCH 011/103] Fix unit tests requiring token whitelist checks --- .../transactions/ethereum_events/events.rs | 10 ++++++++++ .../transactions/ethereum_events/mod.rs | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 05f374c64b..ace6d16f5d 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -797,6 +797,16 @@ mod tests { fn test_act_on_transfers_to_namada_mints_wdai() { let mut wl_storage = TestWlStorage::default(); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let initial_stored_keys_count = stored_keys_count(&wl_storage); let amount = Amount::from(100); diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs index d3cd32972d..c8bd21a4bf 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs @@ -330,6 +330,16 @@ mod tests { )]); let mut wl_storage = TestWlStorage::default(); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let changed_keys = apply_updates(&mut wl_storage, updates, voting_powers)?; @@ -405,6 +415,16 @@ mod tests { vec![(sole_validator.clone(), Amount::native_whole(100))], )); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let receiver = address::testing::established_address_1(); let event = EthereumEvent::TransfersToNamada { From 339e18aa54d09fddeaa11942cb531de668da9ef8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 21:06:38 +0100 Subject: [PATCH 012/103] Fix refunding and burning of NUTs --- core/src/types/eth_bridge_pool.rs | 16 ++++++++++++++++ .../transactions/ethereum_events/events.rs | 8 ++++---- wasm/wasm_source/src/tx_bridge_pool.rs | 4 ++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index aa471ce7d1..c7394af167 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -5,6 +5,7 @@ use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use ethabi::token::Token; use serde::{Deserialize, Serialize}; +use crate::ledger::eth_bridge::storage::wrapped_erc20s; use crate::types::address::Address; use crate::types::eth_abi::Encode; pub use crate::types::ethereum_events::TransferToEthereumKind; @@ -70,6 +71,21 @@ pub struct PendingTransfer { pub gas_fee: GasFee, } +impl PendingTransfer { + /// Get a token [`Address`] from this [`PendingTransfer`]. + #[inline] + pub fn token_address(&self) -> Address { + match &self.transfer.kind { + TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(&self.transfer.asset) + } + TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(&self.transfer.asset) + } + } + } +} + impl From for ethbridge_structs::Erc20Transfer { fn from(pending: PendingTransfer) -> Self { Self { diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index ace6d16f5d..ea97caa72f 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -510,7 +510,7 @@ where ); (escrow_balance_key, sender_balance_key) } else { - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); let sender_balance_key = balance_key(&token, &transfer.transfer.sender); (escrow_balance_key, sender_balance_key) @@ -548,7 +548,7 @@ where return Ok(changed_keys); } - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); update::amount(wl_storage, &escrow_balance_key, |balance| { @@ -690,7 +690,7 @@ mod tests { ) .expect("Test failed"); } else { - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let sender_key = balance_key(&token, &transfer.transfer.sender); let sender_balance = Amount::from(0); wl_storage @@ -1029,7 +1029,7 @@ mod tests { .expect("Test failed"); assert_eq!(escrow_balance, Amount::from(0)); } else { - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let sender_key = balance_key(&token, &transfer.transfer.sender); let value = wl_storage.read_bytes(&sender_key).expect("Test failed"); diff --git a/wasm/wasm_source/src/tx_bridge_pool.rs b/wasm/wasm_source/src/tx_bridge_pool.rs index bf73e83f7d..c1a65403a7 100644 --- a/wasm/wasm_source/src/tx_bridge_pool.rs +++ b/wasm/wasm_source/src/tx_bridge_pool.rs @@ -1,7 +1,7 @@ //! A tx for adding a transfer request across the Ethereum bridge //! into the bridge pool. use borsh::{BorshDeserialize, BorshSerialize}; -use eth_bridge::storage::{bridge_pool, native_erc20_key, wrapped_erc20s}; +use eth_bridge::storage::{bridge_pool, native_erc20_key}; use eth_bridge_pool::{GasFee, PendingTransfer, TransferToEthereum}; use namada_tx_prelude::*; @@ -45,7 +45,7 @@ fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { )?; } else { // Otherwise we escrow ERC20 tokens. - let token = wrapped_erc20s::token(&asset); + let token = transfer.token_address(); token::transfer( ctx, sender, From d115bb094232351dea8e0c9314b6260c4260af66 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 11:14:08 +0100 Subject: [PATCH 013/103] Rework DAI minting unit tests to account for NUTs --- .../transactions/ethereum_events/events.rs | 142 +++++++++++++----- 1 file changed, 102 insertions(+), 40 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index ea97caa72f..e8cc2e723d 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -792,52 +792,114 @@ mod tests { ); } - #[test] - /// Test acting on a single transfer and minting the first ever wDAI - fn test_act_on_transfers_to_namada_mints_wdai() { - let mut wl_storage = TestWlStorage::default(); - test_utils::bootstrap_ethereum_bridge(&mut wl_storage); - test_utils::whitelist_tokens( - &mut wl_storage, - [( - DAI_ERC20_ETH_ADDRESS, - test_utils::WhitelistMeta { - cap: Amount::max(), - denom: 18, - }, - )], - ); - let initial_stored_keys_count = stored_keys_count(&wl_storage); + /// Parameters to test minting DAI in Namada. + struct TestMintDai { + /// The token cap of DAI. + /// + /// If the token is not whitelisted, this value + /// is not set. + dai_token_cap: Option, + /// The transferred amount of DAI. + transferred_amount: token::Amount, + } - let amount = Amount::from(100); - let receiver = address::testing::established_address_1(); - let transfers = vec![TransferToNamada { - amount, - asset: DAI_ERC20_ETH_ADDRESS, - receiver: receiver.clone(), - }]; + impl TestMintDai { + /// Execute a test with the given parameters. + fn run_test(self) { + let dai_token_cap = self.dai_token_cap.unwrap_or_default(); - update_transfers_to_namada_state( - &mut wl_storage, - &mut BTreeSet::new(), - &transfers, - ) - .unwrap(); + let (erc20_amount, nut_amount) = + if dai_token_cap > self.transferred_amount { + (self.transferred_amount, token::Amount::zero()) + } else { + (dai_token_cap, self.transferred_amount - dai_token_cap) + }; + assert_eq!(self.transferred_amount, nut_amount + erc20_amount); + + let mut wl_storage = TestWlStorage::default(); + test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + if !dai_token_cap.is_zero() { + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: dai_token_cap, + denom: 18, + }, + )], + ); + } - let wdai = wrapped_erc20s::token(&DAI_ERC20_ETH_ADDRESS); - let receiver_balance_key = balance_key(&wdai, &receiver); - let wdai_supply_key = minted_balance_key(&wdai); + let receiver = address::testing::established_address_1(); + let transfers = vec![TransferToNamada { + amount: self.transferred_amount, + asset: DAI_ERC20_ETH_ADDRESS, + receiver: receiver.clone(), + }]; + + update_transfers_to_namada_state( + &mut wl_storage, + &mut BTreeSet::new(), + &transfers, + ) + .unwrap(); - assert_eq!( - stored_keys_count(&wl_storage), - initial_stored_keys_count + 2 - ); + for is_nut in [false, true] { + let wdai = if is_nut { + wrapped_erc20s::nut(&DAI_ERC20_ETH_ADDRESS) + } else { + wrapped_erc20s::token(&DAI_ERC20_ETH_ADDRESS) + }; + let expected_amount = + if is_nut { nut_amount } else { erc20_amount }; + + let receiver_balance_key = balance_key(&wdai, &receiver); + let wdai_supply_key = minted_balance_key(&wdai); - let expected_amount = amount.try_to_vec().unwrap(); - for key in vec![receiver_balance_key, wdai_supply_key] { - let value = wl_storage.read_bytes(&key).unwrap(); - assert_matches!(value, Some(bytes) if bytes == expected_amount); + for key in vec![receiver_balance_key, wdai_supply_key] { + let value: Option = + wl_storage.read(&key).unwrap(); + if expected_amount.is_zero() { + assert_matches!(value, None); + } else { + assert_matches!(value, Some(amount) if amount == expected_amount); + } + } + } + } + } + + /// Test that if DAI is never whitelisted, we only mint NUTs. + #[test] + fn test_minting_dai_when_not_whitelisted() { + TestMintDai { + dai_token_cap: None, + transferred_amount: Amount::from(100), + } + .run_test(); + } + + /// Test that overrunning the token caps results in minting DAI NUTs, + /// along with wDAI. + #[test] + fn test_minting_dai_on_cap_overrun() { + TestMintDai { + dai_token_cap: Some(Amount::from(80)), + transferred_amount: Amount::from(100), + } + .run_test(); + } + + /// Test acting on a single "transfer to Namada" Ethereum event + /// and minting the first ever wDAI. + #[test] + fn test_minting_dai_wrapped() { + TestMintDai { + dai_token_cap: Some(Amount::max()), + transferred_amount: Amount::from(100), } + .run_test(); } #[test] From d26e9838a3d5d987677b70b306928fdb9e208211 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 13:53:09 +0100 Subject: [PATCH 014/103] Test burning NUTs --- .../transactions/ethereum_events/events.rs | 147 ++++++++++++++---- 1 file changed, 113 insertions(+), 34 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index e8cc2e723d..da967b85a0 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -613,21 +613,25 @@ mod tests { assets_transferred: A, ) -> Vec where - A: Into>, + A: Into< + BTreeSet<(EthAddress, eth_bridge_pool::TransferToEthereumKind)>, + >, { let sender = address::testing::established_address_1(); let payer = address::testing::established_address_2(); // set pending transfers let mut pending_transfers = vec![]; - for (i, asset) in assets_transferred.into().into_iter().enumerate() { + for (i, (asset, kind)) in + assets_transferred.into().into_iter().enumerate() + { let transfer = PendingTransfer { transfer: eth_bridge_pool::TransferToEthereum { asset, sender: sender.clone(), recipient: EthAddress([i as u8 + 1; 20]), amount: Amount::from(10), - kind: eth_bridge_pool::TransferToEthereumKind::Erc20, + kind, }, gas_fee: GasFee { amount: Amount::from(1), @@ -651,7 +655,18 @@ mod tests { ) -> Vec { init_bridge_pool_transfers( wl_storage, - (0..2).map(|i| EthAddress([i; 20])).collect::>(), + (0..2) + .map(|i| { + ( + EthAddress([i; 20]), + if i & 1 == 0 { + eth_bridge_pool::TransferToEthereumKind::Erc20 + } else { + eth_bridge_pool::TransferToEthereumKind::Nut + }, + ) + }) + .collect::>(), ) } @@ -914,10 +929,19 @@ mod tests { let native_erc20 = read_native_erc20_address(&wl_storage).expect("Test failed"); let random_erc20 = EthAddress([0xff; 20]); - let random_erc20_token = wrapped_erc20s::token(&random_erc20); + let random_erc20_token = wrapped_erc20s::nut(&random_erc20); + let random_erc20_2 = EthAddress([0xee; 20]); + let random_erc20_token_2 = wrapped_erc20s::token(&random_erc20_2); let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, - [native_erc20, random_erc20], + [ + (native_erc20, eth_bridge_pool::TransferToEthereumKind::Erc20), + (random_erc20, eth_bridge_pool::TransferToEthereumKind::Nut), + ( + random_erc20_2, + eth_bridge_pool::TransferToEthereumKind::Erc20, + ), + ], ); init_balance(&mut wl_storage, &pending_transfers); let pending_keys: HashSet = @@ -959,7 +983,16 @@ mod tests { &BRIDGE_POOL_ADDRESS )) ); + assert!( + changed_keys.remove(&balance_key( + &random_erc20_token_2, + &BRIDGE_POOL_ADDRESS + )) + ); assert!(changed_keys.remove(&minted_balance_key(&random_erc20_token))); + assert!( + changed_keys.remove(&minted_balance_key(&random_erc20_token_2)) + ); assert!(changed_keys.remove(&payer_balance_key)); assert!(changed_keys.remove(&pool_balance_key)); assert!(changed_keys.remove(&get_nonce_key())); @@ -981,7 +1014,7 @@ mod tests { .expect("Test failed: no value in storage"), ) .expect("Test failed"); - assert_eq!(relayer_balance, Amount::from(2)); + assert_eq!(relayer_balance, Amount::from(3)); let bp_balance_post = Amount::try_from_slice( &wl_storage .read_bytes(&pool_balance_key) @@ -990,7 +1023,8 @@ mod tests { ) .expect("Test failed"); bp_balance_pre.spend(&bp_balance_post); - assert_eq!(bp_balance_pre, Amount::from(2)); + assert_eq!(bp_balance_pre, Amount::from(3)); + assert_eq!(bp_balance_post, Amount::from(0)); } #[test] @@ -1169,13 +1203,31 @@ mod tests { let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, [ - native_erc20, - EthAddress([0xaa; 20]), - EthAddress([0xbb; 20]), - EthAddress([0xcc; 20]), - EthAddress([0xdd; 20]), - EthAddress([0xee; 20]), - EthAddress([0xff; 20]), + (native_erc20, eth_bridge_pool::TransferToEthereumKind::Nut), + ( + EthAddress([0xaa; 20]), + eth_bridge_pool::TransferToEthereumKind::Erc20, + ), + ( + EthAddress([0xbb; 20]), + eth_bridge_pool::TransferToEthereumKind::Nut, + ), + ( + EthAddress([0xcc; 20]), + eth_bridge_pool::TransferToEthereumKind::Erc20, + ), + ( + EthAddress([0xdd; 20]), + eth_bridge_pool::TransferToEthereumKind::Nut, + ), + ( + EthAddress([0xee; 20]), + eth_bridge_pool::TransferToEthereumKind::Erc20, + ), + ( + EthAddress([0xff; 20]), + eth_bridge_pool::TransferToEthereumKind::Nut, + ), ], ); init_balance(&mut wl_storage, &pending_transfers); @@ -1213,6 +1265,7 @@ mod tests { sent_amount: token::Amount, prev_balance: Option, prev_supply: Option, + kind: eth_bridge_pool::TransferToEthereumKind, } test_wrapped_erc20s_aux(|wl_storage, event| { @@ -1225,29 +1278,48 @@ mod tests { let native_erc20 = read_native_erc20_address(wl_storage).expect("Test failed"); let deltas = transfers - .filter_map(|TransferToEthereum { asset, amount, .. }| { - if asset == &native_erc20 { - return None; - } - let erc20_token = wrapped_erc20s::token(asset); - let prev_balance = wl_storage - .read(&balance_key(&erc20_token, &BRIDGE_POOL_ADDRESS)) - .expect("Test failed"); - let prev_supply = wl_storage - .read(&minted_balance_key(&erc20_token)) - .expect("Test failed"); - Some(Delta { - asset: *asset, - sent_amount: *amount, - prev_balance, - prev_supply, - }) - }) + .filter_map( + |TransferToEthereum { + kind, + asset, + amount, + .. + }| { + if asset == &native_erc20 { + return None; + } + let erc20_token = match kind { + eth_bridge_pool::TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(asset) + } + eth_bridge_pool::TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(asset) + } + }; + let prev_balance = wl_storage + .read(&balance_key( + &erc20_token, + &BRIDGE_POOL_ADDRESS, + )) + .expect("Test failed"); + let prev_supply = wl_storage + .read(&minted_balance_key(&erc20_token)) + .expect("Test failed"); + Some(Delta { + kind: *kind, + asset: *asset, + sent_amount: *amount, + prev_balance, + prev_supply, + }) + }, + ) .collect::>(); _ = act_on(wl_storage, event).unwrap(); for Delta { + kind, ref asset, sent_amount, prev_balance, @@ -1263,7 +1335,14 @@ mod tests { .checked_sub(sent_amount) .expect("Test failed"); - let erc20_token = wrapped_erc20s::token(asset); + let erc20_token = match kind { + eth_bridge_pool::TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(asset) + } + eth_bridge_pool::TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(asset) + } + }; let balance: token::Amount = wl_storage .read(&balance_key(&erc20_token, &BRIDGE_POOL_ADDRESS)) From 8de897da2fa68ffc1c76658c8f1b4f632ae2ef50 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 14 Jul 2023 09:16:27 +0100 Subject: [PATCH 015/103] Check NUT escrow in Bridge pool VP --- shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index df5696e60a..b6232e21fa 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -19,7 +19,6 @@ use namada_core::ledger::eth_bridge::storage::bridge_pool::{ }; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_ethereum_bridge::parameters::read_native_erc20_address; -use namada_ethereum_bridge::storage::wrapped_erc20s; use crate::ledger::native_vp::ethereum_bridge::vp::check_balance_changes; use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; @@ -92,7 +91,7 @@ where transfer: &PendingTransfer, ) -> Result { // check that the assets to be transferred were escrowed - let token = wrapped_erc20s::token(&transfer.transfer.asset); + let token = transfer.token_address(); let owner_key = balance_key(&token, &transfer.transfer.sender); let escrow_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); if keys_changed.contains(&owner_key) @@ -347,6 +346,7 @@ mod test_bridge_pool_vp { use namada_ethereum_bridge::parameters::{ Contracts, EthereumBridgeConfig, UpgradeableContract, }; + use namada_ethereum_bridge::storage::wrapped_erc20s; use super::*; use crate::ledger::gas::VpGasMeter; From a83986679eff0494d9d51c7721b5a096a59f07cd Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 14 Jul 2023 15:57:55 +0100 Subject: [PATCH 016/103] Test NUTs in the Bridge pool --- .../ethereum_bridge/bridge_pool_vp.rs | 173 ++++++++++++++++-- 1 file changed, 159 insertions(+), 14 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index b6232e21fa..9632f4d7f3 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -370,23 +370,33 @@ mod test_bridge_pool_vp { const ASSET: EthAddress = EthAddress([0; 20]); const BERTHA_WEALTH: u64 = 1_000_000; const BERTHA_TOKENS: u64 = 10_000; + const DAES_NUTS: u64 = 10_000; + const DAEWONS_GAS: u64 = 1_000_000; const ESCROWED_AMOUNT: u64 = 1_000; const ESCROWED_TOKENS: u64 = 1_000; + const ESCROWED_NUTS: u64 = 1_000; const GAS_FEE: u64 = 100; const TOKENS: u64 = 100; /// A set of balances for an address struct Balance { + /// NUT or ERC20 Ethereum asset kind. + kind: TransferToEthereumKind, + /// The owner of the ERC20 assets. owner: Address, - balance: Amount, + /// The gas to escrow under the Bridge pool. + gas: Amount, + /// The tokens to be sent across the Ethereum bridge, + /// escrowed to the Bridge pool account. token: Amount, } impl Balance { - fn new(address: Address) -> Self { + fn new(kind: TransferToEthereumKind, address: Address) -> Self { Self { + kind, owner: address, - balance: 0.into(), + gas: 0.into(), token: 0.into(), } } @@ -398,6 +408,22 @@ mod test_bridge_pool_vp { .expect("The token address decoding shouldn't fail") } + /// An implicit user address for testing & development + #[allow(dead_code)] + pub fn daewon_address() -> Address { + use crate::types::key::*; + pub fn daewon_keypair() -> common::SecretKey { + let bytes = [ + 235, 250, 15, 1, 145, 250, 172, 218, 247, 27, 63, 212, 60, 47, + 164, 57, 187, 156, 182, 144, 107, 174, 38, 81, 37, 40, 19, 142, + 68, 135, 57, 50, + ]; + let ed_sk = ed25519::SecretKey::try_from_slice(&bytes).unwrap(); + ed_sk.try_to_sk().unwrap() + } + (&daewon_keypair().ref_to()).into() + } + /// A sampled established address for tests pub fn established_address_1() -> Address { Address::decode("atest1v4ehgw36g56ngwpk8ppnzsf4xqeyvsf3xq6nxde5gseyys3nxgenvvfex5cnyd2rx9zrzwfctgx7sp") @@ -433,20 +459,32 @@ mod test_bridge_pool_vp { writelog .write(&get_pending_key(&transfer), transfer.try_to_vec().unwrap()) .expect("Test failed"); - // set up a user with a balance + // set up users with ERC20 and NUT balances update_balances( &mut writelog, - Balance::new(bertha_address()), + Balance::new(TransferToEthereumKind::Erc20, bertha_address()), SignedAmount::Positive(BERTHA_WEALTH.into()), SignedAmount::Positive(BERTHA_TOKENS.into()), ); + update_balances( + &mut writelog, + Balance::new(TransferToEthereumKind::Nut, daewon_address()), + SignedAmount::Positive(DAEWONS_GAS.into()), + SignedAmount::Positive(DAES_NUTS.into()), + ); // set up the initial balances of the bridge pool update_balances( &mut writelog, - Balance::new(BRIDGE_POOL_ADDRESS), + Balance::new(TransferToEthereumKind::Erc20, BRIDGE_POOL_ADDRESS), SignedAmount::Positive(ESCROWED_AMOUNT.into()), SignedAmount::Positive(ESCROWED_TOKENS.into()), ); + update_balances( + &mut writelog, + Balance::new(TransferToEthereumKind::Nut, BRIDGE_POOL_ADDRESS), + SignedAmount::Positive(ESCROWED_AMOUNT.into()), + SignedAmount::Positive(ESCROWED_NUTS.into()), + ); writelog.commit_tx(); writelog } @@ -460,14 +498,19 @@ mod test_bridge_pool_vp { token_delta: SignedAmount, ) -> BTreeSet { // get the balance keys - let token_key = - balance_key(&wrapped_erc20s::token(&ASSET), &balance.owner); + let token_key = balance_key( + &match balance.kind { + TransferToEthereumKind::Erc20 => wrapped_erc20s::token(&ASSET), + TransferToEthereumKind::Nut => wrapped_erc20s::nut(&ASSET), + }, + &balance.owner, + ); let account_key = balance_key(&nam(), &balance.owner); // update the balance of nam let new_balance = match gas_delta { - SignedAmount::Positive(amount) => balance.balance + amount, - SignedAmount::Negative(amount) => balance.balance - amount, + SignedAmount::Positive(amount) => balance.gas + amount, + SignedAmount::Negative(amount) => balance.gas - amount, } .try_to_vec() .expect("Test failed"); @@ -592,8 +635,9 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + kind: TransferToEthereumKind::Erc20, owner: bertha_address(), - balance: BERTHA_WEALTH.into(), + gas: BERTHA_WEALTH.into(), token: BERTHA_TOKENS.into(), }, payer_gas_delta, @@ -605,8 +649,9 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, - balance: ESCROWED_AMOUNT.into(), + gas: ESCROWED_AMOUNT.into(), token: ESCROWED_TOKENS.into(), }, gas_escrow_delta, @@ -931,8 +976,9 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + kind: TransferToEthereumKind::Erc20, owner: bertha_address(), - balance: BERTHA_WEALTH.into(), + gas: BERTHA_WEALTH.into(), token: BERTHA_TOKENS.into(), }, SignedAmount::Negative(GAS_FEE.into()), @@ -944,8 +990,9 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, - balance: ESCROWED_AMOUNT.into(), + gas: ESCROWED_AMOUNT.into(), token: ESCROWED_TOKENS.into(), }, SignedAmount::Positive(GAS_FEE.into()), @@ -1326,4 +1373,102 @@ mod test_bridge_pool_vp { .expect("Test failed"); assert!(!res); } + + /// Auxiliary function to test NUT functionality. + fn test_nut_aux(kind: TransferToEthereumKind, expect: Expect) { + // setup + let mut wl_storage = setup_storage(); + let tx = Tx::from_type(TxType::Raw); + + // the transfer to be added to the pool + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind, + asset: ASSET, + sender: daewon_address(), + recipient: EthAddress([1; 20]), + amount: TOKENS.into(), + }, + gas_fee: GasFee { + amount: GAS_FEE.into(), + payer: daewon_address(), + }, + }; + + // add transfer to pool + let mut keys_changed = { + wl_storage + .write_log + .write( + &get_pending_key(&transfer), + transfer.try_to_vec().unwrap(), + ) + .unwrap(); + BTreeSet::from([get_pending_key(&transfer)]) + }; + + // update Daewon's balances + let mut new_keys_changed = update_balances( + &mut wl_storage.write_log, + Balance { + kind, + owner: daewon_address(), + gas: DAEWONS_GAS.into(), + token: DAES_NUTS.into(), + }, + SignedAmount::Negative(GAS_FEE.into()), + SignedAmount::Negative(TOKENS.into()), + ); + keys_changed.append(&mut new_keys_changed); + + // change the bridge pool balances + let mut new_keys_changed = update_balances( + &mut wl_storage.write_log, + Balance { + kind, + owner: BRIDGE_POOL_ADDRESS, + gas: ESCROWED_AMOUNT.into(), + token: ESCROWED_NUTS.into(), + }, + SignedAmount::Positive(GAS_FEE.into()), + SignedAmount::Positive(TOKENS.into()), + ); + keys_changed.append(&mut new_keys_changed); + + // create the data to be given to the vp + let verifiers = BTreeSet::default(); + let vp = BridgePoolVp { + ctx: setup_ctx( + &tx, + &wl_storage.storage, + &wl_storage.write_log, + &keys_changed, + &verifiers, + ), + }; + + let mut tx = Tx::from_type(TxType::Raw); + tx.add_data(transfer); + + let res = vp.validate_tx(&tx, &keys_changed, &verifiers); + match expect { + Expect::True => assert!(res.expect("Test failed")), + Expect::False => assert!(!res.expect("Test failed")), + Expect::Error => assert!(res.is_err()), + } + } + + /// Test that the Bridge pool VP rejects a tx based on the fact + /// that an account might hold NUTs of some arbitrary Ethereum + /// asset, but not hold ERC20s. + #[test] + fn test_reject_no_erc20_balance_despite_nut_balance() { + test_nut_aux(TransferToEthereumKind::Erc20, Expect::False) + } + + /// Test the happy flow of escrowing NUTs. + #[test] + fn test_escrowing_nuts_happy_flow() { + test_nut_aux(TransferToEthereumKind::Nut, Expect::True) + } } From 180c986b9a75b82d409c54559b6a9561a2e1e96c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 12 Jul 2023 22:42:49 +0100 Subject: [PATCH 017/103] Add NUT cmdline flag for Bridge pool transfers --- apps/src/lib/cli.rs | 8 ++++++++ shared/src/ledger/args.rs | 5 +++++ shared/src/ledger/eth_bridge/bridge_pool.rs | 9 ++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 1ed2275f2c..e7a34eba67 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2513,6 +2513,7 @@ pub mod args { pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); + pub const NUT: ArgFlag = flag("nut"); pub const OUT_FILE_PATH_OPT: ArgOpt = arg_opt("out-file-path"); pub const OUTPUT_FOLDER_PATH: ArgOpt = arg_opt("output-folder-path"); @@ -2787,6 +2788,7 @@ pub mod args { impl CliToSdk> for EthereumBridgePool { fn to_sdk(self, ctx: &mut Context) -> EthereumBridgePool { EthereumBridgePool:: { + nut: self.nut, tx: self.tx.to_sdk(ctx), asset: self.asset, recipient: self.recipient, @@ -2809,6 +2811,7 @@ pub mod args { let fee_amount = FEE_AMOUNT.parse(matches).amount; let fee_payer = FEE_PAYER.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); + let nut = NUT.parse(matches); Self { tx, asset, @@ -2818,6 +2821,7 @@ pub mod args { fee_amount, fee_payer, code_path, + nut, } } @@ -2852,6 +2856,10 @@ pub mod args { "The Namada address of the account paying the fee.", ), ) + .arg(NUT.def().help( + "Add Non Usable Tokens (NUTs) to the Bridge pool. These \ + are usually obtained from invalid transfers to Namada.", + )) } } diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 2d426d5510..8d884b4a5f 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -701,6 +701,11 @@ pub struct RecommendBatch { /// A transfer to be added to the Ethereum bridge pool. #[derive(Clone, Debug)] pub struct EthereumBridgePool { + /// Whether the transfer is for a NUT. + /// + /// By default, we add wrapped ERC20s onto the + /// Bridge pool. + pub nut: bool, /// The args for building a tx to the bridge pool pub tx: Tx, /// The type of token diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 596538bb29..940524d34f 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -39,6 +39,7 @@ pub async fn build_bridge_pool_tx( client: &C, args::EthereumBridgePool { tx: tx_args, + nut, asset, recipient, sender, @@ -60,9 +61,11 @@ pub async fn build_bridge_pool_tx( recipient, sender: sender.clone(), amount, - // TODO: dispatch on the transfer kind, based - // on CLI arguments - kind: TransferToEthereumKind::Erc20, + kind: if nut { + TransferToEthereumKind::Nut + } else { + TransferToEthereumKind::Erc20 + }, }, gas_fee: GasFee { amount: fee_amount, From 7732e249e354b1fce028aa4e3d94ae56a85bc792 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 15:39:21 +0100 Subject: [PATCH 018/103] Update ethbridge-rs to v0.20.0 --- Cargo.lock | 24 ++++++++++++------------ apps/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- shared/Cargo.toml | 4 ++-- wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4359495965..cb0660f4c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,8 +2021,8 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2032,8 +2032,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2043,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2054,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethers", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index a5c4dec197..0fcade9cac 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -87,9 +87,9 @@ derivative.workspace = true directories.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} eyre.workspace = true fd-lock.workspace = true ferveo-common.workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index 5da1f1f6d6..b596d1fb10 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0" } +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0" } eyre.workspace = true ferveo = {optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index d6c64d6e01..db949a9b7e 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -100,8 +100,8 @@ clru.workspace = true data-encoding.workspace = true derivation-path.workspace = true derivative.workspace = true -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.19.0"} +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} ethers.workspace = true eyre.workspace = true futures.workspace = true diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 2e8816fd54..9c180cd817 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 95d016ca57..cecb47a479 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.19.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.19.0#70122ae122dbf59d6b0de804f232e6acbfc8bc6f" +version = "0.20.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" dependencies = [ "ethabi", "ethers", From 92bb87f51b8a7d062b15e55e32b6a309f967b284 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 15:39:42 +0100 Subject: [PATCH 019/103] Remove whitelist Ethereum events from the ledger --- .../lib/node/ledger/ethereum_oracle/events.rs | 52 ++----------------- .../shell/vote_extensions/eth_events.rs | 5 -- core/src/types/ethereum_events.rs | 34 ------------ core/src/types/storage.rs | 1 - .../transactions/ethereum_events/events.rs | 4 -- 5 files changed, 5 insertions(+), 91 deletions(-) diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 10837829de..645b87d1c4 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -7,15 +7,15 @@ pub mod eth_events { }; use ethbridge_events::{DynEventCodec, Events as RawEvents}; use ethbridge_governance_events::{ - GovernanceEvents, NewContractFilter, UpdateBridgeWhitelistFilter, - UpgradedContractFilter, ValidatorSetUpdateFilter, + GovernanceEvents, NewContractFilter, UpgradedContractFilter, + ValidatorSetUpdateFilter, }; use namada::core::types::ethereum_structs; use namada::eth_bridge::ethers::contract::EthEvent; use namada::types::address::Address; use namada::types::ethereum_events::{ - EthAddress, EthereumEvent, TokenWhitelist, TransferToEthereum, - TransferToEthereumKind, TransferToNamada, Uint, + EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, + TransferToNamada, Uint, }; use namada::types::keccak::KeccakHash; use namada::types::token::Amount; @@ -106,31 +106,6 @@ pub mod eth_events { NewContractFilter::name().into(), )); } - RawEvents::Governance( - GovernanceEvents::UpdateBridgeWhitelistFilter( - UpdateBridgeWhitelistFilter { - nonce, - tokens, - token_cap, - }, - ), - ) => { - let mut whitelist = vec![]; - - for (token, cap) in - tokens.into_iter().zip(token_cap.into_iter()) - { - whitelist.push(TokenWhitelist { - token: token.parse_eth_address()?, - cap: cap.parse_amount()?, - }); - } - - EthereumEvent::UpdateBridgeWhitelist { - nonce: nonce.parse_uint256()?, - whitelist, - } - } RawEvents::Governance( GovernanceEvents::UpgradedContractFilter( UpgradedContractFilter { name: _, addr: _ }, @@ -342,7 +317,7 @@ pub mod eth_events { use ethabi::ethereum_types::{H160, U256}; use ethbridge_events::{ TRANSFER_TO_ERC_CODEC, TRANSFER_TO_NAMADA_CODEC, - UPDATE_BRIDGE_WHITELIST_CODEC, VALIDATOR_SET_UPDATE_CODEC, + VALIDATOR_SET_UPDATE_CODEC, }; use namada::eth_bridge::ethers::abi::AbiEncode; @@ -553,11 +528,6 @@ pub mod eth_events { bridge_validator_set_hash: [1; 32], governance_validator_set_hash: [2; 32], }; - let whitelist = UpdateBridgeWhitelistFilter { - nonce: 0u64.into(), - tokens: vec![H160([0; 20]); 2], - token_cap: vec![0u64.into(); 2], - }; assert_eq!( { let decoded: TransferToNamadaFilter = @@ -593,18 +563,6 @@ pub mod eth_events { }, update ); - assert_eq!( - { - let decoded: UpdateBridgeWhitelistFilter = - UPDATE_BRIDGE_WHITELIST_CODEC - .decode(&get_log(whitelist.clone().encode())) - .expect("Test failed") - .try_into() - .expect("Test failed"); - decoded - }, - whitelist - ); } /// Return an Ethereum events log, from the given encoded event diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index a7006abecc..5282864e6d 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -276,11 +276,6 @@ where return Err(VoteExtensionError::InvalidNamNonce); } } - EthereumEvent::UpdateBridgeWhitelist { .. } => { - // TODO: check nonce of whitelist update; - // for this, we need to store the nonce of - // whitelist updates somewhere - } // consider other ethereum event kinds valid _ => {} } diff --git a/core/src/types/ethereum_events.rs b/core/src/types/ethereum_events.rs index 75bfc5f5cc..b896674a0c 100644 --- a/core/src/types/ethereum_events.rs +++ b/core/src/types/ethereum_events.rs @@ -334,16 +334,6 @@ pub enum EthereumEvent { #[allow(dead_code)] address: EthAddress, }, - /// Event indication a new Ethereum based token has been whitelisted for - /// transfer across the bridge - UpdateBridgeWhitelist { - /// Monotonically increasing nonce - #[allow(dead_code)] - nonce: Uint, - /// Tokens to be allowed to be transferred across the bridge - #[allow(dead_code)] - whitelist: Vec, - }, } impl EthereumEvent { @@ -462,30 +452,6 @@ pub struct TransferToEthereum { pub gas_payer: Address, } -/// struct for whitelisting a token from Ethereum. -/// Includes the address of issuing contract and -/// a cap on the max amount of this token allowed to be -/// held by the bridge. -#[derive( - Clone, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - BorshSerialize, - BorshDeserialize, - BorshSchema, -)] -#[allow(dead_code)] -pub struct TokenWhitelist { - /// Address of Ethereum smart contract issuing token - pub token: EthAddress, - /// Maximum amount of token allowed on the bridge - pub cap: Amount, -} - #[cfg(test)] pub mod tests { use std::str::FromStr; diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 0f0a6032f0..a39089aa04 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -1254,7 +1254,6 @@ pub struct PrefixValue { pub struct EthEventsQueue { /// Queue of transfer to Namada events. pub transfers_to_namada: InnerEthEventsQueue, - // TODO: add queue of update whitelist events } /// A queue of confirmed Ethereum events of type `E`. diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index da967b85a0..3cfbf69fd1 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -752,10 +752,6 @@ mod tests { name: "bridge".to_string(), address: arbitrary_eth_address(), }, - EthereumEvent::UpdateBridgeWhitelist { - nonce: arbitrary_nonce(), - whitelist: vec![], - }, EthereumEvent::UpgradedContract { name: "bridge".to_string(), address: arbitrary_eth_address(), From a1213c7adde739978119e859710c8cc0494c9292 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 13 Jul 2023 16:47:32 +0100 Subject: [PATCH 020/103] Add whitelisted ERC20 tokens to the genesis file --- apps/src/lib/config/genesis.rs | 12 +- apps/src/lib/node/ledger/shell/mod.rs | 23 +-- ethereum_bridge/src/parameters.rs | 133 +++++++++++++++--- ethereum_bridge/src/test_utils.rs | 3 + .../ethereum_bridge/bridge_pool_vp.rs | 1 + .../ledger/native_vp/ethereum_bridge/vp.rs | 1 + tests/src/native_vp/eth_bridge_pool.rs | 1 + 7 files changed, 137 insertions(+), 37 deletions(-) diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index e9abf6c26d..2b920839f0 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -909,10 +909,13 @@ pub fn genesis( } #[cfg(any(test, feature = "dev"))] pub fn genesis(num_validators: u64) -> Genesis { - use namada::ledger::eth_bridge::{Contracts, UpgradeableContract}; + use namada::ledger::eth_bridge::{ + Contracts, Erc20WhitelistEntry, UpgradeableContract, + }; use namada::types::address::{ self, apfel, btc, dot, eth, kartoffel, nam, schnitzel, wnam, }; + use namada::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; use namada::types::ethereum_events::EthAddress; use crate::wallet; @@ -1115,6 +1118,13 @@ pub fn genesis(num_validators: u64) -> Genesis { gov_params: GovernanceParameters::default(), pgf_params: PgfParameters::default(), ethereum_bridge_params: Some(EthereumBridgeConfig { + erc20_whitelist: vec![Erc20WhitelistEntry { + token_address: DAI_ERC20_ETH_ADDRESS, + token_cap: token::DenominatedAmount { + amount: token::Amount::max(), + denom: 18.into(), + }, + }], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index c65fbb3f99..c5e89225e3 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -28,7 +28,7 @@ use std::rc::Rc; use borsh::{BorshDeserialize, BorshSerialize}; use namada::core::ledger::eth_bridge; -use namada::ledger::eth_bridge::{EthBridgeQueries, EthereumBridgeConfig}; +use namada::ledger::eth_bridge::{EthBridgeQueries, EthereumOracleConfig}; use namada::ledger::events::log::EventLog; use namada::ledger::events::Event; use namada::ledger::gas::BlockGasMeter; @@ -968,27 +968,16 @@ where ); return; } - let Some(config) = EthereumBridgeConfig::read(&self.wl_storage) else { - tracing::info!( - "Not starting oracle as the Ethereum bridge config couldn't be found in storage" - ); - return; - }; + let config = EthereumOracleConfig::read(&self.wl_storage).expect( + "The oracle config must be present in storage, since the \ + bridge is enabled", + ); let start_block = self .wl_storage .storage .ethereum_height .clone() - .unwrap_or_else(|| { - self.wl_storage - .read(ð_bridge::storage::eth_start_height_key()) - .expect( - "Failed to read Ethereum start height from storage", - ) - .expect( - "The Ethereum start height should be in storage", - ) - }); + .unwrap_or(config.eth_start_height); tracing::info!( ?start_block, "Found Ethereum height from which the Ethereum oracle should \ diff --git a/ethereum_bridge/src/parameters.rs b/ethereum_bridge/src/parameters.rs index 4f3b2f1bc5..e657167eca 100644 --- a/ethereum_bridge/src/parameters.rs +++ b/ethereum_bridge/src/parameters.rs @@ -3,6 +3,7 @@ use std::num::NonZeroU64; use borsh::{BorshDeserialize, BorshSerialize}; use eyre::{eyre, Result}; +use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage; use namada_core::ledger::storage::types::encode; use namada_core::ledger::storage::WlStorage; @@ -10,11 +11,33 @@ use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::ethereum_events::EthAddress; use namada_core::types::ethereum_structs; use namada_core::types::storage::Key; +use namada_core::types::token::DenominatedAmount; use serde::{Deserialize, Serialize}; -use crate::storage::eth_bridge_queries::{EthBridgeEnabled, EthBridgeStatus}; +use crate::storage::eth_bridge_queries::{ + EthBridgeEnabled, EthBridgeQueries, EthBridgeStatus, +}; use crate::{bridge_pool_vp, storage as bridge_storage, vp}; +/// An ERC20 token whitelist entry. +#[derive( + Clone, + Copy, + Eq, + PartialEq, + Debug, + Deserialize, + Serialize, + BorshSerialize, + BorshDeserialize, +)] +pub struct Erc20WhitelistEntry { + /// The address of the whitelisted ERC20 token. + pub token_address: EthAddress, + /// The token cap of the whitelisted ERC20 token. + pub token_cap: DenominatedAmount, +} + /// Represents a configuration value for the minimum number of /// confirmations an Ethereum event must reach before it can be acted on. #[derive( @@ -135,6 +158,8 @@ pub struct EthereumBridgeConfig { /// Minimum number of confirmations needed to trust an Ethereum branch. /// This must be at least one. pub min_confirmations: MinimumConfirmations, + /// List of ERC20 token types whitelisted at genesis time. + pub erc20_whitelist: Vec, /// The addresses of the Ethereum contracts that need to be directly known /// by validators. pub contracts: Contracts, @@ -151,6 +176,7 @@ impl EthereumBridgeConfig { H: 'static + storage::traits::StorageHasher, { let Self { + erc20_whitelist, eth_start_height, min_confirmations, contracts: @@ -187,13 +213,71 @@ impl EthereumBridgeConfig { wl_storage .write_bytes(ð_start_height_key, encode(eth_start_height)) .unwrap(); + for Erc20WhitelistEntry { + token_address: addr, + token_cap: DenominatedAmount { amount: cap, denom }, + } in erc20_whitelist + { + let key = whitelist::Key { + asset: *addr, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + wl_storage.write_bytes(&key, encode(&true)).unwrap(); + + let key = whitelist::Key { + asset: *addr, + suffix: whitelist::KeyType::Cap, + } + .into(); + wl_storage.write_bytes(&key, encode(cap)).unwrap(); + + let key = whitelist::Key { + asset: *addr, + suffix: whitelist::KeyType::Denomination, + } + .into(); + wl_storage.write_bytes(&key, encode(denom)).unwrap(); + } // Initialize the storage for the Ethereum Bridge VP. vp::init_storage(wl_storage); // Initialize the storage for the Bridge Pool VP. bridge_pool_vp::init_storage(wl_storage); } +} + +/// Subset of [`EthereumBridgeConfig`], containing only Ethereum +/// oracle specific parameters. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EthereumOracleConfig { + /// Initial Ethereum block height when events will first be extracted from. + pub eth_start_height: ethereum_structs::BlockHeight, + /// Minimum number of confirmations needed to trust an Ethereum branch. + /// This must be at least one. + pub min_confirmations: MinimumConfirmations, + /// The addresses of the Ethereum contracts that need to be directly known + /// by validators. + pub contracts: Contracts, +} + +impl From for EthereumOracleConfig { + fn from(config: EthereumBridgeConfig) -> Self { + let EthereumBridgeConfig { + eth_start_height, + min_confirmations, + contracts, + .. + } = config; + Self { + eth_start_height, + min_confirmations, + contracts, + } + } +} - /// Reads the latest [`EthereumBridgeConfig`] from storage. If it is not +impl EthereumOracleConfig { + /// Reads the latest [`EthereumOracleConfig`] from storage. If it is not /// present, `None` will be returned - this could be the case if the bridge /// has not been bootstrapped yet. Panics if the storage appears to be /// corrupt. @@ -202,25 +286,27 @@ impl EthereumBridgeConfig { DB: 'static + storage::DB + for<'iter> storage::DBIter<'iter>, H: 'static + storage::traits::StorageHasher, { + // TODO(namada#1720): remove present key check; `is_bridge_active` + // should not panic, when the active status key has not been + // written to; simply return bridge disabled instead + let has_active_key = + wl_storage.has_key(&bridge_storage::active_key()).unwrap(); + + if !has_active_key || !wl_storage.ethbridge_queries().is_bridge_active() + { + return None; + } + let min_confirmations_key = bridge_storage::min_confirmations_key(); let native_erc20_key = bridge_storage::native_erc20_key(); let bridge_contract_key = bridge_storage::bridge_contract_key(); let governance_contract_key = bridge_storage::governance_contract_key(); let eth_start_height_key = bridge_storage::eth_start_height_key(); - let Some(min_confirmations) = StorageRead::read::( - wl_storage, - &min_confirmations_key, - ) - .unwrap_or_else(|err| { - panic!("Could not read {min_confirmations_key}: {err:?}") - }) else { - // The bridge has not been configured yet - return None; - }; - // These reads must succeed otherwise the storage is corrupt or a // read failed + let min_confirmations = + must_read_key(wl_storage, &min_confirmations_key); let native_erc20 = must_read_key(wl_storage, &native_erc20_key); let bridge_contract = must_read_key(wl_storage, &bridge_contract_key); let governance_contract = @@ -299,6 +385,7 @@ mod tests { #[test] fn test_round_trip_toml_serde() -> Result<()> { let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), contracts: Contracts { @@ -324,6 +411,7 @@ mod tests { fn test_ethereum_bridge_config_read_write_storage() { let mut wl_storage = TestWlStorage::default(); let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), contracts: Contracts { @@ -340,7 +428,8 @@ mod tests { }; config.init_storage(&mut wl_storage); - let read = EthereumBridgeConfig::read(&wl_storage).unwrap(); + let read = EthereumOracleConfig::read(&wl_storage).unwrap(); + let config = EthereumOracleConfig::from(config); assert_eq!(config, read); } @@ -348,7 +437,7 @@ mod tests { #[test] fn test_ethereum_bridge_config_uninitialized() { let wl_storage = TestWlStorage::default(); - let read = EthereumBridgeConfig::read(&wl_storage); + let read = EthereumOracleConfig::read(&wl_storage); assert!(read.is_none()); } @@ -358,6 +447,7 @@ mod tests { fn test_ethereum_bridge_config_storage_corrupt() { let mut wl_storage = TestWlStorage::default(); let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::default(), contracts: Contracts { @@ -379,7 +469,7 @@ mod tests { .unwrap(); // This should panic because the min_confirmations value is not valid - EthereumBridgeConfig::read(&wl_storage); + EthereumOracleConfig::read(&wl_storage); } #[test] @@ -388,16 +478,21 @@ mod tests { )] fn test_ethereum_bridge_config_storage_partially_configured() { let mut wl_storage = TestWlStorage::default(); + wl_storage + .write_bytes( + &bridge_storage::active_key(), + encode(&EthBridgeStatus::Enabled(EthBridgeEnabled::AtGenesis)), + ) + .unwrap(); // Write a valid min_confirmations value - let min_confirmations_key = bridge_storage::min_confirmations_key(); wl_storage .write_bytes( - &min_confirmations_key, + &bridge_storage::min_confirmations_key(), MinimumConfirmations::default().try_to_vec().unwrap(), ) .unwrap(); // This should panic as the other config values are not written - EthereumBridgeConfig::read(&wl_storage); + EthereumOracleConfig::read(&wl_storage); } } diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index f2a1ee0b8c..aec6463396 100644 --- a/ethereum_bridge/src/test_utils.rs +++ b/ethereum_bridge/src/test_utils.rs @@ -92,6 +92,8 @@ pub fn bootstrap_ethereum_bridge( wl_storage: &mut TestWlStorage, ) -> EthereumBridgeConfig { let config = EthereumBridgeConfig { + // start with empty erc20 whitelist + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: MinimumConfirmations::from(unsafe { // SAFETY: The only way the API contract of `NonZeroU64` can @@ -212,6 +214,7 @@ pub fn init_storage_with_validators( ) .expect("Test failed"); let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 9632f4d7f3..949df882b5 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -539,6 +539,7 @@ mod test_bridge_pool_vp { fn setup_storage() -> WlStorage { // a dummy config for testing let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 5fd7aa6cd1..715b8ad616 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -393,6 +393,7 @@ mod tests { // a dummy config for testing let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index ee3c659591..f972215c71 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -63,6 +63,7 @@ mod test_bridge_pool_vp { ..Default::default() }; let config = EthereumBridgeConfig { + erc20_whitelist: vec![], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { From 477703871029dc324c6cddcf4ddad06301ed5c12 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Sat, 15 Jul 2023 13:15:25 +0100 Subject: [PATCH 021/103] Update ethbridge-rs to v0.21.0 --- Cargo.lock | 24 ++++++++++++------------ apps/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- shared/Cargo.toml | 4 ++-- wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb0660f4c0..d8131f58e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,8 +2021,8 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2032,8 +2032,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2043,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2054,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 0fcade9cac..0c8557b774 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -87,9 +87,9 @@ derivative.workspace = true directories.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} eyre.workspace = true fd-lock.workspace = true ferveo-common.workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index b596d1fb10..195f419b39 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0" } +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0" } eyre.workspace = true ferveo = {optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index db949a9b7e..8ea2c055b5 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -100,8 +100,8 @@ clru.workspace = true data-encoding.workspace = true derivation-path.workspace = true derivative.workspace = true -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.20.0"} +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} ethers.workspace = true eyre.workspace = true futures.workspace = true diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 9c180cd817..a273155cd2 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index cecb47a479..6d5a227eb4 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.20.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.20.0#1b905d96f7c121dd35e92e153f190943f6dfea0b" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", From cde95039d8013bfd4ae385b57fc93d5e9c23ff0b Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 31 Jul 2023 13:25:32 +0100 Subject: [PATCH 022/103] Move ethbridge-rs deps to the workspace --- Cargo.toml | 6 ++++++ apps/Cargo.toml | 6 +++--- core/Cargo.toml | 2 +- shared/Cargo.toml | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2d50daf0b6..d524d0c7a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,12 @@ directories = "4.0.1" ed25519-consensus = "1.2.0" escargot = "0.5.7" ethabi = "18.0.0" +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0" } ethers = "2.0.0" expectrl = "0.7.0" eyre = "0.6.5" diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 0c8557b774..16fe028be3 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -87,9 +87,9 @@ derivative.workspace = true directories.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-bridge-events.workspace = true +ethbridge-events.workspace = true +ethbridge-governance-events.workspace = true eyre.workspace = true fd-lock.workspace = true ferveo-common.workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index 195f419b39..75ea0c64c2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -66,7 +66,7 @@ data-encoding.workspace = true derivative.workspace = true ed25519-consensus.workspace = true ethabi.workspace = true -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0" } +ethbridge-structs.workspace = true eyre.workspace = true ferveo = {optional = true, git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} ferveo-common = {git = "https://github.com/anoma/ferveo", rev = "e5abd0acc938da90140351a65a26472eb495ce4d"} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 8ea2c055b5..35c5156cac 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -100,8 +100,8 @@ clru.workspace = true data-encoding.workspace = true derivation-path.workspace = true derivative.workspace = true -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} +ethbridge-bridge-contract.workspace = true +ethbridge-governance-contract.workspace = true ethers.workspace = true eyre.workspace = true futures.workspace = true From 20044d015688eb8cd638b456d6d0d9fbf773f943 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 31 Jul 2023 13:40:24 +0100 Subject: [PATCH 023/103] Add changelog entry for the Ethereum token whitelist Closes #1290 --- .changelog/unreleased/features/1290-token-whitelist.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1290-token-whitelist.md diff --git a/.changelog/unreleased/features/1290-token-whitelist.md b/.changelog/unreleased/features/1290-token-whitelist.md new file mode 100644 index 0000000000..57bf9e61bb --- /dev/null +++ b/.changelog/unreleased/features/1290-token-whitelist.md @@ -0,0 +1,2 @@ +- Implement Ethereum token whitelist. + ([\#1290](https://github.com/anoma/namada/issues/1290)) \ No newline at end of file From 2ae23fb132d3a0c6429ab2cf2d5f0e90765ea939 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 09:44:41 +0100 Subject: [PATCH 024/103] Add helper methods to EthAssetMint --- ethereum_bridge/src/storage/eth_bridge_queries.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 0a738d96a4..08fdcab2fa 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -500,6 +500,20 @@ pub struct EthAssetMint { pub erc20_amount: token::Amount, } +impl EthAssetMint { + /// Check if NUTs should be minted. + #[inline] + pub fn should_mint_nuts(&self) -> bool { + !self.nut_amount.is_zero() + } + + /// Check if ERC20s should be minted. + #[inline] + pub fn should_mint_erc20s(&self) -> bool { + !self.erc20_amount.is_zero() + } +} + /// A handle to the Ethereum addresses of the set of consensus /// validators in Namada, at some given epoch. pub struct ConsensusEthAddresses<'db, D, H> From f1be3f97d0997184744bade64436eadfd0fb1ef9 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 10:14:33 +0100 Subject: [PATCH 025/103] Use EthAssetMint helper methods --- .../protocol/transactions/ethereum_events/events.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 3cfbf69fd1..df386f5c43 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -145,14 +145,14 @@ where // TODO: query denomination of the whitelisted token from storage, // and print this amount with the proper formatting; for now, use // NAM's formatting - if !asset_count.erc20_amount.is_zero() { + if asset_count.should_mint_erc20s() { tracing::info!( "Minted wrapped ERC20s - (asset - {asset}, receiver - \ {receiver}, amount - {})", asset_count.erc20_amount.to_string_native(), ); } - if !asset_count.nut_amount.is_zero() { + if asset_count.should_mint_nuts() { tracing::info!( "Minted NUTs - (asset - {asset}, receiver - {receiver}, \ amount - {})", @@ -254,10 +254,12 @@ where let assets_to_mint = [ // check if we should mint nuts - (!asset_count.nut_amount.is_zero()) + asset_count + .should_mint_nuts() .then(|| (wrapped_erc20s::nut(asset), asset_count.nut_amount)), // check if we should mint erc20s - (!asset_count.erc20_amount.is_zero()) + asset_count + .should_mint_erc20s() .then(|| (wrapped_erc20s::token(asset), asset_count.erc20_amount)), ] .into_iter() From 6ef086f5a2baf57bf1425bc1aafde54ebed9c972 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 10:04:09 +0100 Subject: [PATCH 026/103] Refactor redeem_native_token to use update::amount --- .../transactions/ethereum_events/events.rs | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index df386f5c43..c84b868285 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -183,45 +183,44 @@ where let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, receiver); - let eth_bridge_native_token_balance_pre: token::Amount = - StorageRead::read(wl_storage, ð_bridge_native_token_balance_key)? - .expect( - "Ethereum bridge must always have an explicit balance of the \ - native token", - ); - let receiver_native_token_balance_pre: token::Amount = - StorageRead::read(wl_storage, &receiver_native_token_balance_key)? - .unwrap_or_default(); - - let eth_bridge_native_token_balance_post = - eth_bridge_native_token_balance_pre - .checked_sub(*amount) - .expect( - "Ethereum bridge should always have enough native tokens to \ - redeem any confirmed transfers", - ); - let receiver_native_token_balance_post = receiver_native_token_balance_pre - .checked_add(*amount) - .expect("Receiver's balance is full"); - - StorageWrite::write( + update::amount( wl_storage, ð_bridge_native_token_balance_key, - eth_bridge_native_token_balance_post, + |balance| { + tracing::debug!( + %eth_bridge_native_token_balance_key, + ?balance, + "Existing value found", + ); + balance.spend(amount); + tracing::debug!( + %eth_bridge_native_token_balance_key, + ?balance, + "New value calculated", + ); + }, )?; - StorageWrite::write( + update::amount( wl_storage, &receiver_native_token_balance_key, - receiver_native_token_balance_post, + |balance| { + tracing::debug!( + %receiver_native_token_balance_key, + ?balance, + "Existing value found", + ); + balance.receive(amount); + tracing::debug!( + %receiver_native_token_balance_key, + ?balance, + "New value calculated", + ); + }, )?; tracing::info!( amount = %amount.to_string_native(), %receiver, - eth_bridge_native_token_balance_pre = %eth_bridge_native_token_balance_pre.to_string_native(), - eth_bridge_native_token_balance_post = %eth_bridge_native_token_balance_post.to_string_native(), - receiver_native_token_balance_pre = %receiver_native_token_balance_pre.to_string_native(), - receiver_native_token_balance_post = %receiver_native_token_balance_post.to_string_native(), "Redeemed native token for wrapped ERC20 token" ); Ok(BTreeSet::from([ From f1bd957593fa238c3dce2c9a13d16cfa643b0bb4 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 10:26:08 +0100 Subject: [PATCH 027/103] Vet Ethereum bridge config of the native token --- ethereum_bridge/src/parameters.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ethereum_bridge/src/parameters.rs b/ethereum_bridge/src/parameters.rs index e657167eca..6395dd6cab 100644 --- a/ethereum_bridge/src/parameters.rs +++ b/ethereum_bridge/src/parameters.rs @@ -11,7 +11,7 @@ use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::ethereum_events::EthAddress; use namada_core::types::ethereum_structs; use namada_core::types::storage::Key; -use namada_core::types::token::DenominatedAmount; +use namada_core::types::token::{DenominatedAmount, NATIVE_MAX_DECIMAL_PLACES}; use serde::{Deserialize, Serialize}; use crate::storage::eth_bridge_queries::{ @@ -218,6 +218,15 @@ impl EthereumBridgeConfig { token_cap: DenominatedAmount { amount: cap, denom }, } in erc20_whitelist { + if addr == native_erc20 + && denom != &NATIVE_MAX_DECIMAL_PLACES.into() + { + panic!( + "Error writing Ethereum bridge config: The native token \ + should have {NATIVE_MAX_DECIMAL_PLACES} decimal places" + ); + } + let key = whitelist::Key { asset: *addr, suffix: whitelist::KeyType::Whitelisted, From 69037fbe27cdc6dfa7651f6dfd4d30dafb19f467 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 11:46:51 +0100 Subject: [PATCH 028/103] Harden Bridge pool VP against wNAM NUT transfers --- .../ethereum_bridge/bridge_pool_vp.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 949df882b5..c5a936d103 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -14,6 +14,7 @@ use std::collections::BTreeSet; use borsh::BorshDeserialize; use eyre::eyre; +use namada_core::hints; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_pending_key, is_bridge_pool_key, BRIDGE_POOL_ADDRESS, }; @@ -26,7 +27,7 @@ use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::{DBIter, DB}; use crate::proto::Tx; use crate::types::address::{Address, InternalAddress}; -use crate::types::eth_bridge_pool::PendingTransfer; +use crate::types::eth_bridge_pool::{PendingTransfer, TransferToEthereumKind}; use crate::types::ethereum_events::EthAddress; use crate::types::storage::Key; use crate::types::token::{balance_key, Amount}; @@ -316,6 +317,19 @@ where } // check the escrowed assets if transfer.transfer.asset == wnam_address { + if hints::unlikely(matches!( + &transfer.transfer.kind, + TransferToEthereumKind::Nut + )) { + // NB: this should never be possible: protocol tx state updates + // never result in wNAM NUTs being minted. in turn, this means + // that users should never hold wNAM NUTs. doesn't hurt to add + // the extra check to the vp, though + tracing::error!( + "Attempted to add a wNAM NUT transfer to the Bridge pool" + ); + return Ok(false); + } // if we are going to mint wNam on Ethereum, the appropriate // amount of Nam must be escrowed in the Ethereum bridge VP's // storage. @@ -357,9 +371,7 @@ mod test_bridge_pool_vp { use crate::ledger::storage_api::StorageWrite; use crate::types::address::{nam, wnam}; use crate::types::chain::ChainId; - use crate::types::eth_bridge_pool::{ - GasFee, TransferToEthereum, TransferToEthereumKind, - }; + use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; use crate::types::hash::Hash; use crate::types::storage::TxIndex; use crate::types::transaction::TxType; From a26de50346fb96a52d071ed60d52fd1a0adc3fe8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 11:48:56 +0100 Subject: [PATCH 029/103] Update wNAM supplies when acting on Ethereum events --- .../transactions/ethereum_events/events.rs | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index c84b868285..3c92d3dcab 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -19,7 +19,9 @@ use namada_core::ledger::storage::traits::StorageHasher; use namada_core::ledger::storage::{DBIter, WlStorage, DB}; use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::address::Address; -use namada_core::types::eth_bridge_pool::PendingTransfer; +use namada_core::types::eth_bridge_pool::{ + PendingTransfer, TransferToEthereumKind, +}; use namada_core::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, TransferToNamada, TransfersToNamada, @@ -161,7 +163,12 @@ where } changed } else { - redeem_native_token(wl_storage, receiver, amount)? + redeem_native_token( + wl_storage, + &wrapped_native_erc20, + receiver, + amount, + )? }; changed_keys.append(&mut changed) } @@ -171,6 +178,7 @@ where /// Redeems `amount` of the native token for `receiver` from escrow. fn redeem_native_token( wl_storage: &mut WlStorage, + native_erc20: &EthAddress, receiver: &Address, amount: &token::Amount, ) -> Result> @@ -182,6 +190,8 @@ where token::balance_key(&wl_storage.storage.native_token, &BRIDGE_ADDRESS); let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, receiver); + let native_werc20_supply_key = + minted_balance_key(&wrapped_erc20s::token(native_erc20)); update::amount( wl_storage, @@ -217,6 +227,19 @@ where ); }, )?; + update::amount(wl_storage, &native_werc20_supply_key, |balance| { + tracing::debug!( + %native_werc20_supply_key, + ?balance, + "Existing value found", + ); + balance.spend(amount); + tracing::debug!( + %native_werc20_supply_key, + ?balance, + "New value calculated", + ); + })?; tracing::info!( amount = %amount.to_string_native(), @@ -226,6 +249,7 @@ where Ok(BTreeSet::from([ eth_bridge_native_token_balance_key, receiver_native_token_balance_key, + native_werc20_supply_key, ])) } @@ -357,7 +381,7 @@ where "Valid transfer to Ethereum detected, compensating the \ relayer and burning any Ethereum assets in Namada" ); - changed_keys.append(&mut burn_transferred_assets( + changed_keys.append(&mut update_transferred_asset_balances( wl_storage, &pending_transfer, )?); @@ -529,7 +553,9 @@ where Ok(changed_keys) } -fn burn_transferred_assets( +/// Burns any transferred ERC20s other than wNAM. If NAM is transferred, +/// update the wNAM supply key. +fn update_transferred_asset_balances( wl_storage: &mut WlStorage, transfer: &PendingTransfer, ) -> Result> @@ -544,12 +570,26 @@ where return Err(eyre::eyre!("Could not read wNam key from storage")); }; + let token = transfer.token_address(); + + // the wrapped NAM supply increases when we transfer to Ethereum if transfer.transfer.asset == native_erc20_addr { - tracing::debug!(?transfer, "Keeping wrapped NAM in escrow"); + if hints::unlikely(matches!( + &transfer.transfer.kind, + TransferToEthereumKind::Nut + )) { + unreachable!("Attempted to mint wNAM NUTs!"); + } + let supply_key = minted_balance_key(&token); + update::amount(wl_storage, &supply_key, |supply| { + supply.receive(&transfer.transfer.amount); + })?; + _ = changed_keys.insert(supply_key); + tracing::debug!(?transfer, "Updated wrapped NAM supply"); return Ok(changed_keys); } - let token = transfer.token_address(); + // other asset kinds must be burned let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); update::amount(wl_storage, &escrow_balance_key, |balance| { @@ -1161,8 +1201,13 @@ mod tests { let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, &receiver); - let changed_keys = - redeem_native_token(&mut wl_storage, &receiver, &amount)?; + let native_erc20 = read_native_erc20_address(&wl_storage)?; + let changed_keys = redeem_native_token( + &mut wl_storage, + &native_erc20, + &receiver, + &amount, + )?; assert_eq!( changed_keys, From c67a65604ce009229179c21aeac260f153ad04a8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 1 Aug 2023 15:27:19 +0100 Subject: [PATCH 030/103] Allow null pre-balances during Bridge pool escrow checks --- .../native_vp/ethereum_bridge/bridge_pool_vp.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index c5a936d103..8585e9a79b 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -67,10 +67,15 @@ where let account_key = balance_key(&self.ctx.storage.native_token, address); let before: Amount = (&self.ctx) .read_pre_value(&account_key) - .unwrap_or_else(|error| { + .map_err(|error| { tracing::warn!(?error, %account_key, "reading pre value"); - None - })?; + }) + .ok()? + // NB: the previous balance of the given account might + // have been null. this is valid if the account is + // being credited, such as when we escrow gas under + // the Bridge pool + .unwrap_or_default(); let after: Amount = (&self.ctx) .read_post_value(&account_key) .unwrap_or_else(|error| { From d3a4906d8a54d3f2335bfe6bfbf1bd8c052853c2 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 09:22:55 +0100 Subject: [PATCH 031/103] Flow control of NAM transferred to Ethereum --- .../ethereum_bridge/bridge_pool_vp.rs | 223 ++++++++++++++---- tests/src/native_vp/eth_bridge_pool.rs | 8 +- 2 files changed, 186 insertions(+), 45 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 8585e9a79b..5248f54847 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -18,6 +18,7 @@ use namada_core::hints; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_pending_key, is_bridge_pool_key, BRIDGE_POOL_ADDRESS, }; +use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_ethereum_bridge::parameters::read_native_erc20_address; @@ -39,11 +40,32 @@ use crate::vm::WasmCacheAccess; pub struct Error(#[from] eyre::Error); /// A positive or negative amount +#[derive(Copy, Clone)] enum SignedAmount { Positive(Amount), Negative(Amount), } +/// An [`Amount`] that has been updated with some delta value. +#[derive(Copy, Clone)] +struct AmountDelta { + /// The base [`Amount`], before applying the delta. + base: Amount, + /// The delta to be applied to the base amount. + delta: SignedAmount, +} + +impl AmountDelta { + /// Resolve the updated amount by applying the delta value. + #[inline] + fn resolve(self) -> Amount { + match self.delta { + SignedAmount::Positive(delta) => self.base + delta, + SignedAmount::Negative(delta) => self.base - delta, + } + } +} + /// Validity predicate for the Ethereum bridge pub struct BridgePoolVp<'ctx, D, H, CA> where @@ -63,7 +85,7 @@ where { /// Get the change in the balance of an account /// associated with an address - fn account_balance_delta(&self, address: &Address) -> Option { + fn account_balance_delta(&self, address: &Address) -> Option { let account_key = balance_key(&self.ctx.storage.native_token, address); let before: Amount = (&self.ctx) .read_pre_value(&account_key) @@ -82,11 +104,14 @@ where tracing::warn!(?error, %account_key, "reading post value"); None })?; - if before > after { - Some(SignedAmount::Negative(before - after)) - } else { - Some(SignedAmount::Positive(after - before)) - } + Some(AmountDelta { + base: before, + delta: if before > after { + SignedAmount::Negative(before - after) + } else { + SignedAmount::Positive(after - before) + }, + }) } /// Check that the correct amount of erc20 assets were @@ -124,36 +149,74 @@ where /// Check that the correct amount of Nam was sent /// from the correct account into escrow + #[inline] fn check_nam_escrowed(&self, delta: EscrowDelta) -> Result { + self.check_nam_escrowed_balance(delta) + .map(|balance| balance.is_some()) + } + + /// Check that the correct amount of Nam was sent + /// from the correct account into escrow, and return + /// the updated escrow balance. + fn check_nam_escrowed_balance( + &self, + delta: EscrowDelta, + ) -> Result, Error> { let EscrowDelta { payer_account, escrow_account, expected_debit, expected_credit, } = delta; - let debited = self.account_balance_delta(payer_account); - let credited = self.account_balance_delta(escrow_account); + let debit = self.account_balance_delta(payer_account); + let credit = self.account_balance_delta(escrow_account); - match (debited, credited) { + match (debit, credit) { + // success case + ( + Some(AmountDelta { + delta: SignedAmount::Negative(debit), + .. + }), + Some( + escrow_balance @ AmountDelta { + delta: SignedAmount::Positive(credit), + .. + }, + ), + ) => Ok((debit == expected_debit && credit == expected_credit) + .then_some(escrow_balance)), + // user did not debit from their account ( - Some(SignedAmount::Negative(debit)), - Some(SignedAmount::Positive(credit)), - ) => Ok(debit == expected_debit && credit == expected_credit), - (Some(SignedAmount::Positive(_)), _) => { + Some(AmountDelta { + delta: SignedAmount::Positive(_), + .. + }), + _, + ) => { tracing::debug!( "The account {} was not debited.", payer_account ); - Ok(false) + Ok(None) } - (_, Some(SignedAmount::Negative(_))) => { + // user did not credit escrow account + ( + _, + Some(AmountDelta { + delta: SignedAmount::Negative(_), + .. + }), + ) => { tracing::debug!( "The Ethereum bridge pool's escrow was not credited from \ account {}.", payer_account ); - Ok(false) + Ok(None) } + // some other error occurred while calculating + // balance deltas (None, _) | (_, None) => Err(Error(eyre!( "Could not calculate the balance delta for {}", payer_account @@ -161,6 +224,74 @@ where } } + /// Validate a wrapped NAM transfer to Ethereum. + fn check_wnam_preconditions<'trans>( + &self, + &wnam_address: &EthAddress, + transfer: &'trans PendingTransfer, + escrow_checks: EscrowCheck<'trans>, + ) -> Result { + if hints::unlikely(matches!( + &transfer.transfer.kind, + TransferToEthereumKind::Nut + )) { + // NB: this should never be possible: protocol tx state updates + // never result in wNAM NUTs being minted. in turn, this means + // that users should never hold wNAM NUTs. doesn't hurt to add + // the extra check to the vp, though + tracing::error!( + "Attempted to add a wNAM NUT transfer to the Bridge pool" + ); + return Ok(false); + } + + let wnam_whitelisted = { + let key = whitelist::Key { + asset: wnam_address, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + (&self.ctx).read_pre_value(&key)?.unwrap_or(false) + }; + if !wnam_whitelisted { + tracing::debug!( + ?transfer, + "Wrapped NAM transfers are currently disabled" + ); + return Ok(false); + } + + // if we are going to mint wNam on Ethereum, the appropriate + // amount of Nam must be escrowed in the Ethereum bridge VP's + // storage. + let escrowed_balance = + match self.check_nam_escrowed_balance(escrow_checks.token_check)? { + Some(balance) => balance.resolve(), + None => return Ok(false), + }; + + let wnam_cap = { + let key = whitelist::Key { + asset: wnam_address, + suffix: whitelist::KeyType::Cap, + } + .into(); + (&self.ctx).read_pre_value(&key)?.unwrap_or_default() + }; + if escrowed_balance > wnam_cap { + tracing::debug!( + ?transfer, + escrowed_nam = %escrowed_balance.to_string_native(), + wnam_cap = %wnam_cap.to_string_native(), + "The balance of the escrow account exceeds the amount \ + of NAM that is allowed to cross the Ethereum bridge" + ); + return Ok(false); + } + + Ok(true) + } + /// Deteremine the debit and credit amounts that should be checked. fn escrow_check<'trans>( &self, @@ -226,6 +357,7 @@ where /// Helper struct for handling the different escrow /// checking scenarios. +#[derive(Copy, Clone)] struct EscrowDelta<'a> { payer_account: &'a Address, escrow_account: &'a Address, @@ -236,6 +368,7 @@ struct EscrowDelta<'a> { /// There are two checks we must do when minting wNam. /// 1. Check that gas fees were escrowed. /// 2. Check that the Nam to back wNam was escrowed. +#[derive(Copy, Clone)] struct EscrowCheck<'a> { gas_check: EscrowDelta<'a>, token_check: EscrowDelta<'a>, @@ -322,36 +455,23 @@ where } // check the escrowed assets if transfer.transfer.asset == wnam_address { - if hints::unlikely(matches!( - &transfer.transfer.kind, - TransferToEthereumKind::Nut - )) { - // NB: this should never be possible: protocol tx state updates - // never result in wNAM NUTs being minted. in turn, this means - // that users should never hold wNAM NUTs. doesn't hurt to add - // the extra check to the vp, though - tracing::error!( - "Attempted to add a wNAM NUT transfer to the Bridge pool" - ); - return Ok(false); - } - // if we are going to mint wNam on Ethereum, the appropriate - // amount of Nam must be escrowed in the Ethereum bridge VP's - // storage. - self.check_nam_escrowed(escrow_checks.token_check) - .map(|ok| { - if ok { - tracing::info!( - "The Ethereum bridge pool VP accepted the \ - transfer {:?}.", - transfer - ); - } - ok - }) + self.check_wnam_preconditions( + &wnam_address, + &transfer, + escrow_checks, + ) } else { self.check_erc20s_escrowed(keys_changed, &transfer) } + .map(|ok| { + if ok { + tracing::info!( + "The Ethereum bridge pool VP accepted the transfer {:?}.", + transfer + ); + } + ok + }) } } @@ -476,6 +596,23 @@ mod test_bridge_pool_vp { writelog .write(&get_pending_key(&transfer), transfer.try_to_vec().unwrap()) .expect("Test failed"); + // whitelist wnam + let key = whitelist::Key { + asset: wnam(), + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + writelog + .write(&key, true.try_to_vec().unwrap()) + .expect("Test failed"); + let key = whitelist::Key { + asset: wnam(), + suffix: whitelist::KeyType::Cap, + } + .into(); + writelog + .write(&key, Amount::max().try_to_vec().unwrap()) + .expect("Test failed"); // set up users with ERC20 and NUT balances update_balances( &mut writelog, diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index f972215c71..034ca60ab8 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -5,7 +5,8 @@ mod test_bridge_pool_vp { use borsh::{BorshDeserialize, BorshSerialize}; use namada::core::ledger::eth_bridge::storage::bridge_pool::BRIDGE_POOL_ADDRESS; use namada::ledger::eth_bridge::{ - wrapped_erc20s, Contracts, EthereumBridgeConfig, UpgradeableContract, + wrapped_erc20s, Contracts, Erc20WhitelistEntry, EthereumBridgeConfig, + UpgradeableContract, }; use namada::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; use namada::proto::Tx; @@ -63,7 +64,10 @@ mod test_bridge_pool_vp { ..Default::default() }; let config = EthereumBridgeConfig { - erc20_whitelist: vec![], + erc20_whitelist: vec![Erc20WhitelistEntry { + token_address: wnam(), + token_cap: Amount::max().native_denominated(), + }], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { From 99fe4770fe9069fa69fd46df8847676320e28701 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 10:30:17 +0100 Subject: [PATCH 032/103] Replace ERC20 supply RPC query with atomic read of all flow control data --- shared/src/ledger/queries/shell/eth_bridge.rs | 146 ++++++++++-------- 1 file changed, 80 insertions(+), 66 deletions(-) diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index f7c0731c70..8aa7dcfe4d 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; -use namada_core::ledger::eth_bridge::storage::wrapped_erc20s; +use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::merkle_tree::StoreRef; use namada_core::ledger::storage::{DBIter, StorageHasher, StoreType, DB}; use namada_core::ledger::storage_api::{ @@ -17,7 +17,7 @@ use namada_core::types::ethereum_events::{ }; use namada_core::types::ethereum_structs::RelayProof; use namada_core::types::storage::{BlockHeight, DbKeySeg, Key}; -use namada_core::types::token::{minted_balance_key, Amount}; +use namada_core::types::token::Amount; use namada_core::types::vote_extensions::validator_set_update::{ ValidatorSetArgs, VotingPowersMap, }; @@ -42,6 +42,20 @@ use crate::types::keccak::KeccakHash; use crate::types::storage::Epoch; use crate::types::storage::MembershipProof::BridgePool; +/// Contains information about the flow control of some ERC20 +/// wrapped asset. +#[derive( + Debug, Copy, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize, +)] +pub struct Erc20FlowControl { + /// Whether the wrapped asset is whitelisted. + whitelisted: bool, + /// Total minted supply of some wrapped asset. + supply: Amount, + /// The token cap of some wrapped asset. + cap: Amount, +} + pub type RelayProofBytes = Vec; router! {ETH_BRIDGE, @@ -104,31 +118,48 @@ router! {ETH_BRIDGE, ( "voting_powers" / "epoch" / [epoch: Epoch] ) -> VotingPowersMap = voting_powers_at_epoch, - // Read the total supply of some wrapped ERC20 token in Namada. - ( "erc20" / "supply" / [asset: EthAddress] ) - -> Option = read_erc20_supply, + // Read the total supply and respective cap of some wrapped + // ERC20 token in Namada. + ( "erc20" / "flow_control" / [asset: EthAddress] ) + -> Erc20FlowControl = get_erc20_flow_control, } -/// Read the total supply of some wrapped ERC20 token in Namada. -fn read_erc20_supply( +/// Read the total supply and respective cap of some wrapped +/// ERC20 token in Namada. +fn get_erc20_flow_control( ctx: RequestCtx<'_, D, H>, asset: EthAddress, -) -> storage_api::Result> +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - let Some(native_erc20) = ctx.wl_storage.read(&native_erc20_key())? else { - return Err(storage_api::Error::SimpleMessage( - "The Ethereum bridge storage is not initialized", - )); - }; - let token = if asset == native_erc20 { - ctx.wl_storage.storage.native_token.clone() - } else { - wrapped_erc20s::token(&asset) - }; - ctx.wl_storage.read(&minted_balance_key(&token)) + let key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + let whitelisted = ctx.wl_storage.read(&key)?.unwrap_or(false); + + let key = whitelist::Key { + asset, + suffix: whitelist::KeyType::WrappedSupply, + } + .into(); + let supply = ctx.wl_storage.read(&key)?.unwrap_or_default(); + + let key = whitelist::Key { + asset, + suffix: whitelist::KeyType::Cap, + } + .into(); + let cap = ctx.wl_storage.read(&key)?.unwrap_or_default(); + + Ok(Erc20FlowControl { + whitelisted, + supply, + cap, + }) } /// Helper function to read a smart contract from storage. @@ -577,7 +608,6 @@ mod test_ethbridge_router { use namada_core::types::voting_power::{ EthBridgeVotingPower, FractionalVotingPower, }; - use namada_ethereum_bridge::parameters::read_native_erc20_address; use namada_ethereum_bridge::protocol::transactions::validator_set_update::aggregate_votes; use namada_ethereum_bridge::storage::proof::BridgePoolRootProof; use namada_proof_of_stake::pos_queries::PosQueries; @@ -1372,45 +1402,9 @@ mod test_ethbridge_router { assert!(resp.is_err()); } - /// Test reading the wrapped NAM supply + /// Test reading the supply and cap of an ERC20 token. #[tokio::test] - async fn test_read_wnam_supply() { - let mut client = TestClient::new(RPC); - assert_eq!(client.wl_storage.storage.last_epoch.0, 0); - - // initialize storage - test_utils::init_default_storage(&mut client.wl_storage); - - let native_erc20 = - read_native_erc20_address(&client.wl_storage).expect("Test failed"); - - // write tokens to storage - let amount = Amount::native_whole(12345); - let token = &client.wl_storage.storage.native_token; - client - .wl_storage - .write(&minted_balance_key(token), amount) - .expect("Test failed"); - - // commit the changes - client - .wl_storage - .storage - .commit_block(MockDBWriteBatch) - .expect("Test failed"); - - // check that reading wrapped NAM fails - let result = RPC - .shell() - .eth_bridge() - .read_erc20_supply(&client, &native_erc20) - .await; - assert_matches!(result, Ok(Some(a)) if a == amount); - } - - /// Test reading the supply of an ERC20 token. - #[tokio::test] - async fn test_read_erc20_supply() { + async fn test_get_erc20_flow_control() { const ERC20_TOKEN: EthAddress = EthAddress([0; 20]); let mut client = TestClient::new(RPC); @@ -1419,29 +1413,49 @@ mod test_ethbridge_router { // initialize storage test_utils::init_default_storage(&mut client.wl_storage); - // check supply - should be None + // check supply - should be 0 let result = RPC .shell() .eth_bridge() - .read_erc20_supply(&client, &ERC20_TOKEN) + .get_erc20_flow_control(&client, &ERC20_TOKEN) .await; - assert_matches!(result, Ok(None)); + assert_matches!( + result, + Ok(f) if f.supply.is_zero() && f.cap.is_zero() + ); // write tokens to storage - let amount = Amount::native_whole(12345); - let token = wrapped_erc20s::token(&ERC20_TOKEN); + let supply_amount = Amount::native_whole(123); + let cap_amount = Amount::native_whole(12345); + let key = whitelist::Key { + asset: ERC20_TOKEN, + suffix: whitelist::KeyType::WrappedSupply, + } + .into(); client .wl_storage - .write(&minted_balance_key(&token), amount) + .write(&key, supply_amount) + .expect("Test failed"); + let key = whitelist::Key { + asset: ERC20_TOKEN, + suffix: whitelist::KeyType::Cap, + } + .into(); + client + .wl_storage + .write(&key, cap_amount) .expect("Test failed"); // check that the supply was updated let result = RPC .shell() .eth_bridge() - .read_erc20_supply(&client, &ERC20_TOKEN) + .get_erc20_flow_control(&client, &ERC20_TOKEN) .await; - assert_matches!(result, Ok(Some(a)) if a == amount); + assert_matches!( + result, + Ok(f) if f.supply == supply_amount && f.cap == cap_amount + ); } } From 2861e4680b61a6f9a5f8b85baad161fc22fe6911 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 10:36:24 +0100 Subject: [PATCH 033/103] Refactor get_erc20_flow_control to use EthBridgeQueries --- shared/src/ledger/queries/shell/eth_bridge.rs | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index 8aa7dcfe4d..44ad306789 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -5,7 +5,6 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; -use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::merkle_tree::StoreRef; use namada_core::ledger::storage::{DBIter, StorageHasher, StoreType, DB}; use namada_core::ledger::storage_api::{ @@ -134,26 +133,13 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - let key = whitelist::Key { - asset, - suffix: whitelist::KeyType::Whitelisted, - } - .into(); - let whitelisted = ctx.wl_storage.read(&key)?.unwrap_or(false); - - let key = whitelist::Key { - asset, - suffix: whitelist::KeyType::WrappedSupply, - } - .into(); - let supply = ctx.wl_storage.read(&key)?.unwrap_or_default(); + let ethbridge_queries = ctx.wl_storage.ethbridge_queries(); - let key = whitelist::Key { - asset, - suffix: whitelist::KeyType::Cap, - } - .into(); - let cap = ctx.wl_storage.read(&key)?.unwrap_or_default(); + let whitelisted = ethbridge_queries.is_token_whitelisted(&asset); + let supply = ethbridge_queries + .get_token_supply(&asset) + .unwrap_or_default(); + let cap = ethbridge_queries.get_token_cap(&asset).unwrap_or_default(); Ok(Erc20FlowControl { whitelisted, @@ -597,6 +583,7 @@ mod test_ethbridge_router { use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_pending_key, get_signed_root_key, BridgePoolTree, }; + use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::mockdb::MockDBWriteBatch; use namada_core::ledger::storage_api::StorageWrite; use namada_core::types::address::testing::established_address_1; From 14556d78d0d343d12264933af1df88a632b991bc Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 11:14:48 +0100 Subject: [PATCH 034/103] Add test_wnam_doesnt_mint_nuts() unit test --- .../transactions/ethereum_events/events.rs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 3c92d3dcab..3ee884c413 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -619,7 +619,7 @@ mod tests { use namada_core::ledger::storage::testing::TestWlStorage; use namada_core::ledger::storage::types::encode; use namada_core::types::address::testing::gen_implicit_address; - use namada_core::types::address::{gen_established_address, nam}; + use namada_core::types::address::{gen_established_address, nam, wnam}; use namada_core::types::eth_bridge_pool::GasFee; use namada_core::types::ethereum_events::testing::{ arbitrary_eth_address, arbitrary_keccak_hash, arbitrary_nonce, @@ -1458,4 +1458,30 @@ mod tests { assert_eq!(pre_escrowed_balance, post_escrowed_balance); }) } + + /// Test that the ledger appropriately panics when we try to mint + /// wrapped NAM NUTs. Under normal circumstances, this should never + /// happen. + #[test] + #[should_panic(expected = "Attempted to mint wNAM NUTs!")] + fn test_wnam_doesnt_mint_nuts() { + let mut wl_storage = TestWlStorage::default(); + test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + + let transfer = PendingTransfer { + transfer: eth_bridge_pool::TransferToEthereum { + asset: wnam(), + sender: address::testing::established_address_1(), + recipient: EthAddress([5; 20]), + amount: Amount::from(10), + kind: eth_bridge_pool::TransferToEthereumKind::Nut, + }, + gas_fee: GasFee { + amount: Amount::from(1), + payer: address::testing::established_address_1(), + }, + }; + + _ = update_transferred_asset_balances(&mut wl_storage, &transfer); + } } From 7efe3c2eaf4da88a300154bc9a15f48bebebd825 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 11:25:26 +0100 Subject: [PATCH 035/103] Fix test_wrapped_nam_not_burned() unit test --- .../transactions/ethereum_events/events.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 3ee884c413..d66d4adef8 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -1245,7 +1245,7 @@ mod tests { let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, [ - (native_erc20, eth_bridge_pool::TransferToEthereumKind::Nut), + (native_erc20, eth_bridge_pool::TransferToEthereumKind::Erc20), ( EthAddress([0xaa; 20]), eth_bridge_pool::TransferToEthereumKind::Erc20, @@ -1435,18 +1435,20 @@ mod tests { _ = act_on(wl_storage, event).unwrap(); - // check post supply + // check post supply - the wNAM minted supply should increase + // by the transferred amount assert!( wl_storage .read_bytes(&balance_key(&wnam, &BRIDGE_POOL_ADDRESS)) .expect("Test failed") .is_none() ); - assert!( + assert_eq!( wl_storage - .read_bytes(&minted_balance_key(&wnam)) - .expect("Test failed") - .is_none() + .read::(&minted_balance_key(&wnam)) + .expect("Reading from storage should not fail") + .expect("The wNAM supply should have been updated"), + Amount::from_u64(10), ); // check post balance From a714fb3559e56ac25f2a1d156914f049045380ab Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 13:16:31 +0100 Subject: [PATCH 036/103] Fix test_redeem_native_token() unit test --- .../transactions/ethereum_events/events.rs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index d66d4adef8..739e1d1ce1 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -1193,27 +1193,30 @@ mod tests { &wl_storage.storage.native_token, &BRIDGE_ADDRESS, ); + let bridge_pool_native_erc20_supply_key = + minted_balance_key(&wrapped_erc20s::token(&wnam())); StorageWrite::write( &mut wl_storage, &bridge_pool_native_token_balance_key, bridge_pool_initial_balance, )?; + StorageWrite::write( + &mut wl_storage, + &bridge_pool_native_erc20_supply_key, + amount, + )?; let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, &receiver); - let native_erc20 = read_native_erc20_address(&wl_storage)?; - let changed_keys = redeem_native_token( - &mut wl_storage, - &native_erc20, - &receiver, - &amount, - )?; + let changed_keys = + redeem_native_token(&mut wl_storage, &wnam(), &receiver, &amount)?; assert_eq!( changed_keys, BTreeSet::from([ bridge_pool_native_token_balance_key.clone(), - receiver_native_token_balance_key.clone() + receiver_native_token_balance_key.clone(), + bridge_pool_native_erc20_supply_key.clone(), ]) ); assert_eq!( @@ -1227,6 +1230,13 @@ mod tests { StorageRead::read(&wl_storage, &receiver_native_token_balance_key)?, Some(amount) ); + assert_eq!( + StorageRead::read( + &wl_storage, + &bridge_pool_native_erc20_supply_key + )?, + Some(Amount::zero()) + ); Ok(()) } From 0655085220cf1a187f363127e47d26d53e4fb929 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:26:36 +0100 Subject: [PATCH 037/103] Test that wrapped NAM is never minted --- .../transactions/ethereum_events/events.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 739e1d1ce1..8d50401b11 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -1188,6 +1188,16 @@ mod tests { let receiver = address::testing::established_address_1(); let amount = Amount::from(100); + // pre wNAM balance - 0 + let receiver_wnam_balance_key = + token::balance_key(&wrapped_erc20s::token(&wnam()), &receiver); + assert!( + wl_storage + .read_bytes(&receiver_wnam_balance_key) + .unwrap() + .is_none() + ); + let bridge_pool_initial_balance = Amount::from(100_000_000); let bridge_pool_native_token_balance_key = token::balance_key( &wl_storage.storage.native_token, @@ -1238,6 +1248,16 @@ mod tests { Some(Amount::zero()) ); + // post wNAM balance - 0 + // + // wNAM is never minted, it's converted back to NAM + assert!( + wl_storage + .read_bytes(&receiver_wnam_balance_key) + .unwrap() + .is_none() + ); + Ok(()) } From 7a537326e11ab66ad3febd8dcf25ad330b862631 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 13:43:26 +0100 Subject: [PATCH 038/103] Fix test_act_on_changes_storage_for_transfers_to_eth() unit test --- .../protocol/transactions/ethereum_events/events.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 8d50401b11..d6408cc7cf 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -642,10 +642,8 @@ mod tests { update_epoch_parameter(wl_storage, &epoch_duration) .expect("Test failed"); // set native ERC20 token - let native_erc20_key = bridge_storage::native_erc20_key(); - let native_erc20 = EthAddress([0; 20]); wl_storage - .write_bytes(&native_erc20_key, encode(&native_erc20)) + .write_bytes(&bridge_storage::native_erc20_key(), encode(&wnam())) .expect("Test failed"); } @@ -727,7 +725,7 @@ mod tests { .expect("Test failed"); for transfer in pending_transfers { - if transfer.transfer.asset == EthAddress([0; 20]) { + if transfer.transfer.asset == wnam() { // native ERC20 let sender_key = balance_key(&nam(), &transfer.transfer.sender); let sender_balance = Amount::from(0); @@ -1026,6 +1024,10 @@ mod tests { &BRIDGE_POOL_ADDRESS )) ); + assert!( + changed_keys + .remove(&minted_balance_key(&wrapped_erc20s::token(&wnam()))) + ); assert!(changed_keys.remove(&minted_balance_key(&random_erc20_token))); assert!( changed_keys.remove(&minted_balance_key(&random_erc20_token_2)) @@ -1146,7 +1148,7 @@ mod tests { // Check the balances for transfer in pending_transfers { - if transfer.transfer.asset == EthAddress([0; 20]) { + if transfer.transfer.asset == wnam() { let sender_key = balance_key(&nam(), &transfer.transfer.sender); let value = wl_storage.read_bytes(&sender_key).expect("Test failed"); From c0b0ed580d5dde8a8feda7fcf21f8c64fd534c6e Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 2 Aug 2023 15:20:39 +0100 Subject: [PATCH 039/103] Refactor Bridge pool tests to allow modifying pending transfer --- .../ethereum_bridge/bridge_pool_vp.rs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 5248f54847..df6a9b033b 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -762,14 +762,14 @@ mod test_bridge_pool_vp { insert_transfer: F, expect: Expect, ) where - F: FnOnce(PendingTransfer, &mut WriteLog) -> BTreeSet, + F: FnOnce(&mut PendingTransfer, &mut WriteLog) -> BTreeSet, { // setup let mut wl_storage = setup_storage(); let tx = Tx::from_type(TxType::Raw); // the transfer to be added to the pool - let transfer = PendingTransfer { + let mut transfer = PendingTransfer { transfer: TransferToEthereum { kind: TransferToEthereumKind::Erc20, asset: ASSET, @@ -784,7 +784,7 @@ mod test_bridge_pool_vp { }; // add transfer to pool let mut keys_changed = - insert_transfer(transfer.clone(), &mut wl_storage.write_log); + insert_transfer(&mut transfer, &mut wl_storage.write_log); // change Bertha's balances let mut new_keys_changed = update_balances( @@ -846,11 +846,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::True, ); @@ -867,11 +867,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -888,11 +888,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -909,11 +909,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -931,11 +931,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(10.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -952,11 +952,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(10.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -973,11 +973,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -994,11 +994,11 @@ mod test_bridge_pool_vp { SignedAmount::Negative(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -1013,7 +1013,7 @@ mod test_bridge_pool_vp { SignedAmount::Positive(GAS_FEE.into()), SignedAmount::Negative(TOKENS.into()), SignedAmount::Positive(TOKENS.into()), - |transfer, _| BTreeSet::from([get_pending_key(&transfer)]), + |transfer, _| BTreeSet::from([get_pending_key(transfer)]), Expect::Error, ); } @@ -1041,9 +1041,9 @@ mod test_bridge_pool_vp { payer: bertha_address(), }, }; - log.write(&get_pending_key(&transfer), t.try_to_vec().unwrap()) + log.write(&get_pending_key(transfer), t.try_to_vec().unwrap()) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -1074,7 +1074,7 @@ mod test_bridge_pool_vp { }; log.write(&get_pending_key(&t), transfer.try_to_vec().unwrap()) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::Error, ); @@ -1091,12 +1091,12 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); BTreeSet::from([ - get_pending_key(&transfer), + get_pending_key(transfer), get_signed_root_key(), ]) }, From d704d86e711622e72623d87a6761ec36e5310bee Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:35:58 +0100 Subject: [PATCH 040/103] Add `asset` field to Balance --- .../native_vp/ethereum_bridge/bridge_pool_vp.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index df6a9b033b..8f82d7ea81 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -517,6 +517,8 @@ mod test_bridge_pool_vp { /// A set of balances for an address struct Balance { + /// The address of the Ethereum asset. + asset: EthAddress, /// NUT or ERC20 Ethereum asset kind. kind: TransferToEthereumKind, /// The owner of the ERC20 assets. @@ -532,6 +534,7 @@ mod test_bridge_pool_vp { fn new(kind: TransferToEthereumKind, address: Address) -> Self { Self { kind, + asset: ASSET, owner: address, gas: 0.into(), token: 0.into(), @@ -654,8 +657,12 @@ mod test_bridge_pool_vp { // get the balance keys let token_key = balance_key( &match balance.kind { - TransferToEthereumKind::Erc20 => wrapped_erc20s::token(&ASSET), - TransferToEthereumKind::Nut => wrapped_erc20s::nut(&ASSET), + TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(&balance.asset) + } + TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(&balance.asset) + } }, &balance.owner, ); @@ -790,6 +797,7 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: transfer.transfer.asset, kind: TransferToEthereumKind::Erc20, owner: bertha_address(), gas: BERTHA_WEALTH.into(), @@ -804,6 +812,7 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: transfer.transfer.asset, kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, gas: ESCROWED_AMOUNT.into(), @@ -1131,6 +1140,7 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: ASSET, kind: TransferToEthereumKind::Erc20, owner: bertha_address(), gas: BERTHA_WEALTH.into(), @@ -1145,6 +1155,7 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: ASSET, kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, gas: ESCROWED_AMOUNT.into(), @@ -1567,6 +1578,7 @@ mod test_bridge_pool_vp { &mut wl_storage.write_log, Balance { kind, + asset: ASSET, owner: daewon_address(), gas: DAEWONS_GAS.into(), token: DAES_NUTS.into(), @@ -1581,6 +1593,7 @@ mod test_bridge_pool_vp { &mut wl_storage.write_log, Balance { kind, + asset: ASSET, owner: BRIDGE_POOL_ADDRESS, gas: ESCROWED_AMOUNT.into(), token: ESCROWED_NUTS.into(), From d90a5eac58371fe41215883e897b15c4e4dd163f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:36:24 +0100 Subject: [PATCH 041/103] New Bridge pool VP unit tests for wrapped NAM --- .../ethereum_bridge/bridge_pool_vp.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 8f82d7ea81..4b15d0050e 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -1639,4 +1639,48 @@ mod test_bridge_pool_vp { fn test_escrowing_nuts_happy_flow() { test_nut_aux(TransferToEthereumKind::Nut, Expect::True) } + + /// Test that the Bridge pool VP rejects a wNAM NUT transfer. + #[test] + fn test_bridge_pool_vp_rejects_wnam_nut() { + assert_bridge_pool( + SignedAmount::Negative(GAS_FEE.into()), + SignedAmount::Positive(GAS_FEE.into()), + SignedAmount::Negative(TOKENS.into()), + SignedAmount::Positive(TOKENS.into()), + |transfer, log| { + transfer.transfer.kind = TransferToEthereumKind::Nut; + transfer.transfer.asset = wnam(); + log.write( + &get_pending_key(transfer), + transfer.try_to_vec().unwrap(), + ) + .unwrap(); + BTreeSet::from([get_pending_key(transfer)]) + }, + Expect::False, + ); + } + + /// Test that the Bridge pool VP accepts a wNAM ERC20 transfer. + #[test] + fn test_bridge_pool_vp_accepts_wnam_erc20() { + assert_bridge_pool( + SignedAmount::Negative(GAS_FEE.into()), + SignedAmount::Positive(GAS_FEE.into()), + SignedAmount::Negative(TOKENS.into()), + SignedAmount::Positive(TOKENS.into()), + |transfer, log| { + transfer.transfer.kind = TransferToEthereumKind::Erc20; + transfer.transfer.asset = wnam(); + log.write( + &get_pending_key(transfer), + transfer.try_to_vec().unwrap(), + ) + .unwrap(); + BTreeSet::from([get_pending_key(transfer)]) + }, + Expect::True, + ); + } } From 75cf448ed91a3b73968245e19a3e665bb2549136 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:20:45 +0100 Subject: [PATCH 042/103] Fix wNAM edge cases in Bridge pool VP unit tests --- .../ethereum_bridge/bridge_pool_vp.rs | 125 ++++++++++++------ 1 file changed, 83 insertions(+), 42 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 4b15d0050e..056f41573b 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -27,7 +27,7 @@ use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::{DBIter, DB}; use crate::proto::Tx; -use crate::types::address::{Address, InternalAddress}; +use crate::types::address::Address; use crate::types::eth_bridge_pool::{PendingTransfer, TransferToEthereumKind}; use crate::types::ethereum_events::EthAddress; use crate::types::storage::Key; @@ -324,9 +324,7 @@ where }, token_check: EscrowDelta { payer_account: &transfer.transfer.sender, - escrow_account: &Address::Internal( - InternalAddress::EthBridge, - ), + escrow_account: &BRIDGE_ADDRESS, expected_debit: debit, expected_credit: transfer.transfer.amount, }, @@ -494,7 +492,7 @@ mod test_bridge_pool_vp { use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{Storage, WlStorage}; use crate::ledger::storage_api::StorageWrite; - use crate::types::address::{nam, wnam}; + use crate::types::address::{nam, wnam, InternalAddress}; use crate::types::chain::ChainId; use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; use crate::types::hash::Hash; @@ -642,6 +640,14 @@ mod test_bridge_pool_vp { SignedAmount::Positive(ESCROWED_AMOUNT.into()), SignedAmount::Positive(ESCROWED_NUTS.into()), ); + // set up the initial balances of the ethereum bridge account + update_balances( + &mut writelog, + Balance::new(TransferToEthereumKind::Erc20, BRIDGE_ADDRESS), + SignedAmount::Positive(ESCROWED_AMOUNT.into()), + // we only care about escrowing NAM + SignedAmount::Positive(0.into()), + ); writelog.commit_tx(); writelog } @@ -654,46 +660,79 @@ mod test_bridge_pool_vp { gas_delta: SignedAmount, token_delta: SignedAmount, ) -> BTreeSet { - // get the balance keys - let token_key = balance_key( - &match balance.kind { - TransferToEthereumKind::Erc20 => { - wrapped_erc20s::token(&balance.asset) - } - TransferToEthereumKind::Nut => { - wrapped_erc20s::nut(&balance.asset) - } - }, - &balance.owner, - ); - let account_key = balance_key(&nam(), &balance.owner); + // wnam is drawn from the same account + if balance.asset == wnam() + && !matches!(&balance.owner, Address::Internal(_)) + { + use SignedAmount::*; + + // update the balance of nam + let original_balance = std::cmp::max(balance.token, balance.gas); + let updated_balance = match (gas_delta, token_delta) { + (Negative(x), Negative(y)) => original_balance - x - y, + (Negative(x), Positive(y)) => original_balance - x + y, + (Positive(x), Negative(y)) => original_balance + x - y, + (Positive(x), Positive(y)) => original_balance + x + y, + }; - // update the balance of nam - let new_balance = match gas_delta { - SignedAmount::Positive(amount) => balance.gas + amount, - SignedAmount::Negative(amount) => balance.gas - amount, - } - .try_to_vec() - .expect("Test failed"); + // write the changes to the log + let account_key = balance_key(&nam(), &balance.owner); + write_log + .write( + &account_key, + updated_balance.try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); - // update the balance of tokens - let new_token_balance = match token_delta { - SignedAmount::Positive(amount) => balance.token + amount, - SignedAmount::Negative(amount) => balance.token - amount, - } - .try_to_vec() - .expect("Test failed"); + // changed keys + [account_key].into() + } else { + // get the balance keys + let token_key = if balance.asset == wnam() { + // the match above guards against non-internal addresses, + // so the only logical owner here is the Ethereum bridge + // address, where we escrow NAM to, when minting wNAM on + // Ethereum + assert_eq!(balance.owner, BRIDGE_POOL_ADDRESS); + balance_key(&nam(), &BRIDGE_ADDRESS) + } else { + balance_key( + &match balance.kind { + TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(&balance.asset) + } + TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(&balance.asset) + } + }, + &balance.owner, + ) + }; + let account_key = balance_key(&nam(), &balance.owner); - // write the changes to the log - write_log - .write(&account_key, new_balance) - .expect("Test failed"); - write_log - .write(&token_key, new_token_balance) - .expect("Test failed"); + // update the balance of nam + let new_gas_balance = match gas_delta { + SignedAmount::Positive(amount) => balance.gas + amount, + SignedAmount::Negative(amount) => balance.gas - amount, + }; - // return the keys changed - [account_key, token_key].into() + // update the balance of tokens + let new_token_balance = match token_delta { + SignedAmount::Positive(amount) => balance.token + amount, + SignedAmount::Negative(amount) => balance.token - amount, + }; + + // write the changes to the log + write_log + .write(&account_key, new_gas_balance.try_to_vec().unwrap()) + .expect("Test failed"); + write_log + .write(&token_key, new_token_balance.try_to_vec().unwrap()) + .expect("Test failed"); + + // return the keys changed + [account_key, token_key].into() + } } /// Initialize some dummy storage for testing @@ -1312,7 +1351,9 @@ mod test_bridge_pool_vp { .write_log .write( &eb_account_key, - Amount::from(100).try_to_vec().expect("Test failed"), + Amount::from(ESCROWED_AMOUNT + 100) + .try_to_vec() + .expect("Test failed"), ) .expect("Test failed"); From fd1bf4d89d81601fe2397e27e9695369f76561f2 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 17:09:42 +0100 Subject: [PATCH 043/103] Add invalidate_wnam_over_cap_tx() unit test --- tests/src/native_vp/eth_bridge_pool.rs | 36 ++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 034ca60ab8..956b2a1b8c 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -30,6 +30,7 @@ mod test_bridge_pool_vp { const BERTHA_TOKENS: u64 = 10_000; const GAS_FEE: u64 = 100; const TOKENS: u64 = 10; + const TOKEN_CAP: u64 = TOKENS; /// A signing keypair for good old Bertha. fn bertha_keypair() -> common::SecretKey { @@ -66,7 +67,7 @@ mod test_bridge_pool_vp { let config = EthereumBridgeConfig { erc20_whitelist: vec![Erc20WhitelistEntry { token_address: wnam(), - token_cap: Amount::max().native_denominated(), + token_cap: Amount::from_u64(TOKEN_CAP).native_denominated(), }], eth_start_height: Default::default(), min_confirmations: Default::default(), @@ -96,16 +97,23 @@ mod test_bridge_pool_vp { env } - fn validate_tx(tx: Tx) { + fn run_vp(tx: Tx) -> bool { let env = setup_env(tx); tx_host_env::set(env); let mut tx_env = tx_host_env::take(); tx_env.execute_tx().expect("Test failed."); let vp_env = TestNativeVpEnv::from_tx_env(tx_env, BRIDGE_POOL_ADDRESS); - let result = vp_env + vp_env .validate_tx(|ctx| BridgePoolVp { ctx }) - .expect("Test failed"); - assert!(result); + .expect("Test failed") + } + + fn validate_tx(tx: Tx) { + assert!(run_vp(tx)); + } + + fn invalidate_tx(tx: Tx) { + assert!(!run_vp(tx)); } fn create_tx(transfer: PendingTransfer, keypair: &common::SecretKey) -> Tx { @@ -156,6 +164,24 @@ mod test_bridge_pool_vp { validate_tx(create_tx(transfer, &bertha_keypair())); } + #[test] + fn invalidate_wnam_over_cap_tx() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKEN_CAP + 1), + }, + gas_fee: GasFee { + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + invalidate_tx(create_tx(transfer, &bertha_keypair())); + } + #[test] fn validate_mint_wnam_different_sender_tx() { let transfer = PendingTransfer { From 161cf43190809b28448d1566f8b69063b9e50445 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 13:46:03 +0100 Subject: [PATCH 044/103] Add changelog for #1781 --- .changelog/unreleased/features/1781-cap-wnam.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1781-cap-wnam.md diff --git a/.changelog/unreleased/features/1781-cap-wnam.md b/.changelog/unreleased/features/1781-cap-wnam.md new file mode 100644 index 0000000000..aeba012c6b --- /dev/null +++ b/.changelog/unreleased/features/1781-cap-wnam.md @@ -0,0 +1,2 @@ +- Control the flow of NAM over the Ethereum bridge + ([\#1781](https://github.com/anoma/namada/pull/1781)) \ No newline at end of file From b30a877460fc69d24303a0e744ef2322a882f013 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 16:12:00 +0100 Subject: [PATCH 045/103] Update ethbridge-rs to v0.22.0 --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 12 ++++++------ wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8131f58e9..18e78ed504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,8 +2021,8 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2032,8 +2032,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2043,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2054,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethers", diff --git a/Cargo.toml b/Cargo.toml index d524d0c7a2..7375bb01e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,12 +66,12 @@ directories = "4.0.1" ed25519-consensus = "1.2.0" escargot = "0.5.7" ethabi = "18.0.0" -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0"} -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.21.0" } +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0" } ethers = "2.0.0" expectrl = "0.7.0" eyre = "0.6.5" diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index a273155cd2..0f4f1f4a39 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 6d5a227eb4..6405e7100e 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.21.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" +version = "0.22.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" dependencies = [ "ethabi", "ethers", From 1a63f01d71bff78b6064c266f2f2bee7b6da80ba Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 3 Aug 2023 17:02:50 +0100 Subject: [PATCH 046/103] Fix compilation errors to support new `ethbridge-rs` --- .../lib/node/ledger/ethereum_oracle/events.rs | 80 ++++---- .../lib/node/ledger/ethereum_oracle/mod.rs | 15 +- .../lib/node/ledger/shell/finalize_block.rs | 31 +-- .../shell/vote_extensions/eth_events.rs | 39 +--- core/src/types/eth_bridge_pool.rs | 183 +++++++++++++++--- core/src/types/ethereum_events.rs | 91 +++------ .../transactions/ethereum_events/events.rs | 62 +++--- .../src/storage/eth_bridge_queries.rs | 32 ++- shared/src/ledger/eth_bridge/bridge_pool.rs | 66 ++++--- shared/src/ledger/queries/mod.rs | 3 + shared/src/ledger/queries/shell.rs | 2 +- shared/src/ledger/queries/shell/eth_bridge.rs | 156 ++++++++++----- 12 files changed, 456 insertions(+), 304 deletions(-) diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 645b87d1c4..3472fcb601 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -14,9 +14,9 @@ pub mod eth_events { use namada::eth_bridge::ethers::contract::EthEvent; use namada::types::address::Address; use namada::types::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, - TransferToNamada, Uint, + EthAddress, EthereumEvent, TransferToEthereum, TransferToNamada, Uint, }; + use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::token::Amount; use num256::Uint256; @@ -153,32 +153,33 @@ pub mod eth_events { }; } - /// Trait to add parsing methods to foreign types. - trait Parse: Sized { - parse_method! { parse_eth_transfer_kind -> TransferToEthereumKind } - parse_method! { parse_eth_address -> EthAddress } - parse_method! { parse_address -> Address } - parse_method! { parse_amount -> Amount } - parse_method! { parse_u32 -> u32 } - parse_method! { parse_uint256 -> Uint } - parse_method! { parse_bool -> bool } - parse_method! { parse_string -> String } - parse_method! { parse_keccak -> KeccakHash } - parse_method! { parse_amount_array -> Vec } - parse_method! { parse_eth_address_array -> Vec } - parse_method! { parse_address_array -> Vec
} - parse_method! { parse_string_array -> Vec } - parse_method! { parse_transfer_to_namada_array -> Vec } - parse_method! { parse_transfer_to_namada -> TransferToNamada } - parse_method! { parse_transfer_to_eth_array -> Vec } - parse_method! { parse_transfer_to_eth -> TransferToEthereum } + macro_rules! trait_parse_def { + ($($name:ident -> $type:ty;)*) => { + /// Trait to add parsing methods to foreign types. + trait Parse: Sized { + $( parse_method!($name -> $type); )* + } + } } - impl Parse for u8 { - fn parse_eth_transfer_kind(self) -> Result { - self.try_into() - .map_err(|err| Error::Decode(format!("{:?}", err))) - } + trait_parse_def! { + parse_address -> Address; + parse_address_array -> Vec
; + parse_amount -> Amount; + parse_amount_array -> Vec; + parse_bool -> bool; + parse_eth_address -> EthAddress; + parse_eth_address_array -> Vec; + parse_hash -> Hash; + parse_keccak -> KeccakHash; + parse_string -> String; + parse_string_array -> Vec; + parse_transfer_to_eth -> TransferToEthereum; + parse_transfer_to_eth_array -> Vec; + parse_transfer_to_namada -> TransferToNamada; + parse_transfer_to_namada_array -> Vec; + parse_u32 -> u32; + parse_uint256 -> Uint; } impl Parse for ethabi::Address { @@ -200,7 +201,13 @@ pub mod eth_events { impl Parse for ethabi::Uint { fn parse_amount(self) -> Result { - Ok(Amount::from(self.as_u64())) + let uint = { + use namada::core::types::uint::Uint as NamadaUint; + let mut num_buf = [0; 32]; + self.to_little_endian(&mut num_buf); + NamadaUint::from_little_endian(&num_buf) + }; + Amount::from_uint(uint, 0).map_err(|e| Error::Decode(e.to_string())) } fn parse_u32(self) -> Result { @@ -222,6 +229,10 @@ pub mod eth_events { fn parse_keccak(self) -> Result { Ok(KeccakHash(self)) } + + fn parse_hash(self) -> Result { + Ok(Hash(self)) + } } impl Parse for Vec { @@ -279,21 +290,15 @@ pub mod eth_events { impl Parse for ethereum_structs::Erc20Transfer { fn parse_transfer_to_eth(self) -> Result { - let kind = self.kind.parse_eth_transfer_kind()?; let asset = self.from.parse_eth_address()?; let receiver = self.to.parse_eth_address()?; - let sender = self.sender.parse_address()?; let amount = self.amount.parse_amount()?; - let gas_payer = self.fee_from.parse_address()?; - let gas_amount = self.fee.parse_amount()?; + let checksum = self.namada_data_digest.parse_hash()?; Ok(TransferToEthereum { - kind, asset, amount, - sender, receiver, - gas_amount, - gas_payer, + checksum, }) } } @@ -509,13 +514,10 @@ pub mod eth_events { let eth_transfers = TransferToErcFilter { transfers: vec![ ethereum_structs::Erc20Transfer { - kind: TransferToEthereumKind::Erc20 as u8, from: H160([1; 20]), to: H160([2; 20]), - sender: address.clone(), amount: 0u64.into(), - fee_from: address.clone(), - fee: 0u64.into(), + namada_data_digest: [0; 32], }; 2 ], diff --git a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index 05486aa11e..fc9ae9f0d1 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -569,9 +569,8 @@ mod test_oracle { use namada::eth_bridge::ethers::types::H160; use namada::eth_bridge::structs::Erc20Transfer; use namada::types::address::testing::gen_established_address; - use namada::types::ethereum_events::{ - EthAddress, TransferToEthereum, TransferToEthereumKind, - }; + use namada::types::ethereum_events::{EthAddress, TransferToEthereum}; + use namada::types::hash::Hash; use tokio::sync::oneshot::channel; use tokio::time::timeout; @@ -828,13 +827,10 @@ mod test_oracle { let gas_payer = gen_established_address(); let second_event = TransferToErcFilter { transfers: vec![Erc20Transfer { - kind: TransferToEthereumKind::Erc20 as u8, amount: 0.into(), from: H160([0; 20]), - sender: gas_payer.to_string(), to: H160([1; 20]), - fee: 0.into(), - fee_from: gas_payer.to_string(), + namada_data_digest: [0; 32], }], valid_map: vec![true], relayer_address: gas_payer.to_string(), @@ -898,13 +894,10 @@ mod test_oracle { assert_eq!( transfer, TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: Default::default(), asset: EthAddress([0; 20]), - sender: gas_payer.clone(), receiver: EthAddress([1; 20]), - gas_amount: Default::default(), - gas_payer: gas_payer.clone(), + checksum: Hash::default(), } ); } else { diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index f8fb4fb0a4..f79c5cf421 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1038,9 +1038,7 @@ mod test_finalize_block { }; use namada::proto::{Code, Data, Section, Signature}; use namada::types::dec::POS_DECIMAL_PRECISION; - use namada::types::ethereum_events::{ - EthAddress, TransferToEthereum, TransferToEthereumKind, Uint as ethUint, - }; + use namada::types::ethereum_events::{EthAddress, Uint as ethUint}; use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::key::tm_consensus_key_raw_hash; @@ -1695,17 +1693,24 @@ mod test_finalize_block { } // write transfer to storage let transfer = { - use namada::core::types::eth_bridge_pool::PendingTransfer; - let transfer = TransferToEthereum { - kind: TransferToEthereumKind::Erc20, - amount: 10u64.into(), - asset, - receiver, - gas_amount: 10u64.into(), - sender: bertha.clone(), - gas_payer: bertha.clone(), + use namada::core::types::eth_bridge_pool::{ + GasFee, PendingTransfer, TransferToEthereum, + TransferToEthereumKind, + }; + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + amount: 10u64.into(), + asset, + recipient: receiver, + sender: bertha.clone(), + }, + gas_fee: GasFee { + amount: 10u64.into(), + payer: bertha.clone(), + }, }; - let pending = PendingTransfer::from(&transfer); + let transfer = (&pending).into(); shell .wl_storage .write(&bridge_pool::get_pending_key(&pending), pending) diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 5282864e6d..cec4158940 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -461,9 +461,9 @@ mod test_vote_extensions { #[cfg(feature = "abcipp")] use namada::types::eth_abi::Encode; use namada::types::ethereum_events::{ - EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, - Uint, + EthAddress, EthereumEvent, TransferToEthereum, Uint, }; + use namada::types::hash::Hash; #[cfg(feature = "abcipp")] use namada::types::keccak::keccak_hash; #[cfg(feature = "abcipp")] @@ -594,13 +594,10 @@ mod test_vote_extensions { let event_1 = EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -608,13 +605,10 @@ mod test_vote_extensions { let event_2 = EthereumEvent::TransfersToEthereum { nonce: 1.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -660,13 +654,10 @@ mod test_vote_extensions { let event_1 = EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -723,13 +714,10 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -818,13 +806,10 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -896,13 +881,10 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -979,13 +961,10 @@ mod test_vote_extensions { ethereum_events: vec![EthereumEvent::TransfersToEthereum { nonce: 0.into(), transfers: vec![TransferToEthereum { - kind: TransferToEthereumKind::Erc20, amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index c7394af167..4f12eec619 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -1,6 +1,8 @@ //! The necessary type definitions for the contents of the //! Ethereum bridge pool +use std::borrow::Cow; + use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use ethabi::token::Token; use serde::{Deserialize, Serialize}; @@ -8,16 +10,108 @@ use serde::{Deserialize, Serialize}; use crate::ledger::eth_bridge::storage::wrapped_erc20s; use crate::types::address::Address; use crate::types::eth_abi::Encode; -pub use crate::types::ethereum_events::TransferToEthereumKind; use crate::types::ethereum_events::{ EthAddress, TransferToEthereum as TransferToEthereumEvent, }; +use crate::types::hash::Hash as HashDigest; use crate::types::storage::{DbKeySeg, Key}; use crate::types::token::Amount; +/// A version used in our Ethereuem smart contracts +const VERSION: u8 = 1; + /// A namespace used in our Ethereuem smart contracts const NAMESPACE: &str = "transfer"; +/// Transfer to Ethereum kinds. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +pub enum TransferToEthereumKind { + /// Transfer ERC20 assets from Namada to Ethereum. + /// + /// These transfers burn wrapped ERC20 assets in Namada, once + /// they have been confirmed. + Erc20, + /// Refund non-usable tokens. + /// + /// These Bridge pool transfers should be crafted for assets + /// that have been transferred to Namada, that had either not + /// been whitelisted or whose token caps had been exceeded in + /// Namada at the time of the transfer. + Nut, +} + +/// Additional data appended to a [`TransferToEthereumEvent`] to +/// construct a [`PendingTransfer`]. +#[derive( + Debug, + Clone, + Hash, + PartialOrd, + PartialEq, + Ord, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +pub struct PendingTransferAppendix<'transfer> { + /// The kind of the pending transfer to Ethereum. + pub kind: Cow<'transfer, TransferToEthereumKind>, + /// The sender of the transfer. + pub sender: Cow<'transfer, Address>, + /// The amount of gas fees (in NAM) + /// paid by the user sending this transfer + pub gas_fee: Cow<'transfer, GasFee>, +} + +impl From for PendingTransferAppendix<'static> { + #[inline] + fn from(pending: PendingTransfer) -> Self { + Self { + kind: Cow::Owned(pending.transfer.kind), + sender: Cow::Owned(pending.transfer.sender), + gas_fee: Cow::Owned(pending.gas_fee), + } + } +} + +impl<'t> From<&'t PendingTransfer> for PendingTransferAppendix<'t> { + #[inline] + fn from(pending: &'t PendingTransfer) -> Self { + Self { + kind: Cow::Borrowed(&pending.transfer.kind), + sender: Cow::Borrowed(&pending.transfer.sender), + gas_fee: Cow::Borrowed(&pending.gas_fee), + } + } +} + +impl<'transfer> PendingTransferAppendix<'transfer> { + /// Calculate the checksum of this [`PendingTransferAppendix`]. + pub fn checksum(&self) -> HashDigest { + let serialized = self + .try_to_vec() + .expect("Serializing a PendingTransferAppendix should not fail"); + HashDigest::sha256(serialized) + } +} + /// A transfer message to be submitted to Ethereum /// to move assets from Namada across the bridge. #[derive( @@ -84,51 +178,84 @@ impl PendingTransfer { } } } + + /// Retrieve a reference to the appendix of this [`PendingTransfer`]. + #[inline] + pub fn appendix(&self) -> PendingTransferAppendix<'_> { + self.into() + } + + /// Retrieve the owned appendix of this [`PendingTransfer`]. + #[inline] + pub fn into_appendix(self) -> PendingTransferAppendix<'static> { + self.into() + } + + /// Craft a [`PendingTransfer`] from its constituents. + pub fn from_parts( + event: &TransferToEthereumEvent, + appendix: PendingTransferAppendix<'_>, + ) -> Self { + let transfer = TransferToEthereum { + kind: *appendix.kind, + asset: event.asset, + recipient: event.receiver, + sender: (*appendix.sender).clone(), + amount: event.amount, + }; + let gas_fee = (*appendix.gas_fee).clone(); + Self { transfer, gas_fee } + } } -impl From for ethbridge_structs::Erc20Transfer { - fn from(pending: PendingTransfer) -> Self { +impl From<&PendingTransfer> for ethbridge_structs::Erc20Transfer { + fn from(pending: &PendingTransfer) -> Self { + let HashDigest(namada_data_digest) = pending.appendix().checksum(); Self { - kind: pending.transfer.kind as u8, from: pending.transfer.asset.0.into(), to: pending.transfer.recipient.0.into(), amount: pending.transfer.amount.into(), - fee_from: pending.gas_fee.payer.to_string(), - fee: pending.gas_fee.amount.into(), - sender: pending.transfer.sender.to_string(), + namada_data_digest, + } + } +} + +impl From<&PendingTransfer> for TransferToEthereumEvent { + fn from(pending: &PendingTransfer) -> Self { + Self { + amount: pending.transfer.amount, + asset: pending.transfer.asset, + receiver: pending.transfer.recipient, + checksum: pending.appendix().checksum(), } } } -impl Encode<8> for PendingTransfer { - fn tokenize(&self) -> [Token; 8] { +impl Encode<6> for PendingTransfer { + fn tokenize(&self) -> [Token; 6] { // TODO: This version should be looked up from storage - let version = Token::Uint(1.into()); + let version = Token::Uint(VERSION.into()); let namespace = Token::String(NAMESPACE.into()); let from = Token::Address(self.transfer.asset.0.into()); - let fee = Token::Uint(self.gas_fee.amount.into()); let to = Token::Address(self.transfer.recipient.0.into()); let amount = Token::Uint(self.transfer.amount.into()); - let fee_from = Token::String(self.gas_fee.payer.to_string()); - let sender = Token::String(self.transfer.sender.to_string()); - [version, namespace, from, to, amount, fee_from, fee, sender] + let checksum = Token::FixedBytes(self.appendix().checksum().0.into()); + [version, namespace, from, to, amount, checksum] } } -impl From<&TransferToEthereumEvent> for PendingTransfer { - fn from(event: &TransferToEthereumEvent) -> Self { - let transfer = TransferToEthereum { - kind: event.kind, - asset: event.asset, - recipient: event.receiver, - sender: event.sender.clone(), - amount: event.amount, - }; - let gas_fee = GasFee { - amount: event.gas_amount, - payer: event.gas_payer.clone(), - }; - Self { transfer, gas_fee } +// TODO: test that encode for `PendingTransfer` and +// `TransferToEthereumEvent` yield the same keccak hash +impl Encode<6> for TransferToEthereumEvent { + fn tokenize(&self) -> [Token; 6] { + // TODO: This version should be looked up from storage + let version = Token::Uint(VERSION.into()); + let namespace = Token::String(NAMESPACE.into()); + let from = Token::Address(self.asset.0.into()); + let to = Token::Address(self.receiver.0.into()); + let amount = Token::Uint(self.amount.into()); + let checksum = Token::FixedBytes(self.checksum.0.into()); + [version, namespace, from, to, amount, checksum] } } diff --git a/core/src/types/ethereum_events.rs b/core/src/types/ethereum_events.rs index b896674a0c..f7097e9749 100644 --- a/core/src/types/ethereum_events.rs +++ b/core/src/types/ethereum_events.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; use crate::types::address::Address; use crate::types::eth_abi::Encode; +use crate::types::ethereum_structs::Erc20Transfer; use crate::types::hash::Hash; use crate::types::keccak::KeccakHash; use crate::types::storage::{DbKeySeg, KeySeg}; @@ -366,60 +367,6 @@ pub struct TransferToNamada { pub receiver: Address, } -/// Transfer to Ethereum kinds. -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - BorshSerialize, - BorshDeserialize, - BorshSchema, - Serialize, - Deserialize, -)] -#[repr(u8)] -pub enum TransferToEthereumKind { - /// Transfer ERC20 assets from Namada to Ethereum. - /// - /// These transfers burn wrapped ERC20 assets in Namada, once - /// they have been confirmed. - Erc20 = Self::KIND_ERC20, - /// Refund non-usable tokens. - /// - /// These Bridge pool transfers should be crafted for assets - /// that have been transferred to Namada, that had either not - /// been whitelisted or whose token caps had been exceeded in - /// Namada at the time of the transfer. - Nut = Self::KIND_NUT, -} - -// XXX: keep these values in sync with the smart contracts -impl TransferToEthereumKind { - const KIND_ERC20: u8 = 0; - const KIND_NUT: u8 = 1; -} - -impl TryFrom for TransferToEthereumKind { - type Error = eyre::Error; - - fn try_from(kind: u8) -> Result { - match kind { - Self::KIND_ERC20 => Ok(Self::Erc20), - Self::KIND_NUT => Ok(Self::Nut), - _ => Err(eyre!( - "Only valid kinds are {} (ERC20) and {} (NUT)", - Self::KIND_ERC20, - Self::KIND_NUT - )), - } - } -} - /// An event transferring some kind of value from Namada to Ethereum #[derive( Clone, @@ -436,20 +383,40 @@ impl TryFrom for TransferToEthereumKind { Deserialize, )] pub struct TransferToEthereum { - /// The kind of transfer to Ethereum. - pub kind: TransferToEthereumKind, /// Quantity of wrapped Asset in the transfer pub amount: Amount, /// Address of the smart contract issuing the token pub asset: EthAddress, /// The address receiving assets on Ethereum pub receiver: EthAddress, - /// The amount of fees (in NAM) - pub gas_amount: Amount, - /// The address sending assets to Ethereum. - pub sender: Address, - /// The account of fee payer. - pub gas_payer: Address, + /// Checksum of all Namada specific fields, including, + /// but not limited to, whether it is a NUT transfer, + /// the address of the sender, etc + /// + /// It serves to uniquely identify an event stored under + /// the Bridge pool, in Namada + pub checksum: Hash, +} + +impl From for TransferToEthereum { + #[inline] + fn from(transfer: Erc20Transfer) -> Self { + Self { + amount: { + let uint = { + use crate::types::uint::Uint as NamadaUint; + let mut num_buf = [0; 32]; + transfer.amount.to_little_endian(&mut num_buf); + NamadaUint::from_little_endian(&num_buf) + }; + // this is infallible for a denom of 0 + Amount::from_uint(uint, 0).unwrap() + }, + asset: EthAddress(transfer.from.0), + receiver: EthAddress(transfer.to.0), + checksum: Hash(transfer.namada_data_digest), + } + } } #[cfg(test)] diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index d6408cc7cf..65c8f832ca 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -7,8 +7,7 @@ use borsh::BorshDeserialize; use eyre::{Result, WrapErr}; use namada_core::hints; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ - get_nonce_key, get_pending_key, is_pending_transfer_key, - BRIDGE_POOL_ADDRESS, + get_nonce_key, is_pending_transfer_key, BRIDGE_POOL_ADDRESS, }; use namada_core::ledger::eth_bridge::storage::{ self as bridge_storage, wrapped_erc20s, @@ -370,11 +369,14 @@ where for (event, is_valid) in transfers.iter().zip(valid_transfers.iter().copied()) { - let pending_transfer = event.into(); - let key = get_pending_key(&pending_transfer); - if hints::unlikely(!wl_storage.has_key(&key)?) { + let (pending_transfer, key) = if let Some((pending, key)) = + wl_storage.ethbridge_queries().lookup_transfer_to_eth(event) + { + (pending, key) + } else { + hints::cold(); unreachable!("The transfer should exist in the bridge pool"); - } + }; if hints::likely(is_valid) { tracing::debug!( ?pending_transfer, @@ -612,6 +614,7 @@ mod tests { use assert_matches::assert_matches; use borsh::BorshSerialize; use eyre::Result; + use namada_core::ledger::eth_bridge::storage::bridge_pool::get_pending_key; use namada_core::ledger::parameters::{ update_epoch_parameter, EpochDuration, }; @@ -982,19 +985,10 @@ mod tests { let pending_keys: HashSet = pending_transfers.iter().map(get_pending_key).collect(); let relayer = gen_established_address("random"); - let mut transfers = vec![]; - for transfer in pending_transfers { - let transfer_to_eth = TransferToEthereum { - kind: transfer.transfer.kind, - amount: transfer.transfer.amount, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - gas_amount: transfer.gas_fee.amount, - gas_payer: transfer.gas_fee.payer, - sender: transfer.transfer.sender, - }; - transfers.push(transfer_to_eth); - } + let transfers: Vec<_> = pending_transfers + .iter() + .map(TransferToEthereum::from) + .collect(); let event = EthereumEvent::TransfersToEthereum { nonce: arbitrary_nonce(), valid_transfers_map: transfers.iter().map(|_| true).collect(), @@ -1307,16 +1301,8 @@ mod tests { init_balance(&mut wl_storage, &pending_transfers); let (transfers, valid_transfers_map) = pending_transfers .into_iter() - .map(|transfer| { - let transfer_to_eth = TransferToEthereum { - kind: transfer.transfer.kind, - amount: transfer.transfer.amount, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - gas_amount: transfer.gas_fee.amount, - gas_payer: transfer.gas_fee.payer, - sender: transfer.transfer.sender, - }; + .map(|ref transfer| { + let transfer_to_eth: TransferToEthereum = transfer.into(); (transfer_to_eth, true) }) .unzip(); @@ -1353,16 +1339,18 @@ mod tests { read_native_erc20_address(wl_storage).expect("Test failed"); let deltas = transfers .filter_map( - |TransferToEthereum { - kind, - asset, - amount, - .. - }| { + |event @ TransferToEthereum { asset, amount, .. }| { if asset == &native_erc20 { return None; } - let erc20_token = match kind { + let kind = { + let (pending, _) = wl_storage + .ethbridge_queries() + .lookup_transfer_to_eth(event) + .expect("Test failed"); + pending.transfer.kind + }; + let erc20_token = match &kind { eth_bridge_pool::TransferToEthereumKind::Erc20 => { wrapped_erc20s::token(asset) } @@ -1380,7 +1368,7 @@ mod tests { .read(&minted_balance_key(&erc20_token)) .expect("Test failed"); Some(Delta { - kind: *kind, + kind, asset: *asset, sent_amount: *amount, prev_balance, diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 08fdcab2fa..c30de9a536 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -1,16 +1,19 @@ use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::hints; -use namada_core::ledger::eth_bridge::storage::bridge_pool::{ - get_nonce_key, get_signed_root_key, +use namada_core::ledger::eth_bridge::storage::{ + active_key, bridge_pool, whitelist, }; -use namada_core::ledger::eth_bridge::storage::{active_key, whitelist}; use namada_core::ledger::storage; use namada_core::ledger::storage::{StoreType, WlStorage}; use namada_core::ledger::storage_api::StorageRead; use namada_core::types::address::Address; -use namada_core::types::ethereum_events::{EthAddress, GetEventNonce, Uint}; +use namada_core::types::eth_abi::Encode; +use namada_core::types::eth_bridge_pool::PendingTransfer; +use namada_core::types::ethereum_events::{ + EthAddress, GetEventNonce, TransferToEthereum, Uint, +}; use namada_core::types::keccak::KeccakHash; -use namada_core::types::storage::{BlockHeight, Epoch}; +use namada_core::types::storage::{BlockHeight, Epoch, Key as StorageKey}; use namada_core::types::token; use namada_core::types::vote_extensions::validator_set_update::{ EthAddrBook, ValidatorSetArgs, VotingPowersMap, VotingPowersMapExt, @@ -175,7 +178,7 @@ where &self .wl_storage .storage - .read(&get_nonce_key()) + .read(&bridge_pool::get_nonce_key()) .expect("Reading Bridge pool nonce shouldn't fail.") .0 .expect("Reading Bridge pool nonce shouldn't fail."), @@ -191,7 +194,7 @@ where .storage .db .read_subspace_val_with_height( - &get_nonce_key(), + &bridge_pool::get_nonce_key(), height, self.wl_storage.storage.get_last_block_height(), ) @@ -225,7 +228,7 @@ where self, ) -> Option<(BridgePoolRootProof, BlockHeight)> { self.wl_storage - .read_bytes(&get_signed_root_key()) + .read_bytes(&bridge_pool::get_signed_root_key()) .expect("Reading signed Bridge pool root shouldn't fail.") .map(|bytes| { BorshDeserialize::try_from_slice(&bytes).expect( @@ -488,6 +491,19 @@ where nut_amount: token::Amount::zero(), } } + + /// Given a [`TransferToEthereum`] event, look-up the corresponding + /// [`PendingTransfer`]. + pub fn lookup_transfer_to_eth( + self, + transfer: &TransferToEthereum, + ) -> Option<(PendingTransfer, StorageKey)> { + let pending_key = bridge_pool::get_key_from_hash(&transfer.keccak256()); + self.wl_storage + .read(&pending_key) + .expect("Reading from storage should not fail") + .zip(Some(pending_key)) + } } /// Number of tokens to mint after receiving a "transfer diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 940524d34f..9576b70baf 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -1,5 +1,6 @@ //! Bridge pool SDK functionality. +use std::borrow::Cow; use std::cmp::Ordering; use std::collections::HashMap; use std::io::Write; @@ -17,7 +18,9 @@ use super::{block_on_eth_sync, eth_sync_or_exit, BlockOnEthSync}; use crate::eth_bridge::ethers::abi::AbiDecode; use crate::eth_bridge::structs::RelayProof; use crate::ledger::args; -use crate::ledger::queries::{Client, RPC}; +use crate::ledger::queries::{ + Client, GenBridgePoolProofReq, GenBridgePoolProofRsp, RPC, +}; use crate::ledger::rpc::{query_wasm_code_hash, validate_amount}; use crate::ledger::tx::{prepare_tx, Error}; use crate::proto::Tx; @@ -179,9 +182,8 @@ where /// bridge pool. async fn construct_bridge_pool_proof( client: &C, - transfers: &[KeccakHash], - relayer: Address, -) -> Halt> + args: GenBridgePoolProofReq<'_, '_>, +) -> Halt where C: Client + Sync, { @@ -196,8 +198,8 @@ where .into_iter() .filter_map(|(ref transfer, voting_power)| { if voting_power > FractionalVotingPower::ONE_THIRD { - let hash = PendingTransfer::from(transfer).keccak256(); - transfers.contains(&hash).then_some(hash) + let hash = transfer.keccak256(); + args.transfers.contains(&hash).then_some(hash) } else { None } @@ -234,7 +236,7 @@ where } } - let data = (transfers, relayer).try_to_vec().unwrap(); + let data = args.try_to_vec().unwrap(); let response = RPC .shell() .eth_bridge() @@ -242,7 +244,7 @@ where .await; response.map(|response| response.data).try_halt(|e| { - println!("Encountered error constructing proof:\n{:?}", e); + println!("Encountered error constructing proof:\n{e}"); }) } @@ -265,25 +267,29 @@ pub async fn construct_proof( where C: Client + Sync, { - let bp_proof_bytes = construct_bridge_pool_proof( + let GenBridgePoolProofRsp { + abi_encoded_proof: bp_proof_bytes, + appendices, + } = construct_bridge_pool_proof( client, - &args.transfers, - args.relayer.clone(), + GenBridgePoolProofReq { + transfers: args.transfers.as_slice().into(), + relayer: Cow::Borrowed(&args.relayer), + with_appendix: true, + }, ) .await?; - let bp_proof: RelayProof = - AbiDecode::decode(&bp_proof_bytes).try_halt(|error| { - println!("Unable to decode the generated proof: {:?}", error); - })?; let resp = BridgePoolProofResponse { hashes: args.transfers, relayer_address: args.relayer, - total_fees: bp_proof - .transfers - .iter() - .map(|t| t.fee.as_u64()) - .sum::() - .into(), + total_fees: appendices + .map(|appendices| { + appendices + .into_iter() + .map(|app| app.gas_fee.amount) + .sum::() + }) + .unwrap_or(Amount::zero()), abi_encoded_proof: bp_proof_bytes, }; println!("{}", serde_json::to_string(&resp).unwrap()); @@ -316,9 +322,18 @@ where eth_sync_or_exit(&*eth_client).await?; } - let bp_proof = - construct_bridge_pool_proof(nam_client, &args.transfers, args.relayer) - .await?; + let GenBridgePoolProofRsp { + abi_encoded_proof: bp_proof, + .. + } = construct_bridge_pool_proof( + nam_client, + GenBridgePoolProofReq { + transfers: Cow::Owned(args.transfers), + relayer: Cow::Owned(args.relayer), + with_appendix: false, + }, + ) + .await?; let bridge = match RPC .shell() .eth_bridge() @@ -468,8 +483,7 @@ mod recommendations { .transfer_to_ethereum_progress(client) .await .unwrap() - .keys() - .map(PendingTransfer::from) + .into_keys() .collect::>(); // get the signed bridge pool root so we can analyze the signatures diff --git a/shared/src/ledger/queries/mod.rs b/shared/src/ledger/queries/mod.rs index ce689e6325..bcd9fc8c27 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -12,6 +12,9 @@ pub use types::{ }; use vp::{Vp, VP}; +pub use self::shell::eth_bridge::{ + Erc20FlowControl, GenBridgePoolProofReq, GenBridgePoolProofRsp, +}; use super::storage::traits::StorageHasher; use super::storage::{DBIter, DB}; use super::storage_api; diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 94412f1a15..b1e243f74b 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -1,4 +1,4 @@ -mod eth_bridge; +pub(super) mod eth_bridge; use borsh::{BorshDeserialize, BorshSerialize}; use masp_primitives::asset_type::AssetType; diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index 44ad306789..0dbd76b376 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -1,5 +1,6 @@ //! Ethereum bridge related shell queries. +use std::borrow::Cow; use std::collections::HashMap; use std::str::FromStr; @@ -11,6 +12,7 @@ use namada_core::ledger::storage_api::{ self, CustomError, ResultExt, StorageRead, }; use namada_core::types::address::Address; +use namada_core::types::eth_bridge_pool::PendingTransferAppendix; use namada_core::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, }; @@ -34,6 +36,7 @@ use namada_ethereum_bridge::storage::{ }; use namada_proof_of_stake::pos_queries::PosQueries; +use crate::eth_bridge::ethers::abi::AbiDecode; use crate::ledger::queries::{EncodedResponseQuery, RequestCtx, RequestQuery}; use crate::types::eth_abi::{Encode, EncodeCell}; use crate::types::eth_bridge_pool::PendingTransfer; @@ -55,7 +58,39 @@ pub struct Erc20FlowControl { cap: Amount, } -pub type RelayProofBytes = Vec; +/// Request data to pass to `generate_bridge_pool_proof`. +#[derive(Debug, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct GenBridgePoolProofReq<'transfers, 'relayer> { + /// The hashes of the transfers to be relayed. + pub transfers: Cow<'transfers, [KeccakHash]>, + /// The address of the relayer to compensate. + pub relayer: Cow<'relayer, Address>, + /// Whether to return the appendix of a [`PendingTransfer`]. + pub with_appendix: bool, +} + +/// Response data returned by `generate_bridge_pool_proof`. +#[derive(Debug, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct GenBridgePoolProofRsp { + /// Ethereum ABI encoded [`RelayProof`]. + pub abi_encoded_proof: Vec, + /// Appendix data of all requested pending transfers. + pub appendices: Option>>, +} + +impl GenBridgePoolProofRsp { + /// Retrieve all [`PendingTransfer`] instances returned from the RPC server. + pub fn pending_transfers(self) -> impl Iterator { + RelayProof::decode(&self.abi_encoded_proof) + .into_iter() + .flat_map(|proof| proof.transfers) + .zip(self.appendices.into_iter().flatten()) + .map(|(event, appendix)| { + let event: TransferToEthereum = event.into(); + PendingTransfer::from_parts(&event, appendix) + }) + } +} router! {ETH_BRIDGE, // Get the current contents of the Ethereum bridge pool @@ -70,12 +105,12 @@ router! {ETH_BRIDGE, // Generate a merkle proof for the inclusion of requested // transfers in the Ethereum bridge pool ( "pool" / "proof" ) - -> RelayProofBytes = (with_options generate_bridge_pool_proof), + -> GenBridgePoolProofRsp = (with_options generate_bridge_pool_proof), // Iterates over all ethereum events and returns the amount of // voting power backing each `TransferToEthereum` event. ( "pool" / "transfer_to_eth_progress" ) - -> HashMap + -> HashMap = transfer_to_ethereum_progress, // Request a proof of a validator set signed off for @@ -290,8 +325,11 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - if let Ok((transfer_hashes, relayer)) = - <(Vec, Address)>::try_from_slice(request.data.as_slice()) + if let Ok(GenBridgePoolProofReq { + transfers: transfer_hashes, + relayer, + with_appendix, + }) = BorshDeserialize::try_from_slice(request.data.as_slice()) { // get the latest signed merkle root of the Ethereum bridge pool let (signed_root, height) = ctx @@ -335,14 +373,19 @@ where .into(), ))); } - let transfers = values - .iter() - .map(|bytes| { - PendingTransfer::try_from_slice(bytes) - .expect("Deserializing storage shouldn't fail") - .into() - }) - .collect(); + let (transfers, appendices) = values.iter().fold( + (vec![], vec![]), + |(mut transfers, mut appendices), bytes| { + let pending = PendingTransfer::try_from_slice(bytes) + .expect("Deserializing storage shouldn't fail"); + let eth_transfer = (&pending).into(); + if with_appendix { + appendices.push(pending.into_appendix()); + } + transfers.push(eth_transfer); + (transfers, appendices) + }, + ); // get the membership proof match tree.get_sub_tree_existence_proof( &keys, @@ -353,7 +396,7 @@ where .wl_storage .ethbridge_queries() .get_validator_set_args(None); - let data = RelayProof { + let relay_proof = RelayProof { validator_set_args: validator_args.into(), signatures: sort_sigs( &voting_powers, @@ -366,9 +409,13 @@ where batch_nonce: signed_root.data.1.into(), relayer_address: relayer.to_string(), }; - let data = ethers::abi::AbiEncode::encode(data) - .try_to_vec() - .expect("Serializing a relay proof should not fail."); + let rsp = GenBridgePoolProofRsp { + abi_encoded_proof: ethers::abi::AbiEncode::encode( + relay_proof, + ), + appendices: with_appendix.then_some(appendices), + }; + let data = rsp.try_to_vec().into_storage_result()?; Ok(EncodedResponseQuery { data, ..Default::default() @@ -389,7 +436,7 @@ where /// backing each `TransferToEthereum` event. fn transfer_to_ethereum_progress( ctx: RequestCtx<'_, D, H>, -) -> storage_api::Result> +) -> storage_api::Result> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -444,6 +491,12 @@ where ) .average_voting_power(ctx.wl_storage); for transfer in transfers { + let key = get_key_from_hash(&transfer.keccak256()); + let transfer = ctx + .wl_storage + .read::(&key) + .into_storage_result()? + .expect("The transfer must be present in storage"); pending_events.insert(transfer, voting_power); } } @@ -974,9 +1027,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - (vec![transfer.keccak256()], bertha_address()) - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -999,7 +1056,7 @@ mod test_ethbridge_router { let data = RelayProof { validator_set_args: validator_args.into(), signatures: sort_sigs(&voting_powers, &signed_root.signatures), - transfers: vec![transfer.into()], + transfers: vec![(&transfer).into()], pool_root: signed_root.data.0.0, proof: proof.proof.into_iter().map(|hash| hash.0).collect(), proof_flags: proof.flags, @@ -1007,12 +1064,11 @@ mod test_ethbridge_router { relayer_address: bertha_address().to_string(), }; let proof = ethers::abi::AbiEncode::encode(data); - assert_eq!(proof, resp.data); + assert_eq!(proof, resp.data.abi_encoded_proof); } - /// Test if the merkle tree including a transfer - /// has had its root signed, then we cannot generate - /// a proof. + /// Test if the merkle tree including a transfer has not had its + /// root signed, then we cannot generate a proof. #[tokio::test] async fn test_cannot_get_proof() { let mut client = TestClient::new(RPC); @@ -1090,9 +1146,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - (vec![transfer2.keccak256()], bertha_address()) - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer2.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1204,16 +1264,8 @@ mod test_ethbridge_router { ) .expect("Test failed"); - let event_transfer = - namada_core::types::ethereum_events::TransferToEthereum { - kind: transfer.transfer.kind, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - amount: transfer.transfer.amount, - gas_payer: transfer.gas_fee.payer.clone(), - gas_amount: transfer.gas_fee.amount, - sender: transfer.transfer.sender.clone(), - }; + let event_transfer: namada_core::types::ethereum_events::TransferToEthereum + = (&transfer).into(); let eth_event = EthereumEvent::TransfersToEthereum { nonce: Default::default(), transfers: vec![event_transfer.clone()], @@ -1274,10 +1326,8 @@ mod test_ethbridge_router { .transfer_to_ethereum_progress(&client) .await .unwrap(); - let expected: HashMap< - namada_core::types::ethereum_events::TransferToEthereum, - FractionalVotingPower, - > = [(event_transfer, voting_power)].into_iter().collect(); + let expected: HashMap = + [(transfer, voting_power)].into_iter().collect(); assert_eq!(expected, resp); } @@ -1354,9 +1404,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - vec![(transfer.keccak256(), bertha_address())] - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1377,9 +1431,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - vec![transfer.keccak256()] - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, From 91b2fef6e48940d3e29a8618005ffe7cc7c88f47 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 7 Aug 2023 14:32:08 +0100 Subject: [PATCH 047/103] Test pending transfers and events have the same ABI encoding --- core/src/types/eth_bridge_pool.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index 4f12eec619..3a9b185e9f 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -244,8 +244,6 @@ impl Encode<6> for PendingTransfer { } } -// TODO: test that encode for `PendingTransfer` and -// `TransferToEthereumEvent` yield the same keccak hash impl Encode<6> for TransferToEthereumEvent { fn tokenize(&self) -> [Token; 6] { // TODO: This version should be looked up from storage @@ -292,3 +290,30 @@ pub struct GasFee { /// The account of fee payer. pub payer: Address, } + +#[cfg(test)] +mod test_eth_bridge_pool_types { + use super::*; + use crate::types::address::testing::established_address_1; + + /// Test that [`PendingTransfer`] and [`TransferToEthereum`] + /// have the same keccak hash, after being ABI encoded. + #[test] + fn test_same_keccak_hash() { + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + amount: 10u64.into(), + asset: EthAddress([0xaa; 20]), + recipient: EthAddress([0xbb; 20]), + sender: established_address_1(), + }, + gas_fee: GasFee { + amount: 10u64.into(), + payer: established_address_1(), + }, + }; + let event: TransferToEthereumEvent = (&pending).into(); + assert_eq!(pending.keccak256(), event.keccak256()); + } +} From 5678ee7cabd4d27ce94ef3ef62b1fa5e9a8445be Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 7 Aug 2023 14:48:17 +0100 Subject: [PATCH 048/103] Changelog for #1789 --- .changelog/unreleased/features/1789-update-ethbridge-rs.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1789-update-ethbridge-rs.md diff --git a/.changelog/unreleased/features/1789-update-ethbridge-rs.md b/.changelog/unreleased/features/1789-update-ethbridge-rs.md new file mode 100644 index 0000000000..4fe862e522 --- /dev/null +++ b/.changelog/unreleased/features/1789-update-ethbridge-rs.md @@ -0,0 +1,2 @@ +- Update ethbridge-rs to v0.22.0 + ([\#1789](https://github.com/anoma/namada/pull/1789)) \ No newline at end of file From 28dad92e41db3b104953e1d70f197f3f8ccf3130 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 09:26:06 +0100 Subject: [PATCH 049/103] Move `check_balance_changes` to Bridge pool VP module --- .../ethereum_bridge/bridge_pool_vp.rs | 105 ++++++++++++++++- .../ledger/native_vp/ethereum_bridge/vp.rs | 106 +----------------- 2 files changed, 105 insertions(+), 106 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 056f41573b..f3ee00e3c9 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -22,7 +22,6 @@ use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_ethereum_bridge::parameters::read_native_erc20_address; -use crate::ledger::native_vp::ethereum_bridge::vp::check_balance_changes; use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::{DBIter, DB}; @@ -31,7 +30,7 @@ use crate::types::address::Address; use crate::types::eth_bridge_pool::{PendingTransfer, TransferToEthereumKind}; use crate::types::ethereum_events::EthAddress; use crate::types::storage::Key; -use crate::types::token::{balance_key, Amount}; +use crate::types::token::{balance_key, Amount, Change}; use crate::vm::WasmCacheAccess; #[derive(thiserror::Error, Debug)] @@ -473,6 +472,108 @@ where } } +/// Checks that the balances at both `sender` and `receiver` have changed by +/// some amount, and that the changes balance each other out. If the balance +/// changes are invalid, the reason is logged and a `None` is returned. +/// Otherwise, return the `Amount` of the transfer i.e. by how much the sender's +/// balance decreased, or equivalently by how much the receiver's balance +/// increased +fn check_balance_changes( + reader: impl StorageReader, + sender: &Key, + receiver: &Key, +) -> Result, Error> { + let sender_balance_pre = reader + .read_pre_value::(sender)? + .unwrap_or_default() + .change(); + let sender_balance_post = match reader.read_post_value::(sender)? { + Some(value) => value, + None => { + return Err(Error(eyre!( + "Rejecting transaction as could not read_post balance key {}", + sender, + ))); + } + } + .change(); + let receiver_balance_pre = reader + .read_pre_value::(receiver)? + .unwrap_or_default() + .change(); + let receiver_balance_post = match reader + .read_post_value::(receiver)? + { + Some(value) => value, + None => { + return Err(Error(eyre!( + "Rejecting transaction as could not read_post balance key {}", + receiver, + ))); + } + } + .change(); + + let sender_balance_delta = + calculate_delta(sender_balance_pre, sender_balance_post)?; + let receiver_balance_delta = + calculate_delta(receiver_balance_pre, receiver_balance_post)?; + if receiver_balance_delta != -sender_balance_delta { + tracing::debug!( + ?sender_balance_pre, + ?receiver_balance_pre, + ?sender_balance_post, + ?receiver_balance_post, + ?sender_balance_delta, + ?receiver_balance_delta, + "Rejecting transaction as balance changes do not match" + ); + return Ok(None); + } + if sender_balance_delta.is_zero() || sender_balance_delta > Change::zero() { + assert!( + receiver_balance_delta.is_zero() + || receiver_balance_delta < Change::zero() + ); + tracing::debug!( + "Rejecting transaction as no balance change or invalid change" + ); + return Ok(None); + } + if sender_balance_post < Change::zero() { + tracing::debug!( + ?sender_balance_post, + "Rejecting transaction as balance is negative" + ); + return Ok(None); + } + if receiver_balance_post < Change::zero() { + tracing::debug!( + ?receiver_balance_post, + "Rejecting transaction as balance is negative" + ); + return Ok(None); + } + + Ok(Some(Amount::from_change(receiver_balance_delta))) +} + +/// Return the delta between `balance_pre` and `balance_post`, erroring if there +/// is an underflow +fn calculate_delta( + balance_pre: Change, + balance_post: Change, +) -> Result { + match balance_post.checked_sub(&balance_pre) { + Some(result) => Ok(result), + None => Err(Error(eyre!( + "Underflow while calculating delta: {} - {}", + balance_post, + balance_pre + ))), + } +} + #[cfg(test)] mod test_bridge_pool_vp { use std::env::temp_dir; diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 715b8ad616..6a18554692 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -11,9 +11,9 @@ use namada_core::ledger::storage::traits::StorageHasher; use namada_core::ledger::{eth_bridge, storage as ledger_storage}; use namada_core::types::address::Address; use namada_core::types::storage::Key; -use namada_core::types::token::{balance_key, Amount, Change}; +use namada_core::types::token::{balance_key, Amount}; -use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader, VpEnv}; +use crate::ledger::native_vp::{Ctx, NativeVp, VpEnv}; use crate::proto::Tx; use crate::vm::WasmCacheAccess; @@ -224,108 +224,6 @@ fn determine_check_type( Ok(Some(CheckType::Erc20Transfer)) } -/// Checks that the balances at both `sender` and `receiver` have changed by -/// some amount, and that the changes balance each other out. If the balance -/// changes are invalid, the reason is logged and a `None` is returned. -/// Otherwise, return the `Amount` of the transfer i.e. by how much the sender's -/// balance decreased, or equivalently by how much the receiver's balance -/// increased -pub(super) fn check_balance_changes( - reader: impl StorageReader, - sender: &Key, - receiver: &Key, -) -> Result> { - let sender_balance_pre = reader - .read_pre_value::(sender)? - .unwrap_or_default() - .change(); - let sender_balance_post = match reader.read_post_value::(sender)? { - Some(value) => value, - None => { - return Err(eyre!( - "Rejecting transaction as could not read_post balance key {}", - sender, - )); - } - } - .change(); - let receiver_balance_pre = reader - .read_pre_value::(receiver)? - .unwrap_or_default() - .change(); - let receiver_balance_post = match reader - .read_post_value::(receiver)? - { - Some(value) => value, - None => { - return Err(eyre!( - "Rejecting transaction as could not read_post balance key {}", - receiver, - )); - } - } - .change(); - - let sender_balance_delta = - calculate_delta(sender_balance_pre, sender_balance_post)?; - let receiver_balance_delta = - calculate_delta(receiver_balance_pre, receiver_balance_post)?; - if receiver_balance_delta != -sender_balance_delta { - tracing::debug!( - ?sender_balance_pre, - ?receiver_balance_pre, - ?sender_balance_post, - ?receiver_balance_post, - ?sender_balance_delta, - ?receiver_balance_delta, - "Rejecting transaction as balance changes do not match" - ); - return Ok(None); - } - if sender_balance_delta.is_zero() || sender_balance_delta > Change::zero() { - assert!( - receiver_balance_delta.is_zero() - || receiver_balance_delta < Change::zero() - ); - tracing::debug!( - "Rejecting transaction as no balance change or invalid change" - ); - return Ok(None); - } - if sender_balance_post < Change::zero() { - tracing::debug!( - ?sender_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - if receiver_balance_post < Change::zero() { - tracing::debug!( - ?receiver_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - - Ok(Some(Amount::from_change(receiver_balance_delta))) -} - -/// Return the delta between `balance_pre` and `balance_post`, erroring if there -/// is an underflow -fn calculate_delta( - balance_pre: Change, - balance_post: Change, -) -> Result { - match balance_post.checked_sub(&balance_pre) { - Some(result) => Ok(result), - None => Err(eyre!( - "Underflow while calculating delta: {} - {}", - balance_post, - balance_pre - )), - } -} - #[cfg(test)] mod tests { use std::default::Default; From e102affebec318b4fb70b5459d62c03cd95acb82 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 09:41:45 +0100 Subject: [PATCH 050/103] Add token addr field to Bridge pool gas fees --- core/src/types/eth_bridge_pool.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index 3a9b185e9f..4a7e11cc06 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -75,8 +75,8 @@ pub struct PendingTransferAppendix<'transfer> { pub kind: Cow<'transfer, TransferToEthereumKind>, /// The sender of the transfer. pub sender: Cow<'transfer, Address>, - /// The amount of gas fees (in NAM) - /// paid by the user sending this transfer + /// The amount of gas fees paid by the user + /// sending this transfer. pub gas_fee: Cow<'transfer, GasFee>, } @@ -158,10 +158,10 @@ pub struct TransferToEthereum { BorshSchema, )] pub struct PendingTransfer { - /// The message to send to Ethereum to + /// Transfer to Ethereum data. pub transfer: TransferToEthereum, - /// The amount of gas fees (in NAM) - /// paid by the user sending this transfer + /// Amount of gas fees paid by the user + /// sending the transfer. pub gas_fee: GasFee, } @@ -267,9 +267,9 @@ impl From<&PendingTransfer> for Key { } } -/// The amount of NAM to be payed to the relayer of -/// a transfer across the Ethereum Bridge to compensate -/// for Ethereum gas fees. +/// The amount of fees to be payed, in Namada, to the relayer +/// of a transfer across the Ethereum Bridge, compensating +/// for Ethereum gas costs. #[derive( Debug, Clone, @@ -285,10 +285,13 @@ impl From<&PendingTransfer> for Key { BorshSchema, )] pub struct GasFee { - /// The amount of fees (in NAM) + /// The amount of fees. pub amount: Amount, /// The account of fee payer. pub payer: Address, + /// The address of the fungible token to draw + /// gas fees from. + pub token: Address, } #[cfg(test)] From b30fbeb6171c9fff71f9860bd45a175adfe3312e Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 10:37:27 +0100 Subject: [PATCH 051/103] Fix compilation errors from prev commit --- .../lib/node/ledger/shell/finalize_block.rs | 3 ++- .../ledger/eth_bridge/storage/bridge_pool.rs | 18 ++++++++++++++++++ core/src/types/eth_bridge_pool.rs | 2 ++ .../transactions/ethereum_events/events.rs | 3 +++ shared/src/ledger/eth_bridge/bridge_pool.rs | 3 ++- .../ethereum_bridge/bridge_pool_vp.rs | 9 +++++++++ shared/src/ledger/queries/shell/eth_bridge.rs | 8 ++++++++ tests/src/native_vp/eth_bridge_pool.rs | 4 ++++ wasm/wasm_source/src/tx_bridge_pool.rs | 10 +++++++--- 9 files changed, 55 insertions(+), 5 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index f79c5cf421..55a1c63314 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1656,6 +1656,7 @@ mod test_finalize_block { /// /// Sets the validity of the transfer on Ethereum's side. fn test_bp_nonce_is_incremented_aux(valid_transfer: bool) { + use crate::node::ledger::shell::address::nam; test_bp(|shell: &mut TestShell| { let asset = EthAddress([0xff; 20]); let receiver = EthAddress([0xaa; 20]); @@ -1680,7 +1681,6 @@ mod test_finalize_block { } // add bertha's gas fees the pool { - use crate::node::ledger::shell::address::nam; let amt: Amount = 999_999_u64.into(); let pool_balance_key = token::balance_key( &nam(), @@ -1706,6 +1706,7 @@ mod test_finalize_block { sender: bertha.clone(), }, gas_fee: GasFee { + token: nam(), amount: 10u64.into(), payer: bertha.clone(), }, diff --git a/core/src/ledger/eth_bridge/storage/bridge_pool.rs b/core/src/ledger/eth_bridge/storage/bridge_pool.rs index 0c20c50ff7..167e23e779 100644 --- a/core/src/ledger/eth_bridge/storage/bridge_pool.rs +++ b/core/src/ledger/eth_bridge/storage/bridge_pool.rs @@ -415,6 +415,7 @@ mod test_bridge_pool_tree { use proptest::prelude::*; use super::*; + use crate::types::address::nam; use crate::types::eth_bridge_pool::{ GasFee, TransferToEthereum, TransferToEthereumKind, }; @@ -441,6 +442,7 @@ mod test_bridge_pool_tree { amount: 1.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -468,6 +470,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -496,6 +499,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -534,6 +538,7 @@ mod test_bridge_pool_tree { amount: 1.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -562,6 +567,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -593,6 +599,7 @@ mod test_bridge_pool_tree { amount: 1u64.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -617,6 +624,7 @@ mod test_bridge_pool_tree { amount: 1u64.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -653,6 +661,7 @@ mod test_bridge_pool_tree { amount: 1.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -672,6 +681,7 @@ mod test_bridge_pool_tree { amount: 1u64.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -704,6 +714,7 @@ mod test_bridge_pool_tree { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -733,6 +744,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -763,6 +775,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -793,6 +806,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -821,6 +835,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -849,6 +864,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -877,6 +893,7 @@ mod test_bridge_pool_tree { amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -909,6 +926,7 @@ mod test_bridge_pool_tree { amount: Default::default(), }, gas_fee: GasFee { + token: nam(), amount: Default::default(), payer: bertha_address(), }, diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index 4a7e11cc06..86b0815509 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -297,6 +297,7 @@ pub struct GasFee { #[cfg(test)] mod test_eth_bridge_pool_types { use super::*; + use crate::types::address::nam; use crate::types::address::testing::established_address_1; /// Test that [`PendingTransfer`] and [`TransferToEthereum`] @@ -312,6 +313,7 @@ mod test_eth_bridge_pool_types { sender: established_address_1(), }, gas_fee: GasFee { + token: nam(), amount: 10u64.into(), payer: established_address_1(), }, diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 65c8f832ca..32c9a16e4b 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -676,6 +676,7 @@ mod tests { kind, }, gas_fee: GasFee { + token: nam(), amount: Amount::from(1), payer: payer.clone(), }, @@ -1087,6 +1088,7 @@ mod tests { kind: eth_bridge_pool::TransferToEthereumKind::Erc20, }, gas_fee: GasFee { + token: nam(), amount: Amount::from(1), payer: address::testing::established_address_1(), }, @@ -1499,6 +1501,7 @@ mod tests { kind: eth_bridge_pool::TransferToEthereumKind::Nut, }, gas_fee: GasFee { + token: nam(), amount: Amount::from(1), payer: address::testing::established_address_1(), }, diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 9576b70baf..4b362caf4f 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -62,7 +62,7 @@ pub async fn build_bridge_pool_tx( transfer: TransferToEthereum { asset, recipient, - sender: sender.clone(), + sender, amount, kind: if nut { TransferToEthereumKind::Nut @@ -699,6 +699,7 @@ mod recommendations { amount: Default::default(), }, gas_fee: GasFee { + token: namada_core::types::address::nam(), amount: gas_amount.into(), payer: bertha_address(), }, diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index f3ee00e3c9..9e7bdb1bed 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -680,6 +680,7 @@ mod test_bridge_pool_vp { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -925,6 +926,7 @@ mod test_bridge_pool_vp { amount: TOKENS.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: bertha_address(), }, @@ -1186,6 +1188,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: bertha_address(), }, @@ -1217,6 +1220,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: bertha_address(), }, @@ -1343,6 +1347,7 @@ mod test_bridge_pool_vp { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1410,6 +1415,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: 100.into(), payer: bertha_address(), }, @@ -1500,6 +1506,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: 100.into(), payer: bertha_address(), }, @@ -1607,6 +1614,7 @@ mod test_bridge_pool_vp { amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: 100.into(), payer: established_address_1(), }, @@ -1698,6 +1706,7 @@ mod test_bridge_pool_vp { amount: TOKENS.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: daewon_address(), }, diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index 0dbd76b376..bd9115f48a 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -656,6 +656,7 @@ mod test_ethbridge_router { use super::*; use crate::ledger::queries::testing::TestClient; use crate::ledger::queries::RPC; + use crate::types::address::nam; use crate::types::eth_abi::Encode; use crate::types::eth_bridge_pool::{ GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, @@ -865,6 +866,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -907,6 +909,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -968,6 +971,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1081,6 +1085,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1176,6 +1181,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1248,6 +1254,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1350,6 +1357,7 @@ mod test_ethbridge_router { amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 956b2a1b8c..a452e9b82e 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -139,6 +139,7 @@ mod test_bridge_pool_vp { amount: Amount::from(TOKENS), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: bertha_address(), }, @@ -157,6 +158,7 @@ mod test_bridge_pool_vp { amount: Amount::from(TOKENS), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: bertha_address(), }, @@ -175,6 +177,7 @@ mod test_bridge_pool_vp { amount: Amount::from(TOKEN_CAP + 1), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: bertha_address(), }, @@ -193,6 +196,7 @@ mod test_bridge_pool_vp { amount: Amount::from(TOKENS), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: albert_address(), }, diff --git a/wasm/wasm_source/src/tx_bridge_pool.rs b/wasm/wasm_source/src/tx_bridge_pool.rs index c1a65403a7..765029d647 100644 --- a/wasm/wasm_source/src/tx_bridge_pool.rs +++ b/wasm/wasm_source/src/tx_bridge_pool.rs @@ -12,13 +12,16 @@ fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { .map_err(|e| Error::wrap("Error deserializing PendingTransfer", e))?; log_string("Received transfer to add to pool."); // pay the gas fees - let GasFee { amount, ref payer } = transfer.gas_fee; - let nam_addr = ctx.get_native_token().unwrap(); + let GasFee { + token: ref fee_token_addr, + amount, + ref payer, + } = transfer.gas_fee; token::transfer( ctx, payer, &bridge_pool::BRIDGE_POOL_ADDRESS, - &nam_addr, + fee_token_addr, amount.native_denominated(), &None, &None, @@ -33,6 +36,7 @@ fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { } = transfer.transfer; // if minting wNam, escrow the correct amount if asset == native_erc20_address(ctx)? { + let nam_addr = ctx.get_native_token()?; token::transfer( ctx, sender, From 1850b0742e5e7ea183793acf3ac6753a03ca2def Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 11:14:48 +0100 Subject: [PATCH 052/103] Check correct gas fee token in Bridge pool transfers --- .../transactions/ethereum_events/events.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 32c9a16e4b..08605308be 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -361,10 +361,6 @@ where }) .filter(is_pending_transfer_key) .collect(); - let pool_balance_key = - balance_key(&wl_storage.storage.native_token, &BRIDGE_POOL_ADDRESS); - let relayer_rewards_key = - balance_key(&wl_storage.storage.native_token, relayer); // Remove the completed transfers from the bridge pool for (event, is_valid) in transfers.iter().zip(valid_transfers.iter().copied()) @@ -398,6 +394,10 @@ where &pending_transfer, )?); } + let pool_balance_key = + balance_key(&pending_transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); + let relayer_rewards_key = + balance_key(&pending_transfer.gas_fee.token, relayer); // give the relayer the gas fee for this transfer. update::amount(wl_storage, &relayer_rewards_key, |balance| { balance.receive(&pending_transfer.gas_fee.amount); @@ -409,10 +409,8 @@ where wl_storage.delete(&key)?; _ = pending_keys.remove(&key); _ = changed_keys.insert(key); - } - if !transfers.is_empty() { - changed_keys.insert(relayer_rewards_key); - changed_keys.insert(pool_balance_key); + _ = changed_keys.insert(pool_balance_key); + _ = changed_keys.insert(relayer_rewards_key); } if pending_keys.is_empty() { @@ -494,9 +492,9 @@ where let mut changed_keys = BTreeSet::default(); let payer_balance_key = - balance_key(&wl_storage.storage.native_token, &transfer.gas_fee.payer); + balance_key(&transfer.gas_fee.token, &transfer.gas_fee.payer); let pool_balance_key = - balance_key(&wl_storage.storage.native_token, &BRIDGE_POOL_ADDRESS); + balance_key(&transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); update::amount(wl_storage, &payer_balance_key, |balance| { balance.receive(&transfer.gas_fee.amount); })?; From bbff3cc61f4cfa90dfe06018d115a739d4468f6a Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 14:11:59 +0100 Subject: [PATCH 053/103] Validate different gas fee tokens in Bridge pool VP --- .../ethereum_bridge/bridge_pool_vp.rs | 382 ++++++++---------- 1 file changed, 178 insertions(+), 204 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 9e7bdb1bed..312cdf493d 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -10,7 +10,10 @@ //! correctly. This means that the appropriate data is //! added to the pool and gas fees are submitted appropriately //! and that tokens to be transferred are escrowed. + +use std::borrow::Cow; use std::collections::BTreeSet; +use std::marker::PhantomData; use borsh::BorshDeserialize; use eyre::eyre; @@ -21,16 +24,17 @@ use namada_core::ledger::eth_bridge::storage::bridge_pool::{ use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_ethereum_bridge::parameters::read_native_erc20_address; +use namada_ethereum_bridge::storage::wrapped_erc20s; use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::{DBIter, DB}; use crate::proto::Tx; -use crate::types::address::Address; +use crate::types::address::{Address, InternalAddress}; use crate::types::eth_bridge_pool::{PendingTransfer, TransferToEthereumKind}; use crate::types::ethereum_events::EthAddress; use crate::types::storage::Key; -use crate::types::token::{balance_key, Amount, Change}; +use crate::types::token::{balance_key, Amount}; use crate::vm::WasmCacheAccess; #[derive(thiserror::Error, Debug)] @@ -84,8 +88,12 @@ where { /// Get the change in the balance of an account /// associated with an address - fn account_balance_delta(&self, address: &Address) -> Option { - let account_key = balance_key(&self.ctx.storage.native_token, address); + fn account_balance_delta( + &self, + token: &Address, + address: &Address, + ) -> Option { + let account_key = balance_key(token, address); let before: Amount = (&self.ctx) .read_pre_value(&account_key) .map_err(|error| { @@ -113,62 +121,34 @@ where }) } - /// Check that the correct amount of erc20 assets were - /// sent from the correct account into escrow. - fn check_erc20s_escrowed( + /// Check that the correct amount of tokens were sent + /// from the correct account into escrow. + #[inline] + fn check_escrowed_toks( &self, - keys_changed: &BTreeSet, - transfer: &PendingTransfer, + delta: EscrowDelta, ) -> Result { - // check that the assets to be transferred were escrowed - let token = transfer.token_address(); - let owner_key = balance_key(&token, &transfer.transfer.sender); - let escrow_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); - if keys_changed.contains(&owner_key) - && keys_changed.contains(&escrow_key) - { - match check_balance_changes(&self.ctx, &owner_key, &escrow_key)? { - Some(amount) if amount == transfer.transfer.amount => Ok(true), - _ => { - tracing::debug!( - "The assets of the transfer were not properly \ - escrowed into the Ethereum bridge pool" - ); - Ok(false) - } - } - } else { - tracing::debug!( - "The assets of the transfer were not properly escrowed into \ - the Ethereum bridge pool." - ); - Ok(false) - } - } - - /// Check that the correct amount of Nam was sent - /// from the correct account into escrow - #[inline] - fn check_nam_escrowed(&self, delta: EscrowDelta) -> Result { - self.check_nam_escrowed_balance(delta) + self.check_escrowed_toks_balance(delta) .map(|balance| balance.is_some()) } - /// Check that the correct amount of Nam was sent + /// Check that the correct amount of tokens were sent /// from the correct account into escrow, and return /// the updated escrow balance. - fn check_nam_escrowed_balance( + fn check_escrowed_toks_balance( &self, - delta: EscrowDelta, + delta: EscrowDelta, ) -> Result, Error> { let EscrowDelta { + token, payer_account, escrow_account, expected_debit, expected_credit, + .. } = delta; - let debit = self.account_balance_delta(payer_account); - let credit = self.account_balance_delta(escrow_account); + let debit = self.account_balance_delta(&token, payer_account); + let credit = self.account_balance_delta(&token, escrow_account); match (debit, credit) { // success case @@ -223,12 +203,51 @@ where } } + /// Check that the gas was correctly escrow. + fn check_gas_escrow( + &self, + wnam_address: &EthAddress, + transfer: &PendingTransfer, + gas_check: EscrowDelta<'_, GasCheck>, + ) -> Result { + if hints::unlikely( + *gas_check.token == wrapped_erc20s::token(wnam_address), + ) { + // NB: this should never be possible: protocol tx state updates + // never result in wNAM ERC20s being minted + tracing::error!( + ?transfer, + "Attempted to pay Bridge pool fees with wrapped NAM." + ); + return Ok(false); + } + if matches!( + &*gas_check.token, + Address::Internal(InternalAddress::Nut(_)) + ) { + tracing::debug!( + ?transfer, + "The gas fees of the transfer cannot be paid in NUTs." + ); + return Ok(false); + } + if !self.check_escrowed_toks(gas_check)? { + tracing::debug!( + ?transfer, + "The gas fees of the transfer were not properly escrowed into \ + the Ethereum bridge pool." + ); + return Ok(false); + } + Ok(true) + } + /// Validate a wrapped NAM transfer to Ethereum. - fn check_wnam_preconditions<'trans>( + fn check_wnam_escrow( &self, &wnam_address: &EthAddress, - transfer: &'trans PendingTransfer, - escrow_checks: EscrowCheck<'trans>, + transfer: &PendingTransfer, + token_check: EscrowDelta<'_, TokenCheck>, ) -> Result { if hints::unlikely(matches!( &transfer.transfer.kind, @@ -239,6 +258,7 @@ where // that users should never hold wNAM NUTs. doesn't hurt to add // the extra check to the vp, though tracing::error!( + ?transfer, "Attempted to add a wNAM NUT transfer to the Bridge pool" ); return Ok(false); @@ -264,7 +284,7 @@ where // amount of Nam must be escrowed in the Ethereum bridge VP's // storage. let escrowed_balance = - match self.check_nam_escrowed_balance(escrow_checks.token_check)? { + match self.check_escrowed_toks_balance(token_check)? { Some(balance) => balance.resolve(), None => return Ok(false), }; @@ -292,18 +312,28 @@ where } /// Deteremine the debit and credit amounts that should be checked. - fn escrow_check<'trans>( - &self, + fn determine_escrow_checks<'trans, 'this: 'trans>( + &'this self, wnam_address: &EthAddress, transfer: &'trans PendingTransfer, ) -> Result, Error> { - let is_native_asset = &transfer.transfer.asset == wnam_address; - // there is a corner case where the gas fees and escrowed Nam - // are debited from the same address when mint wNam. - Ok( - if transfer.gas_fee.payer == transfer.transfer.sender - && is_native_asset - { + let tok_is_native_asset = &transfer.transfer.asset == wnam_address; + let gas_is_native_asset = + transfer.gas_fee.token == self.ctx.storage.native_token; + + let (expected_gas_debit, expected_token_debit) = { + let same_sender_and_fee_payer = + transfer.gas_fee.payer == transfer.transfer.sender; + let same_tokens_and_gas_asset = (tok_is_native_asset + && gas_is_native_asset) + || transfer.token_address() == transfer.gas_fee.token; + + // there is a corner case where the gas fees and escrowed tokens + // are debited from the same address + let same_debited_address = + same_sender_and_fee_payer && same_tokens_and_gas_asset; + + if same_debited_address { let debit = transfer .gas_fee .amount @@ -314,63 +344,92 @@ where amount." )) })?; - EscrowCheck { - gas_check: EscrowDelta { - payer_account: &transfer.gas_fee.payer, - escrow_account: &BRIDGE_POOL_ADDRESS, - expected_debit: debit, - expected_credit: transfer.gas_fee.amount, - }, - token_check: EscrowDelta { - payer_account: &transfer.transfer.sender, - escrow_account: &BRIDGE_ADDRESS, - expected_debit: debit, - expected_credit: transfer.transfer.amount, - }, - } + (debit, debit) } else { - EscrowCheck { - gas_check: EscrowDelta { - payer_account: &transfer.gas_fee.payer, - escrow_account: &BRIDGE_POOL_ADDRESS, - expected_debit: transfer.gas_fee.amount, - expected_credit: transfer.gas_fee.amount, - }, - token_check: EscrowDelta { - payer_account: &transfer.transfer.sender, - escrow_account: if is_native_asset { - &BRIDGE_ADDRESS - } else { - &BRIDGE_POOL_ADDRESS - }, - expected_debit: transfer.transfer.amount, - expected_credit: transfer.transfer.amount, - }, - } + (transfer.gas_fee.amount, transfer.transfer.amount) + } + }; + Ok(EscrowCheck { + gas_check: EscrowDelta { + token: if gas_is_native_asset { + Cow::Borrowed(&self.ctx.storage.native_token) + } else { + Cow::Borrowed(&transfer.gas_fee.token) + }, + payer_account: &transfer.gas_fee.payer, + escrow_account: &BRIDGE_POOL_ADDRESS, + expected_debit: expected_gas_debit, + expected_credit: transfer.gas_fee.amount, + _kind: PhantomData, }, - ) + token_check: EscrowDelta { + token: if tok_is_native_asset { + Cow::Borrowed(&self.ctx.storage.native_token) + } else { + Cow::Owned(transfer.token_address()) + }, + payer_account: &transfer.transfer.sender, + escrow_account: if tok_is_native_asset { + &BRIDGE_ADDRESS + } else { + &BRIDGE_POOL_ADDRESS + }, + expected_debit: expected_token_debit, + expected_credit: transfer.transfer.amount, + _kind: PhantomData, + }, + }) } } /// Helper struct for handling the different escrow /// checking scenarios. -#[derive(Copy, Clone)] -struct EscrowDelta<'a> { +struct EscrowDelta<'a, KIND> { + token: Cow<'a, Address>, payer_account: &'a Address, escrow_account: &'a Address, expected_debit: Amount, expected_credit: Amount, + _kind: PhantomData<*const KIND>, +} + +impl EscrowDelta<'_, KIND> { + fn validate_changed_keys(&self, changed_keys: &BTreeSet) -> bool { + let EscrowDelta { + token, + payer_account, + escrow_account, + .. + } = self; + let owner_key = balance_key(token, payer_account); + let escrow_key = balance_key(token, escrow_account); + changed_keys.contains(&owner_key) && changed_keys.contains(&escrow_key) + } } /// There are two checks we must do when minting wNam. +/// /// 1. Check that gas fees were escrowed. /// 2. Check that the Nam to back wNam was escrowed. -#[derive(Copy, Clone)] struct EscrowCheck<'a> { - gas_check: EscrowDelta<'a>, - token_check: EscrowDelta<'a>, + gas_check: EscrowDelta<'a, GasCheck>, + token_check: EscrowDelta<'a, TokenCheck>, +} + +impl EscrowCheck<'_> { + #[inline] + fn validate_changed_keys(&self, changed_keys: &BTreeSet) -> bool { + self.gas_check.validate_changed_keys(changed_keys) + && self.token_check.validate_changed_keys(changed_keys) + } } +/// Perform a gas check. +enum GasCheck {} + +/// Perform a token check. +enum TokenCheck {} + impl<'a, D, H, CA> NativeVp for BridgePoolVp<'a, D, H, CA> where D: 'static + DB + for<'iter> DBIter<'iter>, @@ -445,20 +504,32 @@ where } // The deltas in the escrowed amounts we must check. let wnam_address = read_native_erc20_address(&self.ctx.pre())?; - let escrow_checks = self.escrow_check(&wnam_address, &transfer)?; + let escrow_checks = + self.determine_escrow_checks(&wnam_address, &transfer)?; + if !escrow_checks.validate_changed_keys(keys_changed) { + tracing::debug!( + ?transfer, + "Missing storage modifications in the Bridge pool" + ); + return Ok(false); + } // check that gas was correctly escrowed. - if !self.check_nam_escrowed(escrow_checks.gas_check)? { + if !self.check_gas_escrow( + &wnam_address, + &transfer, + escrow_checks.gas_check, + )? { return Ok(false); } // check the escrowed assets if transfer.transfer.asset == wnam_address { - self.check_wnam_preconditions( + self.check_wnam_escrow( &wnam_address, &transfer, - escrow_checks, + escrow_checks.token_check, ) } else { - self.check_erc20s_escrowed(keys_changed, &transfer) + self.check_escrowed_toks(escrow_checks.token_check) } .map(|ok| { if ok { @@ -466,114 +537,18 @@ where "The Ethereum bridge pool VP accepted the transfer {:?}.", transfer ); + } else { + tracing::debug!( + ?transfer, + "The assets of the transfer were not properly escrowed \ + into the Ethereum bridge pool." + ); } ok }) } } -/// Checks that the balances at both `sender` and `receiver` have changed by -/// some amount, and that the changes balance each other out. If the balance -/// changes are invalid, the reason is logged and a `None` is returned. -/// Otherwise, return the `Amount` of the transfer i.e. by how much the sender's -/// balance decreased, or equivalently by how much the receiver's balance -/// increased -fn check_balance_changes( - reader: impl StorageReader, - sender: &Key, - receiver: &Key, -) -> Result, Error> { - let sender_balance_pre = reader - .read_pre_value::(sender)? - .unwrap_or_default() - .change(); - let sender_balance_post = match reader.read_post_value::(sender)? { - Some(value) => value, - None => { - return Err(Error(eyre!( - "Rejecting transaction as could not read_post balance key {}", - sender, - ))); - } - } - .change(); - let receiver_balance_pre = reader - .read_pre_value::(receiver)? - .unwrap_or_default() - .change(); - let receiver_balance_post = match reader - .read_post_value::(receiver)? - { - Some(value) => value, - None => { - return Err(Error(eyre!( - "Rejecting transaction as could not read_post balance key {}", - receiver, - ))); - } - } - .change(); - - let sender_balance_delta = - calculate_delta(sender_balance_pre, sender_balance_post)?; - let receiver_balance_delta = - calculate_delta(receiver_balance_pre, receiver_balance_post)?; - if receiver_balance_delta != -sender_balance_delta { - tracing::debug!( - ?sender_balance_pre, - ?receiver_balance_pre, - ?sender_balance_post, - ?receiver_balance_post, - ?sender_balance_delta, - ?receiver_balance_delta, - "Rejecting transaction as balance changes do not match" - ); - return Ok(None); - } - if sender_balance_delta.is_zero() || sender_balance_delta > Change::zero() { - assert!( - receiver_balance_delta.is_zero() - || receiver_balance_delta < Change::zero() - ); - tracing::debug!( - "Rejecting transaction as no balance change or invalid change" - ); - return Ok(None); - } - if sender_balance_post < Change::zero() { - tracing::debug!( - ?sender_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - if receiver_balance_post < Change::zero() { - tracing::debug!( - ?receiver_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - - Ok(Some(Amount::from_change(receiver_balance_delta))) -} - -/// Return the delta between `balance_pre` and `balance_post`, erroring if there -/// is an underflow -fn calculate_delta( - balance_pre: Change, - balance_post: Change, -) -> Result { - match balance_post.checked_sub(&balance_pre) { - Some(result) => Ok(result), - None => Err(Error(eyre!( - "Underflow while calculating delta: {} - {}", - balance_post, - balance_pre - ))), - } -} - #[cfg(test)] mod test_bridge_pool_vp { use std::env::temp_dir; @@ -584,7 +559,6 @@ mod test_bridge_pool_vp { use namada_ethereum_bridge::parameters::{ Contracts, EthereumBridgeConfig, UpgradeableContract, }; - use namada_ethereum_bridge::storage::wrapped_erc20s; use super::*; use crate::ledger::gas::VpGasMeter; From fdf00a821fb8776ce35aed0ff5a6b7ee943b6b5c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 15:18:16 +0100 Subject: [PATCH 054/103] Fix test_minting_wnam() unit test --- .../src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 312cdf493d..98e2c2f793 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -1372,7 +1372,7 @@ mod test_bridge_pool_vp { /// Test that we can escrow Nam if we /// want to mint wNam on Ethereum. #[test] - fn test_mint_wnam() { + fn test_minting_wnam() { // setup let mut wl_storage = setup_storage(); let eb_account_key = @@ -1396,7 +1396,7 @@ mod test_bridge_pool_vp { }; // add transfer to pool - let keys_changed = { + let mut keys_changed = { wl_storage .write_log .write( @@ -1418,6 +1418,7 @@ mod test_bridge_pool_vp { .expect("Test failed"), ) .expect("Test failed"); + assert!(keys_changed.insert(account_key)); let bp_account_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); wl_storage .write_log @@ -1428,6 +1429,7 @@ mod test_bridge_pool_vp { .expect("Test failed"), ) .expect("Test failed"); + assert!(keys_changed.insert(bp_account_key)); wl_storage .write_log .write( @@ -1437,6 +1439,7 @@ mod test_bridge_pool_vp { .expect("Test failed"), ) .expect("Test failed"); + assert!(keys_changed.insert(eb_account_key)); let verifiers = BTreeSet::default(); // create the data to be given to the vp From 7ec76d952b29ffa31c42409ca6214bb27cee3b12 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 9 Aug 2023 11:09:49 +0100 Subject: [PATCH 055/103] Add misc Bridge pool VP tests --- tests/src/native_vp/eth_bridge_pool.rs | 87 ++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index a452e9b82e..22584e1464 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -94,6 +94,17 @@ mod test_bridge_pool_vp { // Bertha has ERC20 tokens too. let token = wrapped_erc20s::token(&ASSET); env.credit_tokens(&bertha_address(), &token, BERTHA_TOKENS.into()); + // Bertha has... NUTs? :D + let nuts = wrapped_erc20s::nut(&ASSET); + env.credit_tokens(&bertha_address(), &nuts, BERTHA_TOKENS.into()); + // give Bertha some wNAM. technically this is impossible to mint, + // but we're testing invalid protocol paths... + let wnam_tok_addr = wrapped_erc20s::token(&wnam()); + env.credit_tokens( + &bertha_address(), + &wnam_tok_addr, + BERTHA_TOKENS.into(), + ); env } @@ -203,4 +214,80 @@ mod test_bridge_pool_vp { }; validate_tx(create_tx(transfer, &bertha_keypair())); } + + #[test] + fn invalidate_fees_paid_in_nuts() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::nut(&ASSET), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + invalidate_tx(create_tx(transfer, &bertha_keypair())); + } + + #[test] + fn invalidate_fees_paid_in_wnam() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::token(&wnam()), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + invalidate_tx(create_tx(transfer, &bertha_keypair())); + } + + #[test] + fn validate_erc20_tx_with_same_gas_token() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: ASSET, + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::token(&ASSET), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + validate_tx(create_tx(transfer, &bertha_keypair())); + } + + #[test] + fn validate_wnam_tx_with_diff_gas_token() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::token(&ASSET), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + validate_tx(create_tx(transfer, &bertha_keypair())); + } } From 5c8a030ec3901fd01d2e6551f0a467d872316de3 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 9 Aug 2023 13:58:36 +0100 Subject: [PATCH 056/103] Fix bug in `determine_escrow_checks` and document corner cases --- .../ethereum_bridge/bridge_pool_vp.rs | 107 ++++++++++++------ 1 file changed, 71 insertions(+), 36 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 98e2c2f793..9899ce8238 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -318,64 +318,83 @@ where transfer: &'trans PendingTransfer, ) -> Result, Error> { let tok_is_native_asset = &transfer.transfer.asset == wnam_address; - let gas_is_native_asset = - transfer.gas_fee.token == self.ctx.storage.native_token; + + // NB: this comparison is not enough to check + // if NAM is being used for both tokens and gas + // fees, since wrapped NAM will have a different + // token address + let same_token_and_gas_erc20 = + transfer.token_address() == transfer.gas_fee.token; let (expected_gas_debit, expected_token_debit) = { + // NB: there is a corner case where the gas fees and escrowed + // tokens are debited from the same address, when the gas fee + // payer and token sender are the same, and the underlying + // transferred assets are the same let same_sender_and_fee_payer = transfer.gas_fee.payer == transfer.transfer.sender; - let same_tokens_and_gas_asset = (tok_is_native_asset - && gas_is_native_asset) - || transfer.token_address() == transfer.gas_fee.token; - - // there is a corner case where the gas fees and escrowed tokens - // are debited from the same address + let gas_is_native_asset = + transfer.gas_fee.token == self.ctx.storage.native_token; + let gas_and_token_is_native_asset = + gas_is_native_asset && tok_is_native_asset; + let same_token_and_gas_asset = + gas_and_token_is_native_asset || same_token_and_gas_erc20; let same_debited_address = - same_sender_and_fee_payer && same_tokens_and_gas_asset; + same_sender_and_fee_payer && same_token_and_gas_asset; if same_debited_address { - let debit = transfer - .gas_fee - .amount - .checked_add(transfer.transfer.amount) - .ok_or_else(|| { - Error(eyre!( - "Addition oveflowed adding gas fee + transfer \ - amount." - )) - })?; + let debit = sum_gas_and_token_amounts(transfer)?; (debit, debit) } else { (transfer.gas_fee.amount, transfer.transfer.amount) } }; + let (expected_gas_credit, expected_token_credit) = { + // NB: there is a corner case where the gas fees and escrowed + // tokens are credited to the same address, when the underlying + // transferred assets are the same (unless the asset is NAM) + let same_credited_address = same_token_and_gas_erc20; + + if same_credited_address { + let credit = sum_gas_and_token_amounts(transfer)?; + (credit, credit) + } else { + (transfer.gas_fee.amount, transfer.transfer.amount) + } + }; + let (token_check_addr, token_check_escrow_acc) = if tok_is_native_asset + { + // when minting wrapped NAM on Ethereum, escrow to the Ethereum + // bridge address, and draw from NAM token accounts + let token = Cow::Borrowed(&self.ctx.storage.native_token); + let escrow_account = &BRIDGE_ADDRESS; + (token, escrow_account) + } else { + // otherwise, draw from ERC20/NUT wrapped asset token accounts, + // and escrow to the Bridge pool address + let token = Cow::Owned(transfer.token_address()); + let escrow_account = &BRIDGE_POOL_ADDRESS; + (token, escrow_account) + }; + Ok(EscrowCheck { gas_check: EscrowDelta { - token: if gas_is_native_asset { - Cow::Borrowed(&self.ctx.storage.native_token) - } else { - Cow::Borrowed(&transfer.gas_fee.token) - }, + // NB: it's fine to not check for wrapped NAM here, + // as users won't hold wrapped NAM tokens in practice, + // anyway + token: Cow::Borrowed(&transfer.gas_fee.token), payer_account: &transfer.gas_fee.payer, escrow_account: &BRIDGE_POOL_ADDRESS, expected_debit: expected_gas_debit, - expected_credit: transfer.gas_fee.amount, + expected_credit: expected_gas_credit, _kind: PhantomData, }, token_check: EscrowDelta { - token: if tok_is_native_asset { - Cow::Borrowed(&self.ctx.storage.native_token) - } else { - Cow::Owned(transfer.token_address()) - }, + token: token_check_addr, payer_account: &transfer.transfer.sender, - escrow_account: if tok_is_native_asset { - &BRIDGE_ADDRESS - } else { - &BRIDGE_POOL_ADDRESS - }, + escrow_account: token_check_escrow_acc, expected_debit: expected_token_debit, - expected_credit: transfer.transfer.amount, + expected_credit: expected_token_credit, _kind: PhantomData, }, }) @@ -430,6 +449,22 @@ enum GasCheck {} /// Perform a token check. enum TokenCheck {} +/// Sum gas and token amounts on a pending transfer, checking for overflows. +#[inline] +fn sum_gas_and_token_amounts( + transfer: &PendingTransfer, +) -> Result { + transfer + .gas_fee + .amount + .checked_add(transfer.transfer.amount) + .ok_or_else(|| { + Error(eyre!( + "Addition oveflowed adding gas fee + transfer amount." + )) + }) +} + impl<'a, D, H, CA> NativeVp for BridgePoolVp<'a, D, H, CA> where D: 'static + DB + for<'iter> DBIter<'iter>, From bad6a7c39de3c310d802e97ab5f44ee4049a6ebb Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 10 Aug 2023 09:14:59 +0100 Subject: [PATCH 057/103] Arbitrary gas fees in Ethereum events `init_balances` --- .../transactions/ethereum_events/events.rs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 08605308be..d0bfdfc668 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -715,18 +715,25 @@ mod tests { wl_storage: &mut TestWlStorage, pending_transfers: &Vec, ) { - // Gas payer - let payer = address::testing::established_address_2(); - let payer_key = balance_key(&nam(), &payer); - let payer_balance = Amount::from(0); - wl_storage - .write_bytes( - &payer_key, - payer_balance.try_to_vec().expect("Test failed"), - ) + for transfer in pending_transfers { + // Gas + let payer = address::testing::established_address_2(); + let payer_key = balance_key(&transfer.gas_fee.token, &payer); + let payer_balance = Amount::from(0); + wl_storage + .write_bytes( + &payer_key, + payer_balance.try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); + let escrow_key = + balance_key(&transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); + update::amount(wl_storage, &escrow_key, |balance| { + let gas_fee = Amount::from_u64(1); + balance.receive(&gas_fee); + }) .expect("Test failed"); - for transfer in pending_transfers { if transfer.transfer.asset == wnam() { // native ERC20 let sender_key = balance_key(&nam(), &transfer.transfer.sender); @@ -772,12 +779,6 @@ mod tests { ) .expect("Test failed"); }; - let gas_fee = Amount::from(1); - let escrow_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - update::amount(wl_storage, &escrow_key, |balance| { - balance.receive(&gas_fee); - }) - .expect("Test failed"); } } From 87455467e2e3936ad55f4b56ab932a6a8c5be108 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 10 Aug 2023 10:09:33 +0100 Subject: [PATCH 058/103] Test paying Bridge pool gas fees in ERC20 tokens --- .../transactions/ethereum_events/events.rs | 230 ++++++++++++++---- 1 file changed, 179 insertions(+), 51 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index d0bfdfc668..48aac0b8de 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -609,6 +609,8 @@ where #[cfg(test)] mod tests { + use std::collections::HashMap; + use assert_matches::assert_matches; use borsh::BorshSerialize; use eyre::Result; @@ -648,21 +650,88 @@ mod tests { .expect("Test failed"); } + /// Helper data structure to feed to [`init_bridge_pool_transfers`]. + struct TransferData { + kind: eth_bridge_pool::TransferToEthereumKind, + gas_token: Address, + } + + impl Default for TransferData { + fn default() -> Self { + Self { + kind: eth_bridge_pool::TransferToEthereumKind::Erc20, + gas_token: nam(), + } + } + } + + /// Build [`TransferData`] values. + struct TransferDataBuilder { + kind: Option, + gas_token: Option
, + } + + #[allow(dead_code)] + impl TransferDataBuilder { + fn new() -> Self { + Self { + kind: None, + gas_token: None, + } + } + + fn kind( + mut self, + kind: eth_bridge_pool::TransferToEthereumKind, + ) -> Self { + self.kind = Some(kind); + self + } + + fn kind_erc20(self) -> Self { + self.kind(eth_bridge_pool::TransferToEthereumKind::Erc20) + } + + fn kind_nut(self) -> Self { + self.kind(eth_bridge_pool::TransferToEthereumKind::Nut) + } + + fn gas_token(mut self, address: Address) -> Self { + self.gas_token = Some(address); + self + } + + fn gas_erc20(self, address: &EthAddress) -> Self { + self.gas_token(wrapped_erc20s::token(address)) + } + + fn gas_nut(self, address: &EthAddress) -> Self { + self.gas_token(wrapped_erc20s::nut(address)) + } + + fn build(self) -> TransferData { + TransferData { + kind: self.kind.unwrap_or_else(|| TransferData::default().kind), + gas_token: self + .gas_token + .unwrap_or_else(|| TransferData::default().gas_token), + } + } + } + fn init_bridge_pool_transfers( wl_storage: &mut TestWlStorage, assets_transferred: A, ) -> Vec where - A: Into< - BTreeSet<(EthAddress, eth_bridge_pool::TransferToEthereumKind)>, - >, + A: Into>, { let sender = address::testing::established_address_1(); let payer = address::testing::established_address_2(); // set pending transfers let mut pending_transfers = vec![]; - for (i, (asset, kind)) in + for (i, (asset, TransferData { kind, gas_token })) in assets_transferred.into().into_iter().enumerate() { let transfer = PendingTransfer { @@ -674,7 +743,7 @@ mod tests { kind, }, gas_fee: GasFee { - token: nam(), + token: gas_token, amount: Amount::from(1), payer: payer.clone(), }, @@ -700,14 +769,16 @@ mod tests { .map(|i| { ( EthAddress([i; 20]), - if i & 1 == 0 { - eth_bridge_pool::TransferToEthereumKind::Erc20 - } else { - eth_bridge_pool::TransferToEthereumKind::Nut - }, + TransferDataBuilder::new() + .kind(if i & 1 == 0 { + eth_bridge_pool::TransferToEthereumKind::Erc20 + } else { + eth_bridge_pool::TransferToEthereumKind::Nut + }) + .build(), ) }) - .collect::>(), + .collect::>(), ) } @@ -970,14 +1041,36 @@ mod tests { let random_erc20_token = wrapped_erc20s::nut(&random_erc20); let random_erc20_2 = EthAddress([0xee; 20]); let random_erc20_token_2 = wrapped_erc20s::token(&random_erc20_2); + let random_erc20_3 = EthAddress([0xdd; 20]); + let random_erc20_token_3 = wrapped_erc20s::token(&random_erc20_3); + let random_erc20_4 = EthAddress([0xcc; 20]); + let random_erc20_token_4 = wrapped_erc20s::nut(&random_erc20_4); + let erc20_gas_addr = EthAddress([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, + ]); let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, [ - (native_erc20, eth_bridge_pool::TransferToEthereumKind::Erc20), - (random_erc20, eth_bridge_pool::TransferToEthereumKind::Nut), + (native_erc20, TransferData::default()), + (random_erc20, TransferDataBuilder::new().kind_nut().build()), ( random_erc20_2, - eth_bridge_pool::TransferToEthereumKind::Erc20, + TransferDataBuilder::new().kind_erc20().build(), + ), + ( + random_erc20_3, + TransferDataBuilder::new() + .kind_erc20() + .gas_erc20(&erc20_gas_addr) + .build(), + ), + ( + random_erc20_4, + TransferDataBuilder::new() + .kind_nut() + .gas_erc20(&erc20_gas_addr) + .build(), ), ], ); @@ -995,39 +1088,53 @@ mod tests { transfers, relayer: relayer.clone(), }; - let payer_balance_key = balance_key(&nam(), &relayer); - let pool_balance_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - let mut bp_balance_pre = Amount::try_from_slice( + let payer_nam_balance_key = balance_key(&nam(), &relayer); + let payer_erc_balance_key = + balance_key(&wrapped_erc20s::token(&erc20_gas_addr), &relayer); + let pool_nam_balance_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); + let pool_erc_balance_key = balance_key( + &wrapped_erc20s::token(&erc20_gas_addr), + &BRIDGE_POOL_ADDRESS, + ); + let mut bp_nam_balance_pre = Amount::try_from_slice( + &wl_storage + .read_bytes(&pool_nam_balance_key) + .expect("Test failed") + .expect("Test failed"), + ) + .expect("Test failed"); + let mut bp_erc_balance_pre = Amount::try_from_slice( &wl_storage - .read_bytes(&pool_balance_key) + .read_bytes(&pool_erc_balance_key) .expect("Test failed") .expect("Test failed"), ) .expect("Test failed"); let mut changed_keys = act_on(&mut wl_storage, event).unwrap(); - assert!( - changed_keys.remove(&balance_key( - &random_erc20_token, - &BRIDGE_POOL_ADDRESS - )) - ); - assert!( - changed_keys.remove(&balance_key( - &random_erc20_token_2, - &BRIDGE_POOL_ADDRESS - )) - ); + for erc20 in [ + random_erc20_token, + random_erc20_token_2, + random_erc20_token_3, + random_erc20_token_4, + ] { + assert!( + changed_keys.remove(&balance_key(&erc20, &BRIDGE_POOL_ADDRESS)), + "Expected {erc20:?} Bridge pool balance to change" + ); + assert!( + changed_keys.remove(&minted_balance_key(&erc20)), + "Expected {erc20:?} minted supply to change" + ); + } assert!( changed_keys .remove(&minted_balance_key(&wrapped_erc20s::token(&wnam()))) ); - assert!(changed_keys.remove(&minted_balance_key(&random_erc20_token))); - assert!( - changed_keys.remove(&minted_balance_key(&random_erc20_token_2)) - ); - assert!(changed_keys.remove(&payer_balance_key)); - assert!(changed_keys.remove(&pool_balance_key)); + assert!(changed_keys.remove(&payer_nam_balance_key)); + assert!(changed_keys.remove(&payer_erc_balance_key)); + assert!(changed_keys.remove(&pool_nam_balance_key)); + assert!(changed_keys.remove(&pool_erc_balance_key)); assert!(changed_keys.remove(&get_nonce_key())); assert!(changed_keys.iter().all(|k| pending_keys.contains(k))); @@ -1040,24 +1147,45 @@ mod tests { // NOTE: we should have one write -- the bridge pool nonce update 1 ); - let relayer_balance = Amount::try_from_slice( + let relayer_nam_balance = Amount::try_from_slice( + &wl_storage + .read_bytes(&payer_nam_balance_key) + .expect("Test failed: read error") + .expect("Test failed: no value in storage"), + ) + .expect("Test failed"); + assert_eq!(relayer_nam_balance, Amount::from(3)); + let relayer_erc_balance = Amount::try_from_slice( &wl_storage - .read_bytes(&payer_balance_key) + .read_bytes(&payer_erc_balance_key) .expect("Test failed: read error") .expect("Test failed: no value in storage"), ) .expect("Test failed"); - assert_eq!(relayer_balance, Amount::from(3)); - let bp_balance_post = Amount::try_from_slice( + assert_eq!(relayer_erc_balance, Amount::from(2)); + + let bp_nam_balance_post = Amount::try_from_slice( + &wl_storage + .read_bytes(&pool_nam_balance_key) + .expect("Test failed: read error") + .expect("Test failed: no value in storage"), + ) + .expect("Test failed"); + let bp_erc_balance_post = Amount::try_from_slice( &wl_storage - .read_bytes(&pool_balance_key) + .read_bytes(&pool_erc_balance_key) .expect("Test failed: read error") .expect("Test failed: no value in storage"), ) .expect("Test failed"); - bp_balance_pre.spend(&bp_balance_post); - assert_eq!(bp_balance_pre, Amount::from(3)); - assert_eq!(bp_balance_post, Amount::from(0)); + + bp_nam_balance_pre.spend(&bp_nam_balance_post); + assert_eq!(bp_nam_balance_pre, Amount::from(3)); + assert_eq!(bp_nam_balance_post, Amount::from(0)); + + bp_erc_balance_pre.spend(&bp_erc_balance_post); + assert_eq!(bp_erc_balance_pre, Amount::from(2)); + assert_eq!(bp_erc_balance_post, Amount::from(0)); } #[test] @@ -1272,30 +1400,30 @@ mod tests { let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, [ - (native_erc20, eth_bridge_pool::TransferToEthereumKind::Erc20), + (native_erc20, TransferData::default()), ( EthAddress([0xaa; 20]), - eth_bridge_pool::TransferToEthereumKind::Erc20, + TransferDataBuilder::new().kind_erc20().build(), ), ( EthAddress([0xbb; 20]), - eth_bridge_pool::TransferToEthereumKind::Nut, + TransferDataBuilder::new().kind_nut().build(), ), ( EthAddress([0xcc; 20]), - eth_bridge_pool::TransferToEthereumKind::Erc20, + TransferDataBuilder::new().kind_erc20().build(), ), ( EthAddress([0xdd; 20]), - eth_bridge_pool::TransferToEthereumKind::Nut, + TransferDataBuilder::new().kind_nut().build(), ), ( EthAddress([0xee; 20]), - eth_bridge_pool::TransferToEthereumKind::Erc20, + TransferDataBuilder::new().kind_erc20().build(), ), ( EthAddress([0xff; 20]), - eth_bridge_pool::TransferToEthereumKind::Nut, + TransferDataBuilder::new().kind_nut().build(), ), ], ); From 9c3fbcc20dd324d0b9b7ef348c2fc3f7f1c31794 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 21:46:41 +0100 Subject: [PATCH 059/103] Update CLI args for Bridge pool transfers --- apps/src/lib/cli.rs | 41 ++++++++++++++------- shared/src/ledger/args.rs | 2 + shared/src/ledger/eth_bridge/bridge_pool.rs | 2 + 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index e7a34eba67..bd67eaf454 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2421,6 +2421,20 @@ pub mod args { ); pub const BLOCK_HEIGHT: Arg = arg("block-height"); // pub const BLOCK_HEIGHT_OPT: ArgOpt = arg_opt("height"); + pub const BRIDGE_POOL_GAS_AMOUNT: ArgDefault = + arg_default( + "pool-gas-amount", + DefaultFn(|| token::DenominatedAmount { + amount: token::Amount::default(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }), + ); + pub const BRIDGE_POOL_GAS_PAYER: Arg = arg("pool-gas-payer"); + pub const BRIDGE_POOL_GAS_TOKEN: ArgDefaultFromCtx = + arg_default_from_ctx( + "pool-gas-token", + DefaultFn(|| "NAM".parse().unwrap()), + ); pub const BROADCAST_ONLY: ArgFlag = flag("broadcast-only"); pub const CHAIN_ID: Arg = arg("chain-id"); pub const CHAIN_ID_OPT: ArgOpt = CHAIN_ID.opt(); @@ -2475,14 +2489,6 @@ pub mod args { ); pub const GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".parse().unwrap())); - pub const FEE_PAYER: Arg = arg("fee-payer"); - pub const FEE_AMOUNT: ArgDefault = arg_default( - "fee-amount", - DefaultFn(|| token::DenominatedAmount { - amount: token::Amount::default(), - denom: NATIVE_MAX_DECIMAL_PLACES.into(), - }), - ); pub const GENESIS_PATH: Arg = arg("genesis-path"); pub const GENESIS_VALIDATOR: ArgOpt = arg("genesis-validator").opt(); @@ -2796,6 +2802,7 @@ pub mod args { amount: self.amount, fee_amount: self.fee_amount, fee_payer: ctx.get(&self.fee_payer), + fee_token: ctx.get(&self.fee_token), code_path: self.code_path, } } @@ -2808,8 +2815,9 @@ pub mod args { let recipient = ETH_ADDRESS.parse(matches); let sender = ADDRESS.parse(matches); let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); - let fee_amount = FEE_AMOUNT.parse(matches).amount; - let fee_payer = FEE_PAYER.parse(matches); + let fee_amount = BRIDGE_POOL_GAS_AMOUNT.parse(matches).amount; + let fee_payer = BRIDGE_POOL_GAS_PAYER.parse(matches); + let fee_token = BRIDGE_POOL_GAS_TOKEN.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); let nut = NUT.parse(matches); Self { @@ -2820,6 +2828,7 @@ pub mod args { amount, fee_amount, fee_payer, + fee_token, code_path, nut, } @@ -2847,15 +2856,19 @@ pub mod args { "The amount of tokens being sent across the bridge.", ), ) - .arg(FEE_AMOUNT.def().help( - "The amount of NAM you wish to pay to have this transfer \ + .arg(BRIDGE_POOL_GAS_AMOUNT.def().help( + "The amount of gas you wish to pay to have this transfer \ relayed to Ethereum.", )) .arg( - FEE_PAYER.def().help( - "The Namada address of the account paying the fee.", + BRIDGE_POOL_GAS_PAYER.def().help( + "The Namada address of the account paying the gas.", ), ) + .arg(BRIDGE_POOL_GAS_TOKEN.def().help( + "The token for paying the Bridge pool gas fees. Defaults \ + to NAM.", + )) .arg(NUT.def().help( "Add Non Usable Tokens (NUTs) to the Bridge pool. These \ are usually obtained from invalid transfers to Namada.", diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 8d884b4a5f..7a0939d98d 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -720,6 +720,8 @@ pub struct EthereumBridgePool { pub fee_amount: token::Amount, /// The account of fee payer. pub fee_payer: C::Address, + /// The token in which the gas is being paid + pub fee_token: C::Address, /// Path to the tx WASM code file pub code_path: PathBuf, } diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 4b362caf4f..2e9358502a 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -49,6 +49,7 @@ pub async fn build_bridge_pool_tx( amount, fee_amount, fee_payer, + fee_token, code_path, }: args::EthereumBridgePool, gas_payer: common::PublicKey, @@ -71,6 +72,7 @@ pub async fn build_bridge_pool_tx( }, }, gas_fee: GasFee { + token: fee_token, amount: fee_amount, payer: fee_payer, }, From 4091b555d12d54457cdb329748585ef0f4052096 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 22:13:41 +0100 Subject: [PATCH 060/103] Standardize Bridge pool transfer CLI args --- apps/src/lib/cli.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index bd67eaf454..37ab19b30a 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2435,6 +2435,7 @@ pub mod args { "pool-gas-token", DefaultFn(|| "NAM".parse().unwrap()), ); + pub const BRIDGE_POOL_TARGET: Arg = arg("target"); pub const BROADCAST_ONLY: ArgFlag = flag("broadcast-only"); pub const CHAIN_ID: Arg = arg("chain-id"); pub const CHAIN_ID_OPT: ArgOpt = CHAIN_ID.opt(); @@ -2464,7 +2465,7 @@ pub mod args { pub const ETH_GAS: ArgOpt = arg_opt("eth-gas"); pub const ETH_GAS_PRICE: ArgOpt = arg_opt("eth-gas-price"); pub const ETH_ADDRESS: Arg = arg("ethereum-address"); - pub const ETH_ADDRESS_OPT: ArgOpt = arg_opt("ethereum-address"); + pub const ETH_ADDRESS_OPT: ArgOpt = ETH_ADDRESS.opt(); pub const ETH_RPC_ENDPOINT: ArgDefault = arg_default( "eth-rpc-endpoint", DefaultFn(|| "http://localhost:8545".into()), @@ -2812,8 +2813,8 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let asset = ERC20.parse(matches); - let recipient = ETH_ADDRESS.parse(matches); - let sender = ADDRESS.parse(matches); + let recipient = BRIDGE_POOL_TARGET.parse(matches); + let sender = SOURCE.parse(matches); let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); let fee_amount = BRIDGE_POOL_GAS_AMOUNT.parse(matches).amount; let fee_payer = BRIDGE_POOL_GAS_PAYER.parse(matches); @@ -2842,14 +2843,12 @@ pub mod args { .help("The Ethereum address of the ERC20 token."), ) .arg( - ETH_ADDRESS + BRIDGE_POOL_TARGET .def() .help("The Ethereum address receiving the tokens."), ) .arg( - ADDRESS - .def() - .help("The Namada address sending the tokens."), + SOURCE.def().help("The Namada address sending the tokens."), ) .arg( AMOUNT.def().help( From 5342193e1d8d00a7665842cf963477f8b1a82e58 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 8 Aug 2023 22:36:21 +0100 Subject: [PATCH 061/103] Make Bridge pool gas payer CLI arg optional --- apps/src/lib/cli.rs | 14 +++++++------- shared/src/ledger/args.rs | 4 +++- shared/src/ledger/eth_bridge/bridge_pool.rs | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 37ab19b30a..cc0e81fc30 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2429,7 +2429,8 @@ pub mod args { denom: NATIVE_MAX_DECIMAL_PLACES.into(), }), ); - pub const BRIDGE_POOL_GAS_PAYER: Arg = arg("pool-gas-payer"); + pub const BRIDGE_POOL_GAS_PAYER: ArgOpt = + arg_opt("pool-gas-payer"); pub const BRIDGE_POOL_GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx( "pool-gas-token", @@ -2802,7 +2803,7 @@ pub mod args { sender: ctx.get(&self.sender), amount: self.amount, fee_amount: self.fee_amount, - fee_payer: ctx.get(&self.fee_payer), + fee_payer: self.fee_payer.map(|fee_payer| ctx.get(&fee_payer)), fee_token: ctx.get(&self.fee_token), code_path: self.code_path, } @@ -2859,11 +2860,10 @@ pub mod args { "The amount of gas you wish to pay to have this transfer \ relayed to Ethereum.", )) - .arg( - BRIDGE_POOL_GAS_PAYER.def().help( - "The Namada address of the account paying the gas.", - ), - ) + .arg(BRIDGE_POOL_GAS_PAYER.def().help( + "The Namada address of the account paying the gas. By \ + default, it is the same as the source.", + )) .arg(BRIDGE_POOL_GAS_TOKEN.def().help( "The token for paying the Bridge pool gas fees. Defaults \ to NAM.", diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 7a0939d98d..9aed6793e6 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -719,7 +719,9 @@ pub struct EthereumBridgePool { /// The amount of fees (in NAM) pub fee_amount: token::Amount, /// The account of fee payer. - pub fee_payer: C::Address, + /// + /// If unset, it is the same as the sender. + pub fee_payer: Option, /// The token in which the gas is being paid pub fee_token: C::Address, /// Path to the tx WASM code file diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 2e9358502a..d3f875c76c 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -54,6 +54,7 @@ pub async fn build_bridge_pool_tx( }: args::EthereumBridgePool, gas_payer: common::PublicKey, ) -> Result { + let fee_payer = fee_payer.unwrap_or_else(|| sender.clone()); let DenominatedAmount { amount, .. } = validate_amount(client, amount, &BRIDGE_ADDRESS, tx_args.force) .await From ce21ebd79516086b934ab9500b7ecfe803c85f04 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 09:55:13 +0100 Subject: [PATCH 062/103] Fix docstring Co-authored-by: Jacob Turner --- shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 9899ce8238..3ae649e4be 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -203,7 +203,7 @@ where } } - /// Check that the gas was correctly escrow. + /// Check that the gas was correctly escrowed. fn check_gas_escrow( &self, wnam_address: &EthAddress, From ba741a5174cfe1715f66324bd47d5cc94df51e2f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 13:47:26 +0100 Subject: [PATCH 063/103] Add `DenominatedAmount::is_zero` --- core/src/types/token.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/types/token.rs b/core/src/types/token.rs index 6056495cd1..a72bce448b 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -309,6 +309,12 @@ impl DenominatedAmount { } } + /// Check if the inner [`Amount`] is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.amount.is_zero() + } + /// A precise string representation. The number of /// decimal places in this string gives the denomination. /// This not true of the string produced by the `Display` From 27f07c80e96c11f47c20bd024085c6bce9a84aa6 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 13:47:44 +0100 Subject: [PATCH 064/103] Fix the denomination of Bridge pool gas fees --- apps/src/lib/cli.rs | 3 ++- shared/src/ledger/args.rs | 4 ++-- shared/src/ledger/eth_bridge/bridge_pool.rs | 22 +++++++++++++++------ shared/src/ledger/rpc.rs | 1 + 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index cc0e81fc30..93de834af3 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2817,7 +2817,8 @@ pub mod args { let recipient = BRIDGE_POOL_TARGET.parse(matches); let sender = SOURCE.parse(matches); let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); - let fee_amount = BRIDGE_POOL_GAS_AMOUNT.parse(matches).amount; + let fee_amount = + InputAmount::Unvalidated(BRIDGE_POOL_GAS_AMOUNT.parse(matches)); let fee_payer = BRIDGE_POOL_GAS_PAYER.parse(matches); let fee_token = BRIDGE_POOL_GAS_TOKEN.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 9aed6793e6..642d75fbf0 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -716,8 +716,8 @@ pub struct EthereumBridgePool { pub sender: C::Address, /// The amount to be transferred pub amount: InputAmount, - /// The amount of fees (in NAM) - pub fee_amount: token::Amount, + /// The amount of gas fees + pub fee_amount: InputAmount, /// The account of fee payer. /// /// If unset, it is the same as the sender. diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index d3f875c76c..db575bf6b1 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use borsh::BorshSerialize; use ethbridge_bridge_contract::Bridge; use ethers::providers::Middleware; -use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; +use namada_core::ledger::eth_bridge::storage::wrapped_erc20s; use namada_core::types::key::common; use owo_colors::OwoColorize; use serde::{Deserialize, Serialize}; @@ -55,11 +55,21 @@ pub async fn build_bridge_pool_tx( gas_payer: common::PublicKey, ) -> Result { let fee_payer = fee_payer.unwrap_or_else(|| sender.clone()); - let DenominatedAmount { amount, .. } = - validate_amount(client, amount, &BRIDGE_ADDRESS, tx_args.force) - .await - .expect("Failed to validate amount"); - + let DenominatedAmount { amount, .. } = validate_amount( + client, + amount, + &wrapped_erc20s::token(&asset), + tx_args.force, + ) + .await + .ok_or_else(|| Error::Other("Failed to validate amount".into()))?; + let DenominatedAmount { + amount: fee_amount, .. + } = validate_amount(client, fee_amount, &fee_token, tx_args.force) + .await + .ok_or_else(|| { + Error::Other("Failed to validate Bridge pool fee amount".into()) + })?; let transfer = PendingTransfer { transfer: TransferToEthereum { asset, diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs index d0fad687be..a4f24e666f 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -983,6 +983,7 @@ pub async fn validate_amount( force: bool, ) -> Option { let input_amount = match amount { + InputAmount::Unvalidated(amt) if amt.is_zero() => return Some(amt), InputAmount::Unvalidated(amt) => amt.canonical(), InputAmount::Validated(amt) => return Some(amt), }; From 968f1a6d5663e3485b39d7e090b67d6969487160 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 9 Aug 2023 16:51:30 +0100 Subject: [PATCH 065/103] Changelog for #1795 --- .changelog/unreleased/features/1795-bridge-pool-fees.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/features/1795-bridge-pool-fees.md diff --git a/.changelog/unreleased/features/1795-bridge-pool-fees.md b/.changelog/unreleased/features/1795-bridge-pool-fees.md new file mode 100644 index 0000000000..36a9a73c1c --- /dev/null +++ b/.changelog/unreleased/features/1795-bridge-pool-fees.md @@ -0,0 +1,2 @@ +- Allow Bridge pool transfer fees to be paid in arbitrary token types (except + NUTs) ([\#1795](https://github.com/anoma/namada/pull/1795)) \ No newline at end of file From ee8cdb21eb8d1e108c815ed4fce404d7d00595b9 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 14:34:45 +0100 Subject: [PATCH 066/103] Retrieve the inner raw string on wrapped context values --- apps/src/lib/cli/context.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index ee3a4a8dfa..106b3d00fc 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -239,6 +239,10 @@ impl FromContext { phantom: PhantomData, } } + + pub fn into_raw(self) -> String { + self.raw + } } impl FromContext { From 7a56f841f10a7c7b76e0e4d20eb5559b1911e7ae Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 14:35:18 +0100 Subject: [PATCH 067/103] Bridge pool conversion rates table --- apps/src/lib/cli.rs | 37 ++++++++++++++++++++++++++++++++----- shared/src/ledger/args.rs | 20 ++++++++++++++++++-- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 93de834af3..ed8f11f141 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2449,6 +2449,7 @@ pub mod args { "consensus-timeout-commit", DefaultFn(|| Timeout::from_str("1s").unwrap()), ); + pub const CONVERSION_TABLE: Arg = arg("conversion-table"); pub const DAEMON_MODE: ArgFlag = flag("daemon"); pub const DAEMON_MODE_RETRY_DUR: ArgOpt = arg_opt("retry-sleep"); pub const DAEMON_MODE_SUCCESS_DUR: ArgOpt = @@ -2517,7 +2518,6 @@ pub mod args { arg("max-commission-rate-change"); pub const MAX_ETH_GAS: ArgOpt = arg_opt("max_eth-gas"); pub const MODE: ArgOpt = arg_opt("mode"); - pub const NAM_PER_ETH: Arg = arg("nam-per-eth"); pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); @@ -2877,12 +2877,38 @@ pub mod args { } impl CliToSdkCtxless> for RecommendBatch { - fn to_sdk_ctxless(self) -> RecommendBatch { + fn to_sdk(self, ctx: &mut Context) -> RecommendBatch { RecommendBatch:: { query: self.query.to_sdk_ctxless(), max_gas: self.max_gas, gas: self.gas, - nam_per_eth: self.nam_per_eth, + conversion_table: { + let file = std::io::BufReader::new( + std::fs::File::open(self.conversion_table).expect( + "Failed to open the provided file to the \ + conversion table", + ), + ); + let table: HashMap = + serde_json::from_reader(file) + .expect("Failed to parse conversion table"); + table + .into_iter() + .map(|(token, conversion_rate)| { + let token_from_ctx = + FromContext::
::new(token); + let address = ctx.get(&token_from_ctx); + let alias = token_from_ctx.into_raw(); + ( + address, + BpConversionTableEntry { + alias, + conversion_rate, + }, + ) + }) + .collect() + }, } } } @@ -2892,12 +2918,12 @@ pub mod args { let query = Query::parse(matches); let max_gas = MAX_ETH_GAS.parse(matches); let gas = ETH_GAS.parse(matches); - let nam_to_eth = NAM_PER_ETH.parse(matches); + let conversion_table = CONVERSION_TABLE.parse(matches); Self { query, max_gas, gas, - nam_per_eth: nam_to_eth, + conversion_table, } } @@ -4589,6 +4615,7 @@ pub mod args { impl NamadaTypes for CliTypes { type Address = WalletAddress; type BalanceOwner = WalletBalanceOwner; + type BpConversionTable = PathBuf; type Data = PathBuf; type EthereumAddress = String; type Keypair = WalletKeypair; diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index 642d75fbf0..a63f6fba55 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -1,5 +1,6 @@ //! Structures encapsulating SDK arguments +use std::collections::HashMap; use std::path::PathBuf; use std::time::Duration as StdDuration; @@ -7,6 +8,7 @@ use namada_core::types::chain::ChainId; use namada_core::types::dec::Dec; use namada_core::types::ethereum_events::EthAddress; use namada_core::types::time::DateTimeUtc; +use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; use crate::ibc::core::ics24_host::identifier::{ChannelId, PortId}; @@ -57,15 +59,29 @@ pub trait NamadaTypes: Clone + std::fmt::Debug { type TransferTarget: Clone + std::fmt::Debug; /// Represents some data that is used in a transaction type Data: Clone + std::fmt::Debug; + /// Bridge pool recommendations conversion rates table. + type BpConversionTable: Clone + std::fmt::Debug; } /// The concrete types being used in Namada SDK #[derive(Clone, Debug)] pub struct SdkTypes; +/// An entry in the Bridge pool recommendations conversion +/// rates table. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BpConversionTableEntry { + /// An alias for the token, or the string representation + /// of its address if none is available. + pub alias: String, + /// Conversion rate from the given token to gwei. + pub conversion_rate: f64, +} + impl NamadaTypes for SdkTypes { type Address = Address; type BalanceOwner = namada_core::types::masp::BalanceOwner; + type BpConversionTable = HashMap; type Data = Vec; type EthereumAddress = (); type Keypair = namada_core::types::key::common::SecretKey; @@ -694,8 +710,8 @@ pub struct RecommendBatch { /// An optional parameter indicating how much net /// gas the relayer is willing to pay. pub gas: Option, - /// Estimate of amount of NAM a single ETH is worth. - pub nam_per_eth: f64, + /// Estimate amount of gwei a certain token type is worth. + pub conversion_table: HashMap, } /// A transfer to be added to the Ethereum bridge pool. From 3a1587650a43b41e09b170c22db9ba682ca4e7e9 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 11 Aug 2023 15:04:52 +0100 Subject: [PATCH 068/103] Convert gwei to tokens in Bridge pool conversion table --- apps/src/lib/cli.rs | 9 +-- shared/src/ledger/args.rs | 4 +- shared/src/ledger/eth_bridge/bridge_pool.rs | 75 ++++++++++++++++----- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index ed8f11f141..719b6900a5 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2358,6 +2358,7 @@ pub mod cmds { } pub mod args { + use std::collections::HashMap; use std::convert::TryFrom; use std::env; use std::net::SocketAddr; @@ -2876,7 +2877,7 @@ pub mod args { } } - impl CliToSdkCtxless> for RecommendBatch { + impl CliToSdk> for RecommendBatch { fn to_sdk(self, ctx: &mut Context) -> RecommendBatch { RecommendBatch:: { query: self.query.to_sdk_ctxless(), @@ -2940,9 +2941,9 @@ pub mod args { costs as close to the given value as possible without \ exceeding it.", )) - .arg(NAM_PER_ETH.def().help( - "The amount of NAM that one ETH is worth, represented as \ - a decimal number.", + .arg(CONVERSION_TABLE.def().help( + "Path to a JSON object containing a mapping between token \ + aliases (or addresses) and their conversion rates in gwei", )) } } diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index a63f6fba55..bc7e0c0342 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -710,8 +710,8 @@ pub struct RecommendBatch { /// An optional parameter indicating how much net /// gas the relayer is willing to pay. pub gas: Option, - /// Estimate amount of gwei a certain token type is worth. - pub conversion_table: HashMap, + /// Bridge pool recommendations conversion rates table. + pub conversion_table: C::BpConversionTable, } /// A transfer to be added to the Ethereum bridge pool. diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index db575bf6b1..c058c6f6f8 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -532,26 +532,43 @@ mod recommendations { let validator_gas = signature_fee() * signature_checks(voting_powers, &bp_root.signatures) + valset_fee() * valset_size; - // This is the amount of gwei a single name is worth - let gwei_per_nam = Uint::from_u64( - (10u64.pow(9) as f64 / args.nam_per_eth).floor() as u64, - ); // we don't recommend transfers that have already been relayed let mut contents: Vec<(String, I256, PendingTransfer)> = query_signed_bridge_pool(client) .await? .into_iter() - .filter_map(|(k, v)| { - if !in_progress.contains(&v) { + .filter_map(|(pending_hash, pending)| { + if !in_progress.contains(&pending) { + let conversion_rate = + if let Some(entry) = args + .conversion_table + .get(&pending.gas_fee.token) + { + let rate = entry.conversion_rate; + if rate <= 0.0f64 { + eprintln!( + "Ignoring token with an invalid conversion rate: {}", + pending.gas_fee.token, + ); + return None; + } + rate + } else { + return None; + }; + // This is the amount of gwei a single gas token is worth + let gwei_per_gas_token = Uint::from_u64( + (10u64.pow(9) as f64 / conversion_rate).floor() as u64, + ); Some(( - k, - I256::try_from(v.gas_fee.amount * gwei_per_nam) + pending_hash, + I256::try_from(pending.gas_fee.amount * gwei_per_gas_token) .map(|cost| transfer_fee() - cost) .try_halt(|err| { tracing::debug!(%err, "Failed to convert value to I256"); }), - v, + pending, )) } else { None @@ -568,7 +585,13 @@ mod recommendations { let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); let max_cost = args.gas.map(I256::from).unwrap_or_default(); - generate(contents, validator_gas, max_gas, max_cost)?; + generate( + contents, + &args.conversion_table, + validator_gas, + max_gas, + max_cost, + )?; control_flow::proceed(()) } @@ -611,6 +634,7 @@ mod recommendations { /// input parameters. fn generate( contents: Vec<(String, I256, PendingTransfer)>, + conversion_table: &HashMap, validator_gas: Uint, max_gas: Uint, max_cost: I256, @@ -630,13 +654,11 @@ mod recommendations { let mut total_cost = I256::try_from(validator_gas).try_halt(|err| { tracing::debug!(%err, "Failed to convert value to I256"); })?; - let mut total_fees = uint::ZERO; + let mut total_fees = HashMap::new(); let mut recommendation = vec![]; for (hash, cost, transfer) in contents.into_iter() { let next_total_gas = total_gas + unsigned_transfer_fee(); let next_total_cost = total_cost + cost; - let next_total_fees = - total_fees + Uint::from(transfer.gas_fee.amount); if cost.is_negative() { if next_total_gas <= max_gas && next_total_cost <= max_cost { state.feasible_region = true; @@ -661,7 +683,7 @@ mod recommendations { } total_cost = next_total_cost; total_gas = next_total_gas; - total_fees = next_total_fees; + update_total_fees(&mut total_fees, transfer, conversion_table); } control_flow::proceed( @@ -672,7 +694,7 @@ mod recommendations { total_gas ); println!("Estimated net profit (in gwei): {}", -total_cost); - println!("Total fees (in NAM): {}", total_fees); + println!("Total fees: {total_fees:#?}"); Some(recommendation) } else { println!( @@ -684,6 +706,23 @@ mod recommendations { ) } + fn update_total_fees( + total_fees: &mut HashMap, + transfer: PendingTransfer, + conversion_table: &HashMap, + ) { + let GasFee { token, amount, .. } = transfer.gas_fee; + let fees = total_fees + .entry( + conversion_table + .get(&token) + .map(|entry| entry.alias.clone()) + .unwrap_or_else(|| token.to_string()), + ) + .or_insert(uint::ZERO); + *fees += Uint::from(amount); + } + #[cfg(test)] mod test_recommendations { use namada_core::types::address::Address; @@ -783,6 +822,7 @@ mod recommendations { let expected = vec![hash; 17]; let recommendation = generate( process_transfers(profitable), + &Default::default(), Uint::from_u64(800_000), uint::MAX_VALUE, I256::zero(), @@ -800,6 +840,7 @@ mod recommendations { let expected: Vec<_> = vec![hash; 17]; let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(800_000), uint::MAX_VALUE, I256::zero(), @@ -816,6 +857,7 @@ mod recommendations { let expected = vec![hash; 2]; let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(50_000), Uint::from_u64(150_000), I256(uint::MAX_SIGNED_VALUE), @@ -836,6 +878,7 @@ mod recommendations { .collect(); let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(150_000), uint::MAX_VALUE, I256::from(20_000), @@ -853,6 +896,7 @@ mod recommendations { transfers.extend([transfer(17_500), transfer(17_500)]); let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(150_000), Uint::from_u64(330_000), I256::from(20_000), @@ -867,6 +911,7 @@ mod recommendations { let transfers = vec![transfer(75_000); 4]; let recommendation = generate( process_transfers(transfers), + &Default::default(), Uint::from_u64(300_000), uint::MAX_VALUE, I256::from(20_000), From d1ac06459a0ea355d795a0db0485e343a8b3652e Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 14 Aug 2023 09:44:08 +0100 Subject: [PATCH 069/103] Fix Bridge pool `construct_proof` --- shared/src/ledger/eth_bridge/bridge_pool.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index c058c6f6f8..ae3186a668 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -266,7 +266,7 @@ where struct BridgePoolProofResponse { hashes: Vec, relayer_address: Address, - total_fees: Amount, + total_fees: HashMap, abi_encoded_proof: Vec, } @@ -297,12 +297,20 @@ where relayer_address: args.relayer, total_fees: appendices .map(|appendices| { - appendices - .into_iter() - .map(|app| app.gas_fee.amount) - .sum::() + appendices.into_iter().fold( + HashMap::new(), + |mut total_fees, app| { + let GasFee { token, amount, .. } = + app.gas_fee.into_owned(); + let fees = total_fees + .entry(token) + .or_insert_with(Amount::zero); + fees.receive(&amount); + total_fees + }, + ) }) - .unwrap_or(Amount::zero()), + .unwrap_or_default(), abi_encoded_proof: bp_proof_bytes, }; println!("{}", serde_json::to_string(&resp).unwrap()); From 11aef0cf4330a3e2e6aa6b82a7e2c1ba096c9fbc Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 14 Aug 2023 14:09:42 +0100 Subject: [PATCH 070/103] Add context to CLI Bridge pool cmds --- apps/src/bin/namada-relayer/main.rs | 2 +- apps/src/lib/cli.rs | 131 +++++++++++++----- apps/src/lib/cli/relayer.rs | 76 ++++++---- .../lib/node/ledger/shell/testing/client.rs | 19 ++- 4 files changed, 164 insertions(+), 64 deletions(-) diff --git a/apps/src/bin/namada-relayer/main.rs b/apps/src/bin/namada-relayer/main.rs index 0b314cb9fa..52c15192dc 100644 --- a/apps/src/bin/namada-relayer/main.rs +++ b/apps/src/bin/namada-relayer/main.rs @@ -12,7 +12,7 @@ async fn main() -> Result<()> { // init logging logging::init_from_env_or(LevelFilter::INFO)?; - let (cmd, _) = cli::namada_relayer_cli()?; + let cmd = cli::namada_relayer_cli()?; // run the CLI CliApi::<()>::handle_relayer_command::(None, cmd).await } diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 719b6900a5..08c60ec3b1 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1971,46 +1971,63 @@ pub mod cmds { /// Used as sub-commands (`SubCmd` instance) in `namadar` binary. #[derive(Clone, Debug)] pub enum EthBridgePool { + /// The [`super::Context`] provides access to the wallet and the + /// config. It will generate a new wallet and config, if they + /// don't exist. + WithContext(EthBridgePoolWithCtx), + /// Utils don't have [`super::Context`], only the global arguments. + WithoutContext(EthBridgePoolWithoutCtx), + } + + /// Ethereum Bridge pool commands requiring [`super::Context`]. + #[derive(Clone, Debug)] + pub enum EthBridgePoolWithCtx { /// Get a recommendation on a batch of transfers /// to relay. - RecommendBatch(args::RecommendBatch), + RecommendBatch(RecommendBatch), + } + + /// Ethereum Bridge pool commands not requiring [`super::Context`]. + #[derive(Clone, Debug)] + pub enum EthBridgePoolWithoutCtx { /// Construct a proof that a set of transfers is in the pool. /// This can be used to relay transfers across the /// bridge to Ethereum. - ConstructProof(args::BridgePoolProof), + ConstructProof(ConstructProof), /// Construct and relay a bridge pool proof to /// Ethereum directly. - RelayProof(args::RelayBridgePoolProof), + RelayProof(RelayProof), /// Query the contents of the pool. - QueryPool(args::Query), + QueryPool(QueryEthBridgePool), /// Query to provable contents of the pool. - QuerySigned(args::Query), + QuerySigned(QuerySignedBridgePool), /// Check the confirmation status of `TransferToEthereum` /// events. - QueryRelays(args::Query), + QueryRelays(QueryRelayProgress), } impl Cmd for EthBridgePool { fn add_sub(app: App) -> App { - app.subcommand(ConstructProof::def().display_order(1)) + app.subcommand(RecommendBatch::def().display_order(1)) + .subcommand(ConstructProof::def().display_order(1)) + .subcommand(RelayProof::def().display_order(1)) .subcommand(QueryEthBridgePool::def().display_order(1)) .subcommand(QuerySignedBridgePool::def().display_order(1)) .subcommand(QueryRelayProgress::def().display_order(1)) } fn parse(matches: &ArgMatches) -> Option { - let recommend = RecommendBatch::parse(matches) - .map(|query| Self::RecommendBatch(query.0)); - let construct_proof = ConstructProof::parse(matches) - .map(|proof| Self::ConstructProof(proof.0)); - let relay_proof = RelayProof::parse(matches) - .map(|proof| Self::RelayProof(proof.0)); - let query_pool = QueryEthBridgePool::parse(matches) - .map(|q| Self::QueryPool(q.0)); - let query_signed = QuerySignedBridgePool::parse(matches) - .map(|q| Self::QuerySigned(q.0)); - let query_relays = QueryRelayProgress::parse(matches) - .map(|q| Self::QueryRelays(q.0)); + use EthBridgePoolWithCtx::*; + use EthBridgePoolWithoutCtx::*; + + let recommend = Self::parse_with_ctx(matches, RecommendBatch); + let construct_proof = + Self::parse_without_ctx(matches, ConstructProof); + let relay_proof = Self::parse_without_ctx(matches, RelayProof); + let query_pool = Self::parse_without_ctx(matches, QueryPool); + let query_signed = Self::parse_without_ctx(matches, QuerySigned); + let query_relays = Self::parse_without_ctx(matches, QueryRelays); + construct_proof .or(recommend) .or(relay_proof) @@ -2020,6 +2037,24 @@ pub mod cmds { } } + impl EthBridgePool { + /// A helper method to parse sub cmds with context + fn parse_with_ctx( + matches: &ArgMatches, + sub_to_self: impl Fn(T) -> EthBridgePoolWithCtx, + ) -> Option { + T::parse(matches).map(|sub| Self::WithContext(sub_to_self(sub))) + } + + /// A helper method to parse sub cmds without context + fn parse_without_ctx( + matches: &ArgMatches, + sub_to_self: impl Fn(T) -> EthBridgePoolWithoutCtx, + ) -> Option { + T::parse(matches).map(|sub| Self::WithoutContext(sub_to_self(sub))) + } + } + impl SubCmd for EthBridgePool { const CMD: &'static str = "ethereum-bridge-pool"; @@ -2134,7 +2169,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QueryEthBridgePool(args::Query); + pub struct QueryEthBridgePool(pub args::Query); impl SubCmd for QueryEthBridgePool { const CMD: &'static str = "query"; @@ -2153,7 +2188,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QuerySignedBridgePool(args::Query); + pub struct QuerySignedBridgePool(pub args::Query); impl SubCmd for QuerySignedBridgePool { const CMD: &'static str = "query-signed"; @@ -2175,7 +2210,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QueryRelayProgress(args::Query); + pub struct QueryRelayProgress(pub args::Query); impl SubCmd for QueryRelayProgress { const CMD: &'static str = "query-relayed"; @@ -2199,14 +2234,14 @@ pub mod cmds { /// Query an Ethereum ABI encoding of the consensus validator /// set in Namada, at the given epoch, or the latest /// one, if none is provided. - ConsensusValidatorSet(args::ConsensusValidatorSet), + ConsensusValidatorSet(ConsensusValidatorSet), /// Query an Ethereum ABI encoding of a proof of the consensus /// validator set in Namada, at the given epoch, or the next /// one, if none is provided. - ValidatorSetProof(args::ValidatorSetProof), + ValidatorSetProof(ValidatorSetProof), /// Relay a validator set update to Namada's Ethereum bridge /// smart contracts. - ValidatorSetUpdateRelay(args::ValidatorSetUpdateRelay), + ValidatorSetUpdateRelay(ValidatorSetUpdateRelay), } impl SubCmd for ValidatorSet { @@ -2216,11 +2251,11 @@ pub mod cmds { matches.subcommand_matches(Self::CMD).and_then(|matches| { let consensus_validator_set = ConsensusValidatorSet::parse(matches) - .map(|args| Self::ConsensusValidatorSet(args.0)); + .map(Self::ConsensusValidatorSet); let validator_set_proof = ValidatorSetProof::parse(matches) - .map(|args| Self::ValidatorSetProof(args.0)); + .map(Self::ValidatorSetProof); let relay = ValidatorSetUpdateRelay::parse(matches) - .map(|args| Self::ValidatorSetUpdateRelay(args.0)); + .map(Self::ValidatorSetUpdateRelay); consensus_validator_set.or(validator_set_proof).or(relay) }) } @@ -2241,7 +2276,7 @@ pub mod cmds { #[derive(Clone, Debug)] pub struct ConsensusValidatorSet( - args::ConsensusValidatorSet, + pub args::ConsensusValidatorSet, ); impl SubCmd for ConsensusValidatorSet { @@ -2265,7 +2300,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct ValidatorSetProof(args::ValidatorSetProof); + pub struct ValidatorSetProof(pub args::ValidatorSetProof); impl SubCmd for ValidatorSetProof { const CMD: &'static str = "proof"; @@ -2289,7 +2324,7 @@ pub mod cmds { #[derive(Clone, Debug)] pub struct ValidatorSetUpdateRelay( - args::ValidatorSetUpdateRelay, + pub args::ValidatorSetUpdateRelay, ); impl SubCmd for ValidatorSetUpdateRelay { @@ -5467,9 +5502,39 @@ pub fn namada_wallet_cli() -> Result<(cmds::NamadaWallet, Context)> { cmds::NamadaWallet::parse_or_print_help(app) } -pub fn namada_relayer_cli() -> Result<(cmds::NamadaRelayer, Context)> { +pub enum NamadaRelayer { + EthBridgePoolWithCtx(Box<(cmds::EthBridgePoolWithCtx, Context)>), + EthBridgePoolWithoutCtx(cmds::EthBridgePoolWithoutCtx), + ValidatorSet(cmds::ValidatorSet), +} + +pub fn namada_relayer_cli() -> Result { let app = namada_relayer_app(); - cmds::NamadaRelayer::parse_or_print_help(app) + let matches = app.clone().get_matches(); + match Cmd::parse(&matches) { + Some(cmd) => match cmd { + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithContext(sub_cmd), + ) => { + let global_args = args::Global::parse(&matches); + let context = Context::new(global_args)?; + Ok(NamadaRelayer::EthBridgePoolWithCtx(Box::new(( + sub_cmd, context, + )))) + } + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithoutContext(sub_cmd), + ) => Ok(NamadaRelayer::EthBridgePoolWithoutCtx(sub_cmd)), + cmds::NamadaRelayer::ValidatorSet(sub_cmd) => { + Ok(NamadaRelayer::ValidatorSet(sub_cmd)) + } + }, + None => { + let mut app = app; + app.print_help().unwrap(); + safe_exit(2); + } + } } fn namada_app() -> App { diff --git a/apps/src/lib/cli/relayer.rs b/apps/src/lib/cli/relayer.rs index 531051d27a..242a9ff061 100644 --- a/apps/src/lib/cli/relayer.rs +++ b/apps/src/lib/cli/relayer.rs @@ -5,9 +5,10 @@ use namada::eth_bridge::ethers::providers::{Http, Provider}; use namada::ledger::eth_bridge::{bridge_pool, validator_set}; use namada::types::control_flow::ProceedOrElse; +use crate::cli; use crate::cli::api::{CliApi, CliClient}; -use crate::cli::args::CliToSdkCtxless; -use crate::cli::cmds; +use crate::cli::args::{CliToSdk, CliToSdkCtxless}; +use crate::cli::cmds::*; fn error() -> Report { eyre!("Fatal error") @@ -16,29 +17,38 @@ fn error() -> Report { impl CliApi { pub async fn handle_relayer_command( client: Option, - cmd: cmds::NamadaRelayer, + cmd: cli::NamadaRelayer, ) -> Result<()> where C: CliClient, { match cmd { - cmds::NamadaRelayer::EthBridgePool(sub) => match sub { - cmds::EthBridgePool::RecommendBatch(mut args) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address( - &mut args.query.ledger_address, - ) - }); - client - .wait_until_node_is_synced() - .await - .proceed_or_else(error)?; - let args = args.to_sdk_ctxless(); - bridge_pool::recommend_batch(&client, args) - .await - .proceed_or_else(error)?; + cli::NamadaRelayer::EthBridgePoolWithCtx(boxed) => { + let (sub, mut ctx) = *boxed; + match sub { + EthBridgePoolWithCtx::RecommendBatch(RecommendBatch( + mut args, + )) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.query.ledger_address, + ) + }); + client + .wait_until_node_is_synced() + .await + .proceed_or_else(error)?; + let args = args.to_sdk(&mut ctx); + bridge_pool::recommend_batch(&client, args) + .await + .proceed_or_else(error)?; + } } - cmds::EthBridgePool::ConstructProof(mut args) => { + } + cli::NamadaRelayer::EthBridgePoolWithoutCtx(sub) => match sub { + EthBridgePoolWithoutCtx::ConstructProof(ConstructProof( + mut args, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -53,7 +63,7 @@ impl CliApi { .await .proceed_or_else(error)?; } - cmds::EthBridgePool::RelayProof(mut args) => { + EthBridgePoolWithoutCtx::RelayProof(RelayProof(mut args)) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -74,7 +84,9 @@ impl CliApi { .await .proceed_or_else(error)?; } - cmds::EthBridgePool::QueryPool(mut query) => { + EthBridgePoolWithoutCtx::QueryPool(QueryEthBridgePool( + mut query, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address(&mut query.ledger_address) }); @@ -84,7 +96,9 @@ impl CliApi { .proceed_or_else(error)?; bridge_pool::query_bridge_pool(&client).await; } - cmds::EthBridgePool::QuerySigned(mut query) => { + EthBridgePoolWithoutCtx::QuerySigned( + QuerySignedBridgePool(mut query), + ) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address(&mut query.ledger_address) }); @@ -96,7 +110,9 @@ impl CliApi { .await .proceed_or_else(error)?; } - cmds::EthBridgePool::QueryRelays(mut query) => { + EthBridgePoolWithoutCtx::QueryRelays(QueryRelayProgress( + mut query, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address(&mut query.ledger_address) }); @@ -107,8 +123,10 @@ impl CliApi { bridge_pool::query_relay_progress(&client).await; } }, - cmds::NamadaRelayer::ValidatorSet(sub) => match sub { - cmds::ValidatorSet::ConsensusValidatorSet(mut args) => { + cli::NamadaRelayer::ValidatorSet(sub) => match sub { + ValidatorSet::ConsensusValidatorSet(ConsensusValidatorSet( + mut args, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -122,7 +140,9 @@ impl CliApi { validator_set::query_validator_set_args(&client, args) .await; } - cmds::ValidatorSet::ValidatorSetProof(mut args) => { + ValidatorSet::ValidatorSetProof(ValidatorSetProof( + mut args, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -138,7 +158,9 @@ impl CliApi { ) .await; } - cmds::ValidatorSet::ValidatorSetUpdateRelay(mut args) => { + ValidatorSet::ValidatorSetUpdateRelay( + ValidatorSetUpdateRelay(mut args), + ) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, diff --git a/apps/src/lib/node/ledger/shell/testing/client.rs b/apps/src/lib/node/ledger/shell/testing/client.rs index 164e4a03ea..c07735d0b9 100644 --- a/apps/src/lib/node/ledger/shell/testing/client.rs +++ b/apps/src/lib/node/ledger/shell/testing/client.rs @@ -8,7 +8,7 @@ use tendermint_config::net::Address as TendermintAddress; use super::node::MockNode; use crate::cli::api::{CliApi, CliClient}; use crate::cli::args::Global; -use crate::cli::{args, cmds, Cmd, Context, NamadaClient}; +use crate::cli::{args, cmds, Cmd, Context, NamadaClient, NamadaRelayer}; use crate::node::ledger::shell::testing::utils::Bin; pub fn run( @@ -63,8 +63,21 @@ pub fn run( let app = App::new("test"); let app = cmds::NamadaRelayer::add_sub(args::Global::def(app)); let matches = app.get_matches_from(args.clone()); - let cmd = cmds::NamadaRelayer::parse(&matches) - .expect("Could not parse wallet command"); + let cmd = match cmds::NamadaRelayer::parse(&matches) + .expect("Could not parse relayer command") + { + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithContext(sub_cmd), + ) => NamadaRelayer::EthBridgePoolWithCtx(Box::new(( + sub_cmd, ctx, + ))), + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithoutContext(sub_cmd), + ) => NamadaRelayer::EthBridgePoolWithoutCtx(sub_cmd), + cmds::NamadaRelayer::ValidatorSet(sub_cmd) => { + NamadaRelayer::ValidatorSet(sub_cmd) + } + }; rt.block_on(CliApi::<()>::handle_relayer_command(Some(node), cmd)) } } From 5898fb37b593d9b8bf7270e24c391b31e033d4ed Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 10:19:31 +0100 Subject: [PATCH 071/103] Refactor recommendations to use EligibleRecommendation --- shared/src/ledger/eth_bridge/bridge_pool.rs | 47 +++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index ae3186a668..ed47df5d1d 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -486,6 +486,22 @@ mod recommendations { Generous, } + /// Transfer to Ethereum that is eligible to be recommended + /// for a relay operation, generating a profit. + /// + /// This means that the underlying Ethereum event has not + /// been "seen" yet, and that the user provided appropriate + /// conversion rates to gwei for the gas fee token in + /// the transfer. + struct EligibleRecommendation { + /// Pending transfer to Ethereum. + pending_transfer: PendingTransfer, + /// Hash of the [`PendingTransfer`]. + transfer_hash: String, + /// Cost of relaying the transfer, in gwei. + cost: I256, + } + /// Recommend the most economical batch of transfers to relay based /// on a conversion rate estimates from NAM to ETH and gas usage /// heuristics. @@ -542,7 +558,7 @@ mod recommendations { + valset_fee() * valset_size; // we don't recommend transfers that have already been relayed - let mut contents: Vec<(String, I256, PendingTransfer)> = + let mut contents: Vec = query_signed_bridge_pool(client) .await? .into_iter() @@ -583,12 +599,16 @@ mod recommendations { } }) .try_fold(Vec::new(), |mut accum, (hash, cost, transf)| { - accum.push((hash, cost?, transf)); + accum.push(EligibleRecommendation { + cost: cost?, + transfer_hash: hash, + pending_transfer: transf, + }); control_flow::proceed(accum) })?; // sort transfers in decreasing amounts of profitability - contents.sort_by_key(|(_, cost, _)| *cost); + contents.sort_by_key(|EligibleRecommendation { cost, .. }| *cost); let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); @@ -641,7 +661,7 @@ mod recommendations { /// Generates the actual recommendation from restrictions given by the /// input parameters. fn generate( - contents: Vec<(String, I256, PendingTransfer)>, + contents: Vec, conversion_table: &HashMap, validator_gas: Uint, max_gas: Uint, @@ -664,7 +684,12 @@ mod recommendations { })?; let mut total_fees = HashMap::new(); let mut recommendation = vec![]; - for (hash, cost, transfer) in contents.into_iter() { + for EligibleRecommendation { + cost, + transfer_hash: hash, + pending_transfer: transfer, + } in contents.into_iter() + { let next_total_gas = total_gas + unsigned_transfer_fee(); let next_total_cost = total_cost + cost; if cost.is_negative() { @@ -770,15 +795,13 @@ mod recommendations { /// understands. fn process_transfers( transfers: Vec, - ) -> Vec<(String, I256, PendingTransfer)> { + ) -> Vec { transfers .into_iter() - .map(|t| { - ( - t.keccak256().to_string(), - transfer_fee() - t.gas_fee.amount.change(), - t, - ) + .map(|t| EligibleRecommendation { + cost: transfer_fee() - t.gas_fee.amount.change(), + transfer_hash: t.keccak256().to_string(), + pending_transfer: t, }) .collect() } From 4eae7389e25ccc67599754cef2b7bc26f6cbbff1 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 10:36:34 +0100 Subject: [PATCH 072/103] Rename `generate` to `generate_recommendations` --- shared/src/ledger/eth_bridge/bridge_pool.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index ed47df5d1d..6836662469 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -613,7 +613,7 @@ mod recommendations { let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); let max_cost = args.gas.map(I256::from).unwrap_or_default(); - generate( + generate_recommendations( contents, &args.conversion_table, validator_gas, @@ -660,7 +660,7 @@ mod recommendations { /// Generates the actual recommendation from restrictions given by the /// input parameters. - fn generate( + fn generate_recommendations( contents: Vec, conversion_table: &HashMap, validator_gas: Uint, @@ -791,8 +791,8 @@ mod recommendations { } } - /// Convert transfers into a format that the `generate` function - /// understands. + /// Convert transfers into a format that the + /// [`generate_recommendations`] function understands. fn process_transfers( transfers: Vec, ) -> Vec { @@ -851,7 +851,7 @@ mod recommendations { let profitable = vec![transfer(100_000); 17]; let hash = profitable[0].keccak256().to_string(); let expected = vec![hash; 17]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(profitable), &Default::default(), Uint::from_u64(800_000), @@ -869,7 +869,7 @@ mod recommendations { let hash = transfers[0].keccak256().to_string(); transfers.push(transfer(0)); let expected: Vec<_> = vec![hash; 17]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(800_000), @@ -886,7 +886,7 @@ mod recommendations { let transfers = vec![transfer(75_000); 4]; let hash = transfers[0].keccak256().to_string(); let expected = vec![hash; 2]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(50_000), @@ -907,7 +907,7 @@ mod recommendations { .map(|t| t.keccak256().to_string()) .take(5) .collect(); - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(150_000), @@ -925,7 +925,7 @@ mod recommendations { let hash = transfers[0].keccak256().to_string(); let expected = vec![hash; 4]; transfers.extend([transfer(17_500), transfer(17_500)]); - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(150_000), @@ -940,7 +940,7 @@ mod recommendations { #[test] fn test_wholly_infeasible() { let transfers = vec![transfer(75_000); 4]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), &Default::default(), Uint::from_u64(300_000), From 0fc3e5dc935f9f0ca9e7a24a2a27676fa63f3fdc Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 10:42:32 +0100 Subject: [PATCH 073/103] Fix Bridge pool proof warning voting power --- shared/src/ledger/eth_bridge/bridge_pool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 6836662469..5119fa4e2c 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -210,7 +210,7 @@ where let warnings: Vec<_> = in_progress .into_iter() .filter_map(|(ref transfer, voting_power)| { - if voting_power > FractionalVotingPower::ONE_THIRD { + if voting_power >= FractionalVotingPower::ONE_THIRD { let hash = transfer.keccak256(); args.transfers.contains(&hash).then_some(hash) } else { From ed3b5a3a75800b625a629b3efbc5c6cd13b338b7 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 11:49:30 +0100 Subject: [PATCH 074/103] Factor out `generate_eligible()` --- shared/src/ledger/eth_bridge/bridge_pool.rs | 124 +++++++++++--------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 5119fa4e2c..7302dbbb14 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -436,6 +436,8 @@ where } mod recommendations { + use std::collections::BTreeSet; + use borsh::BorshDeserialize; use namada_core::types::uint::{self, Uint, I256}; @@ -521,7 +523,8 @@ mod recommendations { .await .unwrap() .into_keys() - .collect::>(); + .map(|pending| pending.keccak256().to_string()) + .collect::>(); // get the signed bridge pool root so we can analyze the signatures // the estimate the gas cost of verifying them. @@ -558,63 +561,17 @@ mod recommendations { + valset_fee() * valset_size; // we don't recommend transfers that have already been relayed - let mut contents: Vec = - query_signed_bridge_pool(client) - .await? - .into_iter() - .filter_map(|(pending_hash, pending)| { - if !in_progress.contains(&pending) { - let conversion_rate = - if let Some(entry) = args - .conversion_table - .get(&pending.gas_fee.token) - { - let rate = entry.conversion_rate; - if rate <= 0.0f64 { - eprintln!( - "Ignoring token with an invalid conversion rate: {}", - pending.gas_fee.token, - ); - return None; - } - rate - } else { - return None; - }; - // This is the amount of gwei a single gas token is worth - let gwei_per_gas_token = Uint::from_u64( - (10u64.pow(9) as f64 / conversion_rate).floor() as u64, - ); - Some(( - pending_hash, - I256::try_from(pending.gas_fee.amount * gwei_per_gas_token) - .map(|cost| transfer_fee() - cost) - .try_halt(|err| { - tracing::debug!(%err, "Failed to convert value to I256"); - }), - pending, - )) - } else { - None - } - }) - .try_fold(Vec::new(), |mut accum, (hash, cost, transf)| { - accum.push(EligibleRecommendation { - cost: cost?, - transfer_hash: hash, - pending_transfer: transf, - }); - control_flow::proceed(accum) - })?; - - // sort transfers in decreasing amounts of profitability - contents.sort_by_key(|EligibleRecommendation { cost, .. }| *cost); + let eligible = generate_eligible( + &args.conversion_table, + &in_progress, + query_signed_bridge_pool(client).await?, + )?; let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); let max_cost = args.gas.map(I256::from).unwrap_or_default(); generate_recommendations( - contents, + eligible, &args.conversion_table, validator_gas, max_gas, @@ -658,6 +615,67 @@ mod recommendations { ) } + /// Generate eligible recommendations. + fn generate_eligible( + conversion_table: &HashMap, + in_progress: &BTreeSet, + signed_pool: HashMap, + ) -> Halt> { + let mut eligible: Vec<_> = signed_pool + .into_iter() + .filter_map(|(pending_hash, pending)| { + if in_progress.contains(&pending_hash) { + return None; + } + + let conversion_rate = if let Some(entry) = + conversion_table.get(&pending.gas_fee.token) + { + let rate = entry.conversion_rate; + if rate <= 0.0f64 { + eprintln!( + "Ignoring token with an invalid conversion rate: \ + {}", + pending.gas_fee.token, + ); + return None; + } + rate + } else { + return None; + }; + + // This is the amount of gwei a single gas token is worth + let gwei_per_gas_token = Uint::from_u64( + (10u64.pow(9) as f64 / conversion_rate).floor() as u64, + ); + + Some( + I256::try_from(pending.gas_fee.amount * gwei_per_gas_token) + .map_err(|err| err.to_string()) + .and_then(|cost| { + transfer_fee().checked_sub(&cost).ok_or_else(|| { + "Underflowed calculating relaying cost".into() + }) + }) + .map(|cost| EligibleRecommendation { + cost, + pending_transfer: pending, + transfer_hash: pending_hash, + }), + ) + }) + .collect::, _>>() + .try_halt(|err| { + tracing::debug!(%err, "Failed to calculate relaying cost"); + })?; + + // sort transfers in increasing amounts of profitability + eligible.sort_by_key(|EligibleRecommendation { cost, .. }| *cost); + + control_flow::proceed(eligible) + } + /// Generates the actual recommendation from restrictions given by the /// input parameters. fn generate_recommendations( From fb89bff2fab0cebd9e759018fbc942b9a2c687d6 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 11:59:46 +0100 Subject: [PATCH 075/103] Improve conversion rate query --- shared/src/ledger/eth_bridge/bridge_pool.rs | 30 ++++++++++----------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 7302dbbb14..4b0fdd57c7 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -628,22 +628,20 @@ mod recommendations { return None; } - let conversion_rate = if let Some(entry) = - conversion_table.get(&pending.gas_fee.token) - { - let rate = entry.conversion_rate; - if rate <= 0.0f64 { - eprintln!( - "Ignoring token with an invalid conversion rate: \ - {}", - pending.gas_fee.token, - ); - return None; - } - rate - } else { - return None; - }; + let conversion_rate = conversion_table + .get(&pending.gas_fee.token) + .and_then(|entry| { + let rate = entry.conversion_rate; + if rate <= 0.0f64 { + eprintln!( + "Ignoring token with an invalid conversion \ + rate: {}", + pending.gas_fee.token, + ); + return None; + } + Some(rate) + })?; // This is the amount of gwei a single gas token is worth let gwei_per_gas_token = Uint::from_u64( From ad57fabb7115b371a50fcbf5ae7ad6d8cd5163a8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 16:09:53 +0100 Subject: [PATCH 076/103] Avoid overflowing when calculating earned gwei amount --- shared/src/ledger/eth_bridge/bridge_pool.rs | 23 ++++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 4b0fdd57c7..f6b13e7752 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -644,17 +644,24 @@ mod recommendations { })?; // This is the amount of gwei a single gas token is worth - let gwei_per_gas_token = Uint::from_u64( - (10u64.pow(9) as f64 / conversion_rate).floor() as u64, - ); + let gwei_per_gas_token = + Uint::from_u64((1e9 / conversion_rate).floor() as u64); Some( - I256::try_from(pending.gas_fee.amount * gwei_per_gas_token) + Uint::from(pending.gas_fee.amount) + .checked_mul(gwei_per_gas_token) + .ok_or_else(|| { + "Overflowed calculating earned gwei".into() + }) + .and_then(I256::try_from) .map_err(|err| err.to_string()) - .and_then(|cost| { - transfer_fee().checked_sub(&cost).ok_or_else(|| { - "Underflowed calculating relaying cost".into() - }) + .and_then(|amt_of_earned_gwei| { + transfer_fee() + .checked_sub(&amt_of_earned_gwei) + .ok_or_else(|| { + "Underflowed calculating relaying cost" + .into() + }) }) .map(|cost| EligibleRecommendation { cost, From 3701ff71845121dd0b5768d36b31a35f1da168ec Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 16:10:39 +0100 Subject: [PATCH 077/103] Add test_generate_eligible_happy_path() unit test --- shared/src/ledger/eth_bridge/bridge_pool.rs | 48 +++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index f6b13e7752..37adf25bd5 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -495,6 +495,7 @@ mod recommendations { /// been "seen" yet, and that the user provided appropriate /// conversion rates to gwei for the gas fee token in /// the transfer. + #[derive(Debug, Eq, PartialEq)] struct EligibleRecommendation { /// Pending transfer to Ethereum. pending_transfer: PendingTransfer, @@ -836,6 +837,53 @@ mod recommendations { } } + /// Test the happy path of generating eligible recommendations + /// for Bridge pool relayed transfers. + #[test] + fn test_generate_eligible_happy_path() { + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: EthAddress([1; 20]), + recipient: EthAddress([2; 20]), + sender: bertha_address(), + amount: Default::default(), + }, + gas_fee: GasFee { + token: namada_core::types::address::eth(), + amount: 1_000_000_000_u64.into(), // 1 GWEI + payer: bertha_address(), + }, + }; + let conversion_table = { + let mut table = HashMap::new(); + table.insert( + namada_core::types::address::eth(), + args::BpConversionTableEntry { + alias: "ETH".into(), + conversion_rate: 1e9, // 1 ETH = 1e9 GWEI + }, + ); + table + }; + let in_progress = BTreeSet::new(); + let signed_pool = HashMap::from([( + pending.keccak256().to_string(), + pending.clone(), + )]); + let eligible = + generate_eligible(&conversion_table, &in_progress, signed_pool) + .proceed(); + let expected = vec![EligibleRecommendation { + transfer_hash: pending.keccak256().to_string(), + cost: transfer_fee() + - I256::try_from(pending.gas_fee.amount) + .expect("Test failed"), + pending_transfer: pending, + }]; + assert_eq!(eligible, expected); + } + #[test] fn test_signature_count() { let voting_powers = VotingPowersMap::from([ From bf1a3beee6b8af77f65f4e0f8db9af01ecad596c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 16:20:50 +0100 Subject: [PATCH 078/103] Abstract `generate_eligible` unit tests with a helper fn --- shared/src/ledger/eth_bridge/bridge_pool.rs | 58 +++++++++++++++------ 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 37adf25bd5..6c7f1933a1 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -837,10 +837,20 @@ mod recommendations { } } - /// Test the happy path of generating eligible recommendations - /// for Bridge pool relayed transfers. - #[test] - fn test_generate_eligible_happy_path() { + /// Data to pass to the [`test_generate_eligible_aux`] callback. + struct TestGenerateEligible<'a> { + pending: &'a PendingTransfer, + #[allow(dead_code)] + in_progress: &'a mut BTreeSet, + signed_pool: &'a mut HashMap, + expected_eligible: &'a mut Vec, + } + + /// Helper function to test [`generate_eligible`]. + fn test_generate_eligible_aux(mut callback: F) + where + F: FnMut(TestGenerateEligible<'_>), + { let pending = PendingTransfer { transfer: TransferToEthereum { kind: TransferToEthereumKind::Erc20, @@ -866,24 +876,40 @@ mod recommendations { ); table }; - let in_progress = BTreeSet::new(); - let signed_pool = HashMap::from([( - pending.keccak256().to_string(), - pending.clone(), - )]); + let mut in_progress = BTreeSet::new(); + let mut signed_pool = HashMap::new(); + let mut expected = vec![]; + callback(TestGenerateEligible { + pending: &pending, + in_progress: &mut in_progress, + signed_pool: &mut signed_pool, + expected_eligible: &mut expected, + }); let eligible = generate_eligible(&conversion_table, &in_progress, signed_pool) .proceed(); - let expected = vec![EligibleRecommendation { - transfer_hash: pending.keccak256().to_string(), - cost: transfer_fee() - - I256::try_from(pending.gas_fee.amount) - .expect("Test failed"), - pending_transfer: pending, - }]; assert_eq!(eligible, expected); } + /// Test the happy path of generating eligible recommendations + /// for Bridge pool relayed transfers. + #[test] + fn test_generate_eligible_happy_path() { + test_generate_eligible_aux(|ctx| { + ctx.signed_pool.insert( + ctx.pending.keccak256().to_string(), + ctx.pending.clone(), + ); + ctx.expected_eligible.push(EligibleRecommendation { + transfer_hash: ctx.pending.keccak256().to_string(), + cost: transfer_fee() + - I256::try_from(ctx.pending.gas_fee.amount) + .expect("Test failed"), + pending_transfer: ctx.pending.clone(), + }); + }); + } + #[test] fn test_signature_count() { let voting_powers = VotingPowersMap::from([ From e7fa5eea8c8741be1dc2976ad40f40cd09c0fb03 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 16:46:31 +0100 Subject: [PATCH 079/103] More conversion table unit tests --- shared/src/ledger/eth_bridge/bridge_pool.rs | 62 ++++++++++++++++----- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 6c7f1933a1..caadf38a9e 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -840,12 +840,26 @@ mod recommendations { /// Data to pass to the [`test_generate_eligible_aux`] callback. struct TestGenerateEligible<'a> { pending: &'a PendingTransfer, - #[allow(dead_code)] + conversion_table: + &'a mut HashMap, in_progress: &'a mut BTreeSet, signed_pool: &'a mut HashMap, expected_eligible: &'a mut Vec, } + impl TestGenerateEligible<'_> { + /// Add ETH to a conversion table. + fn add_eth_to_conversion_table(&mut self) { + self.conversion_table.insert( + namada_core::types::address::eth(), + args::BpConversionTableEntry { + alias: "ETH".into(), + conversion_rate: 1e9, // 1 ETH = 1e9 GWEI + }, + ); + } + } + /// Helper function to test [`generate_eligible`]. fn test_generate_eligible_aux(mut callback: F) where @@ -865,29 +879,19 @@ mod recommendations { payer: bertha_address(), }, }; - let conversion_table = { - let mut table = HashMap::new(); - table.insert( - namada_core::types::address::eth(), - args::BpConversionTableEntry { - alias: "ETH".into(), - conversion_rate: 1e9, // 1 ETH = 1e9 GWEI - }, - ); - table - }; + let mut table = HashMap::new(); let mut in_progress = BTreeSet::new(); let mut signed_pool = HashMap::new(); let mut expected = vec![]; callback(TestGenerateEligible { pending: &pending, + conversion_table: &mut table, in_progress: &mut in_progress, signed_pool: &mut signed_pool, expected_eligible: &mut expected, }); let eligible = - generate_eligible(&conversion_table, &in_progress, signed_pool) - .proceed(); + generate_eligible(&table, &in_progress, signed_pool).proceed(); assert_eq!(eligible, expected); } @@ -895,7 +899,8 @@ mod recommendations { /// for Bridge pool relayed transfers. #[test] fn test_generate_eligible_happy_path() { - test_generate_eligible_aux(|ctx| { + test_generate_eligible_aux(|mut ctx| { + ctx.add_eth_to_conversion_table(); ctx.signed_pool.insert( ctx.pending.keccak256().to_string(), ctx.pending.clone(), @@ -910,6 +915,33 @@ mod recommendations { }); } + /// Test that a transfer is not recommended if it + /// is in the process of being relayed (has >0 voting + /// power behind it). + #[test] + fn test_generate_eligible_with_in_progress() { + test_generate_eligible_aux(|mut ctx| { + ctx.add_eth_to_conversion_table(); + ctx.signed_pool.insert( + ctx.pending.keccak256().to_string(), + ctx.pending.clone(), + ); + ctx.in_progress.insert(ctx.pending.keccak256().to_string()); + }); + } + + /// Test that a transfer is not recommended if its gas + /// token is not found in the conversion table. + #[test] + fn test_generate_eligible_no_gas_token() { + test_generate_eligible_aux(|ctx| { + ctx.signed_pool.insert( + ctx.pending.keccak256().to_string(), + ctx.pending.clone(), + ); + }); + } + #[test] fn test_signature_count() { let voting_powers = VotingPowersMap::from([ From 1b0dfee78bcb46bf5b14114734576e6bdc7bb189 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 16 Aug 2023 20:35:04 +0100 Subject: [PATCH 080/103] Prohibit generating BP proofs when the signed root is outdated --- shared/src/ledger/queries/shell/eth_bridge.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index bd9115f48a..2588ef33bd 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -342,6 +342,21 @@ where )) .into_storage_result()?; + // make sure a relay attempt won't happen before the new signed + // root has had time to be generated + let latest_bp_nonce = + ctx.wl_storage.ethbridge_queries().get_bridge_pool_nonce(); + if latest_bp_nonce != signed_root.data.1 { + return Err(storage_api::Error::Custom(CustomError( + format!( + "Mismatch between the nonce in the Bridge pool root proof \ + ({}) and the latest Bridge pool nonce in storage ({})", + signed_root.data.1, latest_bp_nonce, + ) + .into(), + ))); + } + // get the merkle tree corresponding to the above root. let tree = ctx .wl_storage From a0f5852ab12efaa02060bb8a6fb6d1dc4044b24d Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 17 Aug 2023 08:57:03 +0100 Subject: [PATCH 081/103] Fail recommending BP batch of txs if nonce is out of date --- shared/src/ledger/eth_bridge/bridge_pool.rs | 37 ++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index caadf38a9e..97e28eb3b2 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -442,8 +442,11 @@ mod recommendations { use namada_core::types::uint::{self, Uint, I256}; use super::*; - use crate::eth_bridge::storage::bridge_pool::get_signed_root_key; + use crate::eth_bridge::storage::bridge_pool::{ + get_nonce_key, get_signed_root_key, + }; use crate::eth_bridge::storage::proof::BridgePoolRootProof; + use crate::types::ethereum_events::Uint as EthUint; use crate::types::storage::BlockHeight; use crate::types::vote_extensions::validator_set_update::{ EthAddrBook, VotingPowersMap, VotingPowersMapExt, @@ -535,15 +538,41 @@ mod recommendations { .storage_value( client, None, - Some(0.into()), + None, false, &get_signed_root_key(), ) .await - .unwrap() + .try_halt(|err| { + eprintln!("Failed to query Bridge pool proof: {err}"); + })? .data, ) - .unwrap(); + .try_halt(|err| { + eprintln!("Failed to decode Bridge pool proof: {err}"); + })?; + + // get the latest bridge pool nonce + let latest_bp_nonce = EthUint::try_from_slice( + &RPC.shell() + .storage_value(client, None, None, false, &get_nonce_key()) + .await + .try_halt(|err| { + eprintln!("Failed to query Bridge pool nonce: {err}"); + })? + .data, + ) + .try_halt(|err| { + eprintln!("Failed to decode Bridge pool nonce: {err}"); + })?; + + if latest_bp_nonce != bp_root.data.1 { + eprintln!( + "The signed Bridge pool nonce is not up to date, repeat this \ + query at a later time" + ); + return control_flow::halt(); + } // Get the voting powers of each of validator who signed // the above root. From 371b1feb256a6c1c0981f11bd8da5b69cec2a0a5 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 17 Aug 2023 10:35:49 +0100 Subject: [PATCH 082/103] Store height when BP root was signed --- ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs | 3 ++- ethereum_bridge/src/storage/eth_bridge_queries.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs index 7262c20a31..b512798ce8 100644 --- a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs +++ b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs @@ -45,6 +45,7 @@ where bridge pool root and nonce." ); let voting_powers = utils::get_voting_powers(wl_storage, &vext)?; + let root_height = vext.iter().next().unwrap().data.block_height; let (partial_proof, seen_by) = parse_vexts(wl_storage, vext); // apply updates to the bridge pool root. @@ -65,7 +66,7 @@ where wl_storage .write_bytes( &get_signed_root_key(), - (proof, wl_storage.storage.get_last_block_height()) + (proof, root_height) .try_to_vec() .expect("Serializing a Bridge pool root shouldn't fail."), ) diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index c30de9a536..5d068d2e38 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -220,7 +220,7 @@ where /// root and nonce. /// /// Also returns the block height at which the - /// a quorum of signatures was collected. + /// Bridge pool root was originally signed. /// /// No value exists when the bridge if first /// started. From 7b29efd4d24e939873dd400e8e9951e321b7e346 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 17 Aug 2023 10:36:41 +0100 Subject: [PATCH 083/103] Check BP root's height when vote extension was signed --- .../shell/vote_extensions/bridge_pool_vext.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs index edd83cdb3a..4decd44127 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs @@ -134,19 +134,16 @@ where VoteExtensionError::VerifySigFailed })?; - let bp_root = if cfg!(feature = "abcipp") { - self.wl_storage.ethbridge_queries().get_bridge_pool_root().0 - } else { - self.wl_storage - .ethbridge_queries() - .get_bridge_pool_root_at_height(ext.data.block_height) - .expect("We asserted that the queried height is correct") - .0 - }; + let bp_root = self + .wl_storage + .ethbridge_queries() + .get_bridge_pool_root_at_height(ext.data.block_height) + .expect("We asserted that the queried height is correct") + .0; let nonce = self .wl_storage .ethbridge_queries() - .get_bridge_pool_nonce() + .get_bridge_pool_nonce_at_height(ext.data.block_height) .to_bytes(); let signed = Signed::<_, SignableEthMessage>::new_from( keccak_hash([bp_root, nonce].concat()), From 00a859ac3ce742f1821edefd6875f2190d3ac474 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 17 Aug 2023 14:08:04 +0100 Subject: [PATCH 084/103] Test the height of the BP proof stored in the db --- .../transactions/bridge_pool_roots.rs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs index b512798ce8..8571881453 100644 --- a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs +++ b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs @@ -196,11 +196,17 @@ mod test_apply_bp_roots_to_storage { use namada_core::ledger::storage_api::StorageRead; use namada_core::proto::{SignableEthMessage, Signed}; use namada_core::types::address; + use namada_core::types::dec::Dec; use namada_core::types::ethereum_events::Uint; use namada_core::types::keccak::{keccak_hash, KeccakHash}; + use namada_core::types::key::RefTo; use namada_core::types::storage::Key; use namada_core::types::token::Amount; use namada_core::types::vote_extensions::bridge_pool_roots; + use namada_proof_of_stake::parameters::PosParams; + use namada_proof_of_stake::{ + become_validator, bond_tokens, write_pos_params, BecomeValidator, + }; use super::*; use crate::protocol::transactions::votes::{ @@ -688,4 +694,127 @@ mod test_apply_bp_roots_to_storage { assert_eq!(proof.signatures, expected.signatures); assert_eq!(proof.data, expected.data); } + + /// Test that when we acquire a complete BP roots proof, + /// the block height stored in storage is that of the + /// tree root that was decided. + #[test] + fn test_bp_roots_across_epoch_boundaries() { + // the validators that will vote in the tally + let validator_1 = address::testing::established_address_1(); + let validator_1_stake = Amount::native_whole(100); + + let validator_2 = address::testing::established_address_2(); + let validator_2_stake = Amount::native_whole(100); + + let validator_3 = address::testing::established_address_3(); + let validator_3_stake = Amount::native_whole(100); + + // start epoch 0 with validator 1 + let (mut wl_storage, keys) = test_utils::setup_storage_with_validators( + HashMap::from([(validator_1.clone(), validator_1_stake)]), + ); + + // update the pos params + let params = PosParams { + pipeline_len: 1, + ..Default::default() + }; + write_pos_params(&mut wl_storage, params.clone()).expect("Test failed"); + + // insert validators 2 and 3 at epoch 1 + for (validator, stake) in [ + (&validator_2, validator_2_stake), + (&validator_3, validator_3_stake), + ] { + let keys = test_utils::TestValidatorKeys::generate(); + let consensus_key = &keys.consensus.ref_to(); + let eth_cold_key = &keys.eth_gov.ref_to(); + let eth_hot_key = &keys.eth_bridge.ref_to(); + become_validator(BecomeValidator { + storage: &mut wl_storage, + params: ¶ms, + address: validator, + consensus_key, + eth_cold_key, + eth_hot_key, + current_epoch: 0.into(), + commission_rate: Dec::new(5, 2).unwrap(), + max_commission_rate_change: Dec::new(1, 2).unwrap(), + }) + .expect("Test failed"); + bond_tokens(&mut wl_storage, None, validator, stake, 0.into()) + .expect("Test failed"); + } + + // query validators to make sure they were inserted correctly + macro_rules! query_validators { + () => { + |epoch: u64| { + wl_storage + .pos_queries() + .get_consensus_validators(Some(epoch.into())) + .iter() + .map(|validator| { + (validator.address, validator.bonded_stake) + }) + .collect::>() + } + }; + } + let query_validators = query_validators!(); + let epoch_0_validators = query_validators(0); + let epoch_1_validators = query_validators(1); + _ = query_validators; + assert_eq!( + epoch_0_validators, + HashMap::from([(validator_1.clone(), validator_1_stake)]) + ); + assert_eq!( + epoch_1_validators, + HashMap::from([ + (validator_1.clone(), validator_1_stake), + (validator_2, validator_2_stake), + (validator_3, validator_3_stake), + ]) + ); + + // set up the bridge pool's storage + bridge_pool_vp::init_storage(&mut wl_storage); + test_utils::commit_bridge_pool_root_at_height( + &mut wl_storage.storage, + &KeccakHash([1; 32]), + 3.into(), + ); + + // construct proof + let root = wl_storage.ethbridge_queries().get_bridge_pool_root(); + let nonce = wl_storage.ethbridge_queries().get_bridge_pool_nonce(); + let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); + let hot_key = &keys[&validator_1].eth_bridge; + let vext = bridge_pool_roots::Vext { + validator_addr: validator_1.clone(), + block_height: 3.into(), + sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, + } + .sign(&keys[&validator_1].protocol); + + _ = apply_derived_tx(&mut wl_storage, vext.into()) + .expect("Test failed"); + + // query validator set of the proof + // (should be the one from epoch 0) + let (_, root_height) = wl_storage + .ethbridge_queries() + .get_signed_bridge_pool_root() + .expect("Test failed"); + let root_epoch = wl_storage + .pos_queries() + .get_epoch(root_height) + .expect("Test failed"); + + let query_validators = query_validators!(); + let root_epoch_validators = query_validators(root_epoch.0); + assert_eq!(epoch_0_validators, root_epoch_validators); + } } From ac5d3acf5cd92e9cb1cee11545a263a8dcb519a1 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 18 Aug 2023 09:07:14 +0100 Subject: [PATCH 085/103] Return recommendation data in `generate_recommendations` --- shared/src/ledger/eth_bridge/bridge_pool.rs | 77 ++++++++++++++++----- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 97e28eb3b2..0f3f5d3597 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -508,6 +508,23 @@ mod recommendations { cost: I256, } + /// Batch of recommended transfers to Ethereum that generate + /// a profit after a relay operation. + #[derive(Debug, Eq, PartialEq)] + struct RecommendedBatch { + /// Hashes of the recommended transfers to be relayed. + transfer_hashes: Vec, + /// Estimate of the total fees, measured in gwei, that will be paid + /// on Ethereum. + ethereum_gas_fees: Uint, + /// Net profitt in gwei, based on the conversion rates provided + /// to the algorithm. + net_profit: I256, + /// Gas fees paid by the transfers considered for relaying, + /// paid in various token types. + bridge_pool_gas_fees: HashMap, + } + /// Recommend the most economical batch of transfers to relay based /// on a conversion rate estimates from NAM to ETH and gas usage /// heuristics. @@ -600,13 +617,36 @@ mod recommendations { let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); let max_cost = args.gas.map(I256::from).unwrap_or_default(); + generate_recommendations( eligible, &args.conversion_table, validator_gas, max_gas, max_cost, - )?; + )? + .map( + |RecommendedBatch { + transfer_hashes, + ethereum_gas_fees, + net_profit, + bridge_pool_gas_fees, + }| { + println!("Recommended batch: {transfer_hashes:#?}"); + println!( + "Estimated Ethereum transaction gas (in gwei): \ + {ethereum_gas_fees}", + ); + println!("Estimated net profit (in gwei): {net_profit}"); + println!("Total fees: {bridge_pool_gas_fees:#?}"); + }, + ) + .unwrap_or_else(|| { + println!( + "Unable to find a recommendation satisfying the input \ + parameters." + ); + }); control_flow::proceed(()) } @@ -719,7 +759,7 @@ mod recommendations { validator_gas: Uint, max_gas: Uint, max_cost: I256, - ) -> Halt>> { + ) -> Halt> { let mut state = AlgorithState { profitable: true, feasible_region: false, @@ -774,19 +814,13 @@ mod recommendations { control_flow::proceed( if state.feasible_region && !recommendation.is_empty() { - println!("Recommended batch: {:#?}", recommendation); - println!( - "Estimated Ethereum transaction gas (in gwei): {}", - total_gas - ); - println!("Estimated net profit (in gwei): {}", -total_cost); - println!("Total fees: {total_fees:#?}"); - Some(recommendation) + Some(RecommendedBatch { + transfer_hashes: recommendation, + ethereum_gas_fees: total_gas, + net_profit: -total_cost, + bridge_pool_gas_fees: total_fees, + }) } else { - println!( - "Unable to find a recommendation satisfying the input \ - parameters." - ); None }, ) @@ -1017,7 +1051,8 @@ mod recommendations { I256::zero(), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -1035,7 +1070,8 @@ mod recommendations { I256::zero(), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -1052,7 +1088,8 @@ mod recommendations { I256(uint::MAX_SIGNED_VALUE), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -1073,7 +1110,8 @@ mod recommendations { I256::from(20_000), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -1091,7 +1129,8 @@ mod recommendations { I256::from(20_000), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } From b2a1e9520a70f055ec1f1e9d5e9a28d759828e1c Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 18 Aug 2023 14:45:21 +0100 Subject: [PATCH 086/103] Proper Debug impl for I256 values --- core/src/types/uint.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index f7c390e3c4..3ba8754713 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -208,7 +208,6 @@ const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); #[derive( Copy, Clone, - Debug, Default, PartialEq, Eq, @@ -219,6 +218,13 @@ const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); )] pub struct I256(pub Uint); +impl fmt::Debug for I256 { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(self, f) + } +} + impl fmt::Display for I256 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_negative() { From eff97857d6e67870830581340edca33668d8e15a Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 18 Aug 2023 15:22:45 +0100 Subject: [PATCH 087/103] Add test_conversion_table_profit_margin() unit test --- shared/src/ledger/eth_bridge/bridge_pool.rs | 96 ++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 0f3f5d3597..2dcdc028f5 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -924,7 +924,9 @@ mod recommendations { } /// Helper function to test [`generate_eligible`]. - fn test_generate_eligible_aux(mut callback: F) + fn test_generate_eligible_aux( + mut callback: F, + ) -> Vec where F: FnMut(TestGenerateEligible<'_>), { @@ -956,6 +958,7 @@ mod recommendations { let eligible = generate_eligible(&table, &in_progress, signed_pool).proceed(); assert_eq!(eligible, expected); + eligible } /// Test the happy path of generating eligible recommendations @@ -1147,6 +1150,97 @@ mod recommendations { .proceed(); assert!(recommendation.is_none()) } + + /// Test the profit margin obtained from relaying two + /// Bridge pool transfers with two distinct token types, + /// whose relation is 1:2 in value. + #[test] + fn test_conversion_table_profit_margin() { + // apfel is worth twice as much as schnitzel + const APF_RATE: f64 = 5e8; + const SCH_RATE: f64 = 1e9; + const APFEL: &str = "APF"; + const SCHNITZEL: &str = "SCH"; + + let conversion_table = { + let mut t = HashMap::new(); + t.insert( + namada_core::types::address::apfel(), + args::BpConversionTableEntry { + alias: APFEL.into(), + conversion_rate: APF_RATE, + }, + ); + t.insert( + namada_core::types::address::schnitzel(), + args::BpConversionTableEntry { + alias: SCHNITZEL.into(), + conversion_rate: SCH_RATE, + }, + ); + t + }; + + let eligible = test_generate_eligible_aux(|ctx| { + ctx.conversion_table.clone_from(&conversion_table); + // tune the pending transfer provided by the ctx + let transfer_paid_in_apfel = { + let mut pending = ctx.pending.clone(); + pending.transfer.amount = 1.into(); + pending.gas_fee.token = + namada_core::types::address::apfel(); + pending + }; + let transfer_paid_in_schnitzel = { + let mut pending = ctx.pending.clone(); + pending.transfer.amount = 2.into(); + pending.gas_fee.token = + namada_core::types::address::schnitzel(); + pending + }; + // add the transfers to the pool, and expect them to + // be eligible transfers + for (pending, rate) in [ + (transfer_paid_in_apfel, APF_RATE), + (transfer_paid_in_schnitzel, SCH_RATE), + ] { + ctx.signed_pool.insert( + pending.keccak256().to_string(), + pending.clone(), + ); + ctx.expected_eligible.push(EligibleRecommendation { + transfer_hash: pending.keccak256().to_string(), + cost: transfer_fee() + - I256::from((1e9 / rate).floor() as u64) + * I256::try_from(pending.gas_fee.amount) + .expect("Test failed"), + pending_transfer: pending, + }); + } + }); + + const VALIDATOR_GAS_FEE: Uint = Uint::from_u64(100_000); + + let recommended_batch = generate_recommendations( + eligible, + &conversion_table, + // gas spent by validator signature checks + VALIDATOR_GAS_FEE, + // unlimited amount of gas + uint::MAX_VALUE, + // only profitable + I256::zero(), + ) + .proceed() + .expect("Test failed"); + + assert_eq!( + recommended_batch.net_profit, + I256::from(1_000_000_000_u64) + I256::from(2_000_000_000_u64) + - I256(VALIDATOR_GAS_FEE) + - transfer_fee() * I256::from(2_u64) + ); + } } } From 225c885aa44971b2ae98d6399d5ffae2f198817b Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 18 Aug 2023 15:30:11 +0100 Subject: [PATCH 088/103] Ignore invalid conversion rates --- shared/src/ledger/eth_bridge/bridge_pool.rs | 27 +++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 2dcdc028f5..829c868e27 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -700,17 +700,30 @@ mod recommendations { let conversion_rate = conversion_table .get(&pending.gas_fee.token) - .and_then(|entry| { - let rate = entry.conversion_rate; - if rate <= 0.0f64 { + .and_then(|entry| match entry.conversion_rate { + r if r == 0.0f64 => { eprintln!( - "Ignoring token with an invalid conversion \ - rate: {}", + "{}: Ignoring null conversion rate", pending.gas_fee.token, ); - return None; + None } - Some(rate) + r if r < 0.0f64 => { + eprintln!( + "{}: Ignoring negative conversion rate: {r:.1}", + pending.gas_fee.token, + ); + None + } + r if r > 1e9 => { + eprintln!( + "{}: Ignoring high conversion rate: {r:.1} > \ + 10^9", + pending.gas_fee.token, + ); + None + } + r => Some(r), })?; // This is the amount of gwei a single gas token is worth From c4e4819d6210f46fd66931643e0abd63b284e0de Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 14 Aug 2023 14:18:51 +0100 Subject: [PATCH 089/103] Add changelog for #1811 --- .../unreleased/improvements/1811-fix-bp-recommendations.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/1811-fix-bp-recommendations.md diff --git a/.changelog/unreleased/improvements/1811-fix-bp-recommendations.md b/.changelog/unreleased/improvements/1811-fix-bp-recommendations.md new file mode 100644 index 0000000000..4c718ef920 --- /dev/null +++ b/.changelog/unreleased/improvements/1811-fix-bp-recommendations.md @@ -0,0 +1,2 @@ +- Added various fee types to the output of the Bridge pool recommendations RPC + ([\#1811](https://github.com/anoma/namada/pull/1811)) \ No newline at end of file From 655601c64c848ce7c2a21ea4dc89d309693537f8 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Fri, 25 Aug 2023 11:01:26 +0100 Subject: [PATCH 090/103] Split Bridge pool transfer hashes on all whitespace toks --- apps/src/lib/cli.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 08c60ec3b1..a5550f0481 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1994,7 +1994,7 @@ pub mod cmds { /// This can be used to relay transfers across the /// bridge to Ethereum. ConstructProof(ConstructProof), - /// Construct and relay a bridge pool proof to + /// Construct and relay a Bridge pool proof to /// Ethereum directly. RelayProof(RelayProof), /// Query the contents of the pool. @@ -2093,7 +2093,7 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) - .about("Add a new transfer to the Ethereum bridge pool.") + .about("Add a new transfer to the Ethereum Bridge pool.") .arg_required_else_help(true) .add_args::>() } @@ -2182,7 +2182,7 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) - .about("Get the contents of the Ethereum bridge pool.") + .about("Get the contents of the Ethereum Bridge pool.") .add_args::>() } } @@ -2202,7 +2202,7 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about( - "Get the contents of the Ethereum bridge pool with a \ + "Get the contents of the Ethereum Bridge pool with a \ signed Merkle root.", ) .add_args::>() @@ -3001,7 +3001,7 @@ pub mod args { Self { query, transfers: hashes - .split(' ') + .split_whitespace() .map(|hash| { KeccakHash::try_from(hash).unwrap_or_else(|_| { tracing::info!( @@ -3019,7 +3019,8 @@ pub mod args { fn def(app: App) -> App { app.add_args::>() .arg(HASH_LIST.def().help( - "List of Keccak hashes of transfers in the bridge pool.", + "Whitespace separated Keccak hash list of transfers in \ + the Bridge pool.", )) .arg( RELAYER @@ -3092,7 +3093,8 @@ pub mod args { ensure Ethereum transfers aren't canceled midway through.", )) .arg(HASH_LIST.def().help( - "List of Keccak hashes of transfers in the bridge pool.", + "Whitespace separated Keccak hash list of transfers in \ + the Bridge pool.", )) .arg( RELAYER From 9a5988cac7b9033ee8ea8cb709cc89731dde0f79 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 29 Aug 2023 09:41:43 +0100 Subject: [PATCH 091/103] Changelog for #1851 --- .changelog/unreleased/improvements/1851-bridge-pool-hashlist.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/1851-bridge-pool-hashlist.md diff --git a/.changelog/unreleased/improvements/1851-bridge-pool-hashlist.md b/.changelog/unreleased/improvements/1851-bridge-pool-hashlist.md new file mode 100644 index 0000000000..4ff7002035 --- /dev/null +++ b/.changelog/unreleased/improvements/1851-bridge-pool-hashlist.md @@ -0,0 +1,2 @@ +- Split Bridge pool transfer hashes on all whitespace toks + ([\#1851](https://github.com/anoma/namada/pull/1851)) \ No newline at end of file From ecc5e863b4119e6135d3918b1a049943952d2b90 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 22 Aug 2023 15:47:11 +0100 Subject: [PATCH 092/103] Update ethbridge-rs to v0.23.0 --- Cargo.lock | 25 ++++++++++++------------- Cargo.toml | 12 ++++++------ wasm/Cargo.lock | 20 ++++++++++---------- wasm_for_tests/wasm_source/Cargo.lock | 20 ++++++++++---------- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18e78ed504..2f2b3197b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,19 +2021,18 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", "ethers", - "smallvec", ] [[package]] name = "ethbridge-governance-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2042,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2053,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethers", diff --git a/Cargo.toml b/Cargo.toml index 7375bb01e1..2398902368 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,12 +66,12 @@ directories = "4.0.1" ed25519-consensus = "1.2.0" escargot = "0.5.7" ethabi = "18.0.0" -ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0"} -ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.22.0" } +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0" } ethers = "2.0.0" expectrl = "0.7.0" eyre = "0.6.5" diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 0f4f1f4a39..2e083c3f46 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethers", diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 6405e7100e..108e62cfad 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.22.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.22.0#1c1028a823a7c2148b3efacea800bfc6c8969c20" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethers", From 305598e5a41f6dcef97e694e41f79e05839395a9 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Mon, 28 Aug 2023 15:17:10 +0100 Subject: [PATCH 093/103] Fix Ethereum oracle tests --- .../lib/node/ledger/ethereum_oracle/events.rs | 26 +++---- .../lib/node/ledger/ethereum_oracle/mod.rs | 18 ++--- .../ledger/ethereum_oracle/test_tools/mod.rs | 76 ++++++++++++++++--- 3 files changed, 85 insertions(+), 35 deletions(-) diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 3472fcb601..bd11cf09e8 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -324,9 +324,9 @@ pub mod eth_events { TRANSFER_TO_ERC_CODEC, TRANSFER_TO_NAMADA_CODEC, VALIDATOR_SET_UPDATE_CODEC, }; - use namada::eth_bridge::ethers::abi::AbiEncode; use super::*; + use crate::node::ledger::ethereum_oracle::test_tools::event_log::GetLog; /// Test that for Ethereum events for which a custom number of /// confirmations may be specified, if a value lower than the @@ -350,7 +350,7 @@ pub mod eth_events { let pending_event = PendingEvent::decode( codec, arbitrary_block_height, - &get_log(event.encode()), + &event.get_log(), min_confirmations.clone(), )?; @@ -397,7 +397,10 @@ pub mod eth_events { ]; let raw: TransferToNamadaFilter = TRANSFER_TO_NAMADA_CODEC - .decode(&get_log(data)) + .decode(ðabi::RawLog { + topics: vec![TransferToNamadaFilter::signature()], + data, + }) .expect("Test failed") .try_into() .expect("Test failed"); @@ -433,7 +436,7 @@ pub mod eth_events { let pending_event = PendingEvent::decode( codec, arbitrary_block_height, - &get_log(event.encode()), + &event.get_log(), min_confirmations, ) .unwrap(); @@ -534,7 +537,7 @@ pub mod eth_events { { let decoded: TransferToNamadaFilter = TRANSFER_TO_NAMADA_CODEC - .decode(&get_log(nam_transfers.clone().encode())) + .decode(&nam_transfers.clone().get_log()) .expect("Test failed") .try_into() .expect("Test failed"); @@ -545,7 +548,7 @@ pub mod eth_events { assert_eq!( { let decoded: TransferToErcFilter = TRANSFER_TO_ERC_CODEC - .decode(&get_log(eth_transfers.clone().encode())) + .decode(ð_transfers.clone().get_log()) .expect("Test failed") .try_into() .expect("Test failed"); @@ -557,7 +560,7 @@ pub mod eth_events { { let decoded: ValidatorSetUpdateFilter = VALIDATOR_SET_UPDATE_CODEC - .decode(&get_log(update.clone().encode())) + .decode(&update.clone().get_log()) .expect("Test failed") .try_into() .expect("Test failed"); @@ -566,15 +569,6 @@ pub mod eth_events { update ); } - - /// Return an Ethereum events log, from the given encoded event - /// data. - fn get_log(data: Vec) -> ethabi::RawLog { - ethabi::RawLog { - data, - topics: vec![], - } - } } } diff --git a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index fc9ae9f0d1..a8db91831c 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -565,7 +565,6 @@ mod test_oracle { use ethbridge_bridge_events::{ TransferToErcFilter, TransferToNamadaFilter, }; - use namada::eth_bridge::ethers::abi::AbiEncode; use namada::eth_bridge::ethers::types::H160; use namada::eth_bridge::structs::Erc20Transfer; use namada::types::address::testing::gen_established_address; @@ -575,6 +574,7 @@ mod test_oracle { use tokio::time::timeout; use super::*; + use crate::node::ledger::ethereum_oracle::test_tools::event_log::GetLog; use crate::node::ledger::ethereum_oracle::test_tools::mock_web3_client::{ event_signature, TestCmd, TestOracle, Web3Client, Web3Controller, }; @@ -716,11 +716,11 @@ mod test_oracle { valid_map: vec![], confirmations: 100.into(), } - .encode(); + .get_log(); let (sender, _) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: new_event, + log: new_event, height: 101, seen: sender, }); @@ -766,11 +766,11 @@ mod test_oracle { valid_map: vec![], confirmations: 100.into(), } - .encode(); + .get_log(); let (sender, mut seen) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: new_event, + log: new_event, height: 150, seen: sender, }); @@ -821,7 +821,7 @@ mod test_oracle { valid_map: vec![], confirmations: 100.into(), } - .encode(); + .get_log(); // confirmed after 125 blocks let gas_payer = gen_established_address(); @@ -836,20 +836,20 @@ mod test_oracle { relayer_address: gas_payer.to_string(), nonce: 0.into(), } - .encode(); + .get_log(); // send in the events to the logs let (sender, seen_second) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: second_event, + log: second_event, height: 125, seen: sender, }); let (sender, _recv) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: first_event, + log: first_event, height: 100, seen: sender, }); diff --git a/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs index dd948a4032..672a1546f8 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs @@ -1,5 +1,64 @@ pub mod events_endpoint; +#[cfg(test)] +pub mod event_log { + // praise be unto thee whom'st've read and understand this code + // p.s.: https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378 + + use ethbridge_bridge_events::{ + TransferToErcFilter, TransferToNamadaFilter, + }; + use ethbridge_governance_events::ValidatorSetUpdateFilter; + use namada::eth_bridge::ethers::abi::AbiEncode; + use namada::eth_bridge::ethers::contract::EthEvent; + + /// Get an [`ethabi::RawLog`] from a given Ethereum event. + pub trait GetLog { + /// Return an [`ethabi::RawLog`]. + fn get_log(self) -> ethabi::RawLog; + } + + impl GetLog for TransferToNamadaFilter { + fn get_log(self) -> ethabi::RawLog { + ethabi::RawLog { + topics: vec![Self::signature()], + data: self.encode(), + } + } + } + + impl GetLog for TransferToErcFilter { + fn get_log(self) -> ethabi::RawLog { + ethabi::RawLog { + topics: vec![Self::signature(), { + let mut buf = [0; 32]; + self.nonce.to_big_endian(&mut buf); + ethabi::ethereum_types::H256(buf) + }], + data: (self.transfers, self.valid_map, self.relayer_address) + .encode(), + } + } + } + + impl GetLog for ValidatorSetUpdateFilter { + fn get_log(self) -> ethabi::RawLog { + ethabi::RawLog { + topics: vec![Self::signature(), { + let mut buf = [0; 32]; + self.validator_set_nonce.to_big_endian(&mut buf); + ethabi::ethereum_types::H256(buf) + }], + data: ( + self.bridge_validator_set_hash, + self.governance_validator_set_hash, + ) + .encode(), + } + } + } +} + #[cfg(test)] pub mod mock_web3_client { use std::borrow::Cow; @@ -32,7 +91,7 @@ pub mod mock_web3_client { NewHeight(Uint256), NewEvent { event_type: MockEventType, - data: Vec, + log: ethabi::RawLog, height: u32, seen: Sender<()>, }, @@ -60,10 +119,10 @@ pub mod mock_web3_client { } TestCmd::NewEvent { event_type: ty, - data, + log, height, seen, - } => oracle.events.push((ty, data, height, seen)), + } => oracle.events.push((ty, log, height, seen)), } } } @@ -82,7 +141,7 @@ pub mod mock_web3_client { pub struct Web3ClientInner { active: bool, latest_block_height: Uint256, - events: Vec<(MockEventType, Vec, u32, Sender<()>)>, + events: Vec<(MockEventType, ethabi::RawLog, u32, Sender<()>)>, blocks_processed: UnboundedSender, last_block_processed: Option, } @@ -116,16 +175,13 @@ pub mod mock_web3_client { let mut logs = vec![]; let mut events = vec![]; std::mem::swap(&mut client.events, &mut events); - for (event_ty, data, height, seen) in events.into_iter() { + for (event_ty, log, height, seen) in events.into_iter() { if event_ty == ty && block_to_check >= Uint256::from(height) { seen.send(()).unwrap(); - logs.push(ethabi::RawLog { - data, - topics: vec![], - }); + logs.push(log); } else { - client.events.push((event_ty, data, height, seen)); + client.events.push((event_ty, log, height, seen)); } } if client.last_block_processed.as_ref() < Some(&block_to_check) From 9a0320c9fbced3d2a16a583e072ca651e7ca4ba0 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 29 Aug 2023 10:47:05 +0100 Subject: [PATCH 094/103] Changelog for #1852 --- .changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md diff --git a/.changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md b/.changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md new file mode 100644 index 0000000000..820642bd7a --- /dev/null +++ b/.changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md @@ -0,0 +1,2 @@ +- Fix the decoding of events observed by the Ethereum oracle + ([\#1852](https://github.com/anoma/namada/pull/1852)) \ No newline at end of file From 5c380dabcc264e6cc421e9dfa993e00e95a431fa Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Aug 2023 09:39:39 +0100 Subject: [PATCH 095/103] Translate NUT denoms to ERC20 denoms --- core/src/ledger/storage_api/token.rs | 29 ++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index 95987d6a83..02adcc32be 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -2,7 +2,7 @@ use super::{StorageRead, StorageWrite}; use crate::ledger::storage_api; -use crate::types::address::Address; +use crate::types::address::{Address, InternalAddress}; use crate::types::token; pub use crate::types::token::{ balance_key, is_any_minted_balance_key, is_balance_key, minted_balance_key, @@ -46,12 +46,29 @@ pub fn read_denom( where S: StorageRead, { - let key = token::denom_key(token); + let (key, nut) = match token { + Address::Internal(InternalAddress::Nut(erc20)) => { + let token = Address::Internal(InternalAddress::Erc20(*erc20)); + (token::denom_key(&token), true) + } + token => (token::denom_key(token), false), + }; storage.read(&key).map(|opt_denom| { - Some( - opt_denom - .unwrap_or_else(|| token::NATIVE_MAX_DECIMAL_PLACES.into()), - ) + Some(opt_denom.unwrap_or_else(|| { + if nut { + // NB: always use the equivalent ERC20's smallest + // denomination to specify amounts, if we cannot + // find a denom in storage + 0u8.into() + } else { + // FIXME: perhaps when we take this branch, we should + // assume the same behavior as NUTs? maybe this branch + // is unreachable, anyway. when would regular tokens + // ever not be denominated? + crate::hints::cold(); + token::NATIVE_MAX_DECIMAL_PLACES.into() + } + })) }) } From 4ce6079a6c8c307a121f036bfd1a32f7edb60517 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 29 Aug 2023 13:28:53 +0100 Subject: [PATCH 096/103] Changelog for #1853 --- .changelog/unreleased/improvements/1853-nut-denom.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/improvements/1853-nut-denom.md diff --git a/.changelog/unreleased/improvements/1853-nut-denom.md b/.changelog/unreleased/improvements/1853-nut-denom.md new file mode 100644 index 0000000000..ba56463d25 --- /dev/null +++ b/.changelog/unreleased/improvements/1853-nut-denom.md @@ -0,0 +1,2 @@ +- Denominate non-whitelisted NUT amounts + ([\#1853](https://github.com/anoma/namada/pull/1853)) \ No newline at end of file From 28e039a84e9e0c3a631c85ad6ed26320dbe5f38f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Aug 2023 10:29:45 +0100 Subject: [PATCH 097/103] Iterate over all addrs in a storage key --- core/src/ledger/storage/write_log.rs | 2 +- core/src/types/storage.rs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/core/src/ledger/storage/write_log.rs b/core/src/ledger/storage/write_log.rs index 641fa7fc19..53f547aee6 100644 --- a/core/src/ledger/storage/write_log.rs +++ b/core/src/ledger/storage/write_log.rs @@ -484,7 +484,7 @@ impl WriteLog { verifiers .insert(Address::Internal(InternalAddress::Multitoken)); } else { - for addr in &key.find_addresses() { + for addr in key.iter_addresses() { if verifiers_from_tx.contains(addr) || initialized_accounts.contains(addr) { diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index a39089aa04..1767715e3b 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -503,14 +503,17 @@ impl Key { /// Returns the addresses from the key segments pub fn find_addresses(&self) -> Vec
{ - let mut addresses = Vec::new(); - for s in &self.segments { - match s { - DbKeySeg::AddressSeg(addr) => addresses.push(addr.clone()), - _ => continue, - } - } - addresses + self.iter_addresses().cloned().collect() + } + + /// Iterates over all addresses in the key segments + pub fn iter_addresses<'k, 'this: 'k>( + &'this self, + ) -> impl Iterator + 'k { + self.segments.iter().filter_map(|s| match s { + DbKeySeg::AddressSeg(addr) => Some(addr), + _ => None, + }) } /// Return the segment at the index parameter From 28319929f5d3761b26bfce2f515d3d913c70f3ae Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Wed, 23 Aug 2023 10:35:30 +0100 Subject: [PATCH 098/103] hotfix: Add NUT native VP as a verifier --- core/src/ledger/storage/write_log.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/src/ledger/storage/write_log.rs b/core/src/ledger/storage/write_log.rs index 53f547aee6..958021cc31 100644 --- a/core/src/ledger/storage/write_log.rs +++ b/core/src/ledger/storage/write_log.rs @@ -474,7 +474,15 @@ impl WriteLog { // get changed keys grouped by the address for key in changed_keys.iter() { // for token keys, trigger Multitoken VP and the owner's VP - if let Some([_, owner]) = is_any_token_balance_key(key) { + // + // TODO: this should not be a special case, as it is error prone. + // any internal addresses corresponding to tokens which have + // native vp equivalents should be automatically added as verifiers + if let Some([token, owner]) = is_any_token_balance_key(key) { + if matches!(&token, Address::Internal(InternalAddress::Nut(_))) + { + verifiers.insert(token.clone()); + } verifiers .insert(Address::Internal(InternalAddress::Multitoken)); verifiers.insert(owner.clone()); From bfc99e7d53b876cc45bac48aa857c9dfdae3603f Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 29 Aug 2023 13:38:12 +0100 Subject: [PATCH 099/103] Changelog for #1854 --- .changelog/unreleased/bug-fixes/1854-hotfix-trigger-nut-vp.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/bug-fixes/1854-hotfix-trigger-nut-vp.md diff --git a/.changelog/unreleased/bug-fixes/1854-hotfix-trigger-nut-vp.md b/.changelog/unreleased/bug-fixes/1854-hotfix-trigger-nut-vp.md new file mode 100644 index 0000000000..67737e03c2 --- /dev/null +++ b/.changelog/unreleased/bug-fixes/1854-hotfix-trigger-nut-vp.md @@ -0,0 +1,2 @@ +- Trigger the NUT VP when NUTs are moved between accounts during wasm + transaction execution ([\#1854](https://github.com/anoma/namada/pull/1854)) \ No newline at end of file From 602978311c34e7734b518da375c56bff90e452a9 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 24 Aug 2023 11:35:50 +0100 Subject: [PATCH 100/103] Introduce `has_eth_addr_segment()` storage key predicate --- core/src/ledger/eth_bridge/storage/mod.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/core/src/ledger/eth_bridge/storage/mod.rs b/core/src/ledger/eth_bridge/storage/mod.rs index b777caa265..e728603fb7 100644 --- a/core/src/ledger/eth_bridge/storage/mod.rs +++ b/core/src/ledger/eth_bridge/storage/mod.rs @@ -7,7 +7,7 @@ use super::ADDRESS; use crate::ledger::parameters::storage::*; use crate::ledger::parameters::ADDRESS as PARAM_ADDRESS; use crate::types::address::Address; -use crate::types::storage::{Key, KeySeg}; +use crate::types::storage::{DbKeySeg, Key, KeySeg}; use crate::types::token::balance_key; /// Key prefix for the storage subspace @@ -26,6 +26,15 @@ pub fn escrow_key(nam_addr: &Address) -> Key { balance_key(nam_addr, &ADDRESS) } +/// Check if the given `key` contains an Ethereum +/// bridge address segment. +#[inline] +pub fn has_eth_addr_segment(key: &Key) -> bool { + key.segments + .iter() + .any(|s| matches!(s, DbKeySeg::AddressSeg(ADDRESS))) +} + /// Returns whether a key belongs to this account or not pub fn is_eth_bridge_key(nam_addr: &Address, key: &Key) -> bool { key == &escrow_key(nam_addr) From 4c76977ea96d87742d6d5bf878492e9f698664e6 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 24 Aug 2023 11:37:39 +0100 Subject: [PATCH 101/103] Refactor the Ethereum bridge native VP Remove ERC20 transfer checks from the Ethereum bridge native VP. Now, the only checks performed by this VP are that NAM was escrowed correctly under the Ethereum bridge address, and that no keys other than balance keys under this address can be modified. --- .../ledger/native_vp/ethereum_bridge/vp.rs | 257 ++++++------------ 1 file changed, 80 insertions(+), 177 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 6a18554692..af965da715 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -1,22 +1,23 @@ //! Validity predicate for the Ethereum bridge use std::collections::{BTreeSet, HashSet}; -use borsh::BorshDeserialize; use eyre::{eyre, Result}; -use itertools::Itertools; -use namada_core::ledger::eth_bridge::storage::{ - self, escrow_key, wrapped_erc20s, -}; +use namada_core::ledger::eth_bridge::storage::{self, escrow_key}; use namada_core::ledger::storage::traits::StorageHasher; use namada_core::ledger::{eth_bridge, storage as ledger_storage}; use namada_core::types::address::Address; use namada_core::types::storage::Key; -use namada_core::types::token::{balance_key, Amount}; +use namada_core::types::token::{balance_key, is_balance_key, Amount}; -use crate::ledger::native_vp::{Ctx, NativeVp, VpEnv}; +use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; use crate::proto::Tx; use crate::vm::WasmCacheAccess; +/// Generic error that may be returned by the validity predicate +#[derive(thiserror::Error, Debug)] +#[error(transparent)] +pub struct Error(#[from] eyre::Error); + /// Validity predicate for the Ethereum bridge pub struct EthBridge<'ctx, DB, H, CA> where @@ -34,39 +35,29 @@ where H: 'static + StorageHasher, CA: 'static + WasmCacheAccess, { - /// If the bridge's escrow key was changed, we check - /// that the balance increased and that the bridge pool - /// VP has been triggered. The bridge pool VP will carry - /// out the rest of the checks. + /// If the Ethereum bridge's escrow key was written to, we check + /// that the NAM balance increased and that the Bridge pool VP has + /// been triggered. fn check_escrow( &self, verifiers: &BTreeSet
, ) -> Result { let escrow_key = balance_key(&self.ctx.storage.native_token, ð_bridge::ADDRESS); - let escrow_pre: Amount = if let Ok(Some(bytes)) = - self.ctx.read_bytes_pre(&escrow_key) - { - BorshDeserialize::try_from_slice(bytes.as_slice()).map_err( - |_| Error(eyre!("Couldn't deserialize a balance from storage")), - )? - } else { - tracing::debug!( - "Could not retrieve the Ethereum bridge VP's balance from \ - storage" - ); - return Ok(false); - }; + + let escrow_pre: Amount = + if let Ok(Some(value)) = (&self.ctx).read_pre_value(&escrow_key) { + value + } else { + tracing::debug!( + "Could not retrieve the Ethereum bridge VP's balance from \ + storage" + ); + return Ok(false); + }; let escrow_post: Amount = - if let Ok(Some(bytes)) = self.ctx.read_bytes_post(&escrow_key) { - BorshDeserialize::try_from_slice(bytes.as_slice()).map_err( - |_| { - Error(eyre!( - "Couldn't deserialize the balance of the Ethereum \ - bridge VP from storage." - )) - }, - )? + if let Ok(Some(value)) = (&self.ctx).read_post_value(&escrow_key) { + value } else { tracing::debug!( "Could not retrieve the modified Ethereum bridge VP's \ @@ -77,6 +68,8 @@ where // The amount escrowed should increase. if escrow_pre < escrow_post { + // NB: normally, we only escrow NAM under the Ethereum bridge + // addresss in the context of a Bridge pool transfer Ok(verifiers.contains(&storage::bridge_pool::BRIDGE_POOL_ADDRESS)) } else { tracing::info!( @@ -88,19 +81,6 @@ where } } -/// One of the the two types of checks -/// this VP must perform. -#[derive(Debug)] -enum CheckType { - Escrow, - Erc20Transfer, -} - -#[derive(thiserror::Error, Debug)] -#[error(transparent)] -/// Generic error that may be returned by the validity predicate -pub struct Error(#[from] eyre::Error); - impl<'a, DB, H, CA> NativeVp for EthBridge<'a, DB, H, CA> where DB: 'static + ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, @@ -112,12 +92,8 @@ where /// Validate that a wasm transaction is permitted to change keys under this /// account. /// - /// We permit only the following changes via wasm for the time being: - /// - a wrapped ERC20's supply key to decrease iff one of its balance keys - /// decreased by the same amount - /// - a wrapped ERC20's balance key to decrease iff another one of its - /// balance keys increased by the same amount - /// - Escrowing Nam in order to mint wrapped Nam on Ethereum + /// We only permit increasing the escrowed balance of NAM under the Ethereum + /// bridge address, when writing to storage from wasm transactions. /// /// Some other changes to the storage subspace of this account are expected /// to happen natively i.e. bypassing this validity predicate. For example, @@ -135,33 +111,37 @@ where "Ethereum Bridge VP triggered", ); - match determine_check_type( - &self.ctx.storage.native_token, - keys_changed, - )? { - // Multitoken VP checks the balance changes for the ERC20 transfer - Some(CheckType::Erc20Transfer) => Ok(true), - Some(CheckType::Escrow) => self.check_escrow(verifiers), - None => Ok(false), + if !validate_changed_keys(&self.ctx.storage.native_token, keys_changed)? + { + return Ok(false); } + + self.check_escrow(verifiers) } } /// Checks if `keys_changed` represents a valid set of changed keys. -/// Depending on which keys get changed, chooses which type of -/// check to perform in the `validate_tx` function. -/// 1. If the Ethereum bridge escrow key was changed, we need to check -/// that escrow was performed correctly. -/// 2. If two erc20 keys where changed, this is a transfer that needs -/// to be checked. -fn determine_check_type( +/// +/// This implies cheking if two distinct keys were changed: +/// +/// 1. The Ethereum bridge escrow account's NAM balance key. +/// 2. Another account's NAM balance key. +/// +/// Any other keys changed under the Ethereum bridge account +/// are rejected. +fn validate_changed_keys( nam_addr: &Address, keys_changed: &BTreeSet, -) -> Result, Error> { - // we aren't concerned with keys that changed outside of our account +) -> Result { + // acquire all keys that either changed our account, or that touched + // nam balances let keys_changed: HashSet<_> = keys_changed .iter() - .filter(|key| storage::is_eth_bridge_key(nam_addr, key)) + .filter(|&key| { + let changes_eth_storage = storage::has_eth_addr_segment(key); + let changes_nam_balance = is_balance_key(nam_addr, key).is_some(); + changes_nam_balance || changes_eth_storage + }) .collect(); if keys_changed.is_empty() { return Err(Error(eyre!( @@ -173,55 +153,11 @@ fn determine_check_type( relevant_keys.len = keys_changed.len(), "Found keys changed under our account" ); - if keys_changed.len() == 1 && keys_changed.contains(&escrow_key(nam_addr)) { - return Ok(Some(CheckType::Escrow)); - } else if keys_changed.len() != 2 { - tracing::debug!( - relevant_keys.len = keys_changed.len(), - "Rejecting transaction as only two keys should have changed" - ); - return Ok(None); - } - - let mut keys = HashSet::<_>::default(); - for key in keys_changed.into_iter() { - let key = match wrapped_erc20s::Key::try_from((nam_addr, key)) { - Ok(key) => { - // Disallow changes to any supply keys via wasm transactions, - // since these should only ever be changed via FinalizeBlock - // after a successful transfer to or from Ethereum - if matches!(key.suffix, wrapped_erc20s::KeyType::Supply) { - tracing::debug!( - ?key, - "Rejecting transaction as key is a supply key" - ); - return Ok(None); - } - key - } - Err(error) => { - tracing::debug!( - %key, - ?error, - "Rejecting transaction as key is not a wrapped ERC20 key" - ); - return Ok(None); - } - }; - keys.insert(key); - } - - // We can .unwrap() here as we know for sure that this set has len=2 - let (key_a, key_b) = keys.into_iter().collect_tuple().unwrap(); - if key_a.asset != key_b.asset { - tracing::debug!( - ?key_a, - ?key_b, - "Rejecting transaction as keys are for different assets" - ); - return Ok(None); - } - Ok(Some(CheckType::Erc20Transfer)) + Ok(keys_changed.len() == 2 + && keys_changed + .iter() + .all(|key| is_balance_key(nam_addr, key).is_some()) + && keys_changed.contains(&escrow_key(nam_addr))) } #[cfg(test)] @@ -232,6 +168,7 @@ mod tests { use borsh::BorshSerialize; use namada_core::ledger::eth_bridge; use namada_core::ledger::eth_bridge::storage::bridge_pool::BRIDGE_POOL_ADDRESS; + use namada_core::ledger::eth_bridge::storage::wrapped_erc20s; use namada_core::ledger::storage_api::StorageWrite; use namada_ethereum_bridge::parameters::{ Contracts, EthereumBridgeConfig, UpgradeableContract, @@ -245,6 +182,7 @@ mod tests { use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{Storage, WlStorage}; use crate::proto::Tx; + use crate::types::address::testing::established_address_1; use crate::types::address::{nam, wnam}; use crate::types::ethereum_events; use crate::types::ethereum_events::EthAddress; @@ -256,8 +194,6 @@ mod tests { const ARBITRARY_OWNER_A_ADDRESS: &str = "atest1d9khqw36x9zyxwfhgfpygv2pgc65gse4gy6rjs34gfzr2v69gy6y23zpggurjv2yx5m52sesu6r4y4"; - const ARBITRARY_OWNER_B_ADDRESS: &str = - "atest1v4ehgw36xuunwd6989prwdfkxqmnvsfjxs6nvv6xxucrs3f3xcmns3fcxdzrvvz9xverzvzr56le8f"; const ARBITRARY_OWNER_A_INITIAL_BALANCE: u64 = 100; const ESCROW_AMOUNT: u64 = 100; const BRIDGE_POOL_ESCROW_INITIAL_BALANCE: u64 = 0; @@ -332,11 +268,23 @@ mod tests { ) } + #[test] + fn test_accepts_expected_keys_changed() { + let keys_changed = BTreeSet::from([ + balance_key(&nam(), &established_address_1()), + balance_key(&nam(), ð_bridge::ADDRESS), + ]); + + let result = validate_changed_keys(&nam(), &keys_changed); + + assert_matches!(result, Ok(true)); + } + #[test] fn test_error_if_triggered_without_keys_changed() { let keys_changed = BTreeSet::new(); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); assert!(result.is_err()); } @@ -346,9 +294,9 @@ mod tests { { let keys_changed = BTreeSet::from_iter(vec![arbitrary_key(); 3]); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } { let keys_changed = BTreeSet::from_iter(vec![ @@ -357,9 +305,9 @@ mod tests { arbitrary_key(), ]); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } } @@ -369,9 +317,9 @@ mod tests { let keys_changed = BTreeSet::from_iter(vec![arbitrary_key(), arbitrary_key()]); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } { @@ -382,9 +330,9 @@ mod tests { )), ]); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } { @@ -399,54 +347,9 @@ mod tests { ), ]); - let result = determine_check_type(&nam(), &keys_changed); - - assert_matches!(result, Ok(None)); - } - } - - #[test] - fn test_rejects_if_multitoken_keys_for_different_assets() { - { - let keys_changed = BTreeSet::from_iter(vec![ - balance_key( - &wrapped_erc20s::token( - ðereum_events::testing::DAI_ERC20_ETH_ADDRESS, - ), - &Address::decode(ARBITRARY_OWNER_A_ADDRESS) - .expect("Couldn't set up test"), - ), - balance_key( - &wrapped_erc20s::token( - ðereum_events::testing::USDC_ERC20_ETH_ADDRESS, - ), - &Address::decode(ARBITRARY_OWNER_B_ADDRESS) - .expect("Couldn't set up test"), - ), - ]); - - let result = determine_check_type(&nam(), &keys_changed); - - assert_matches!(result, Ok(None)); - } - } - - #[test] - fn test_rejects_if_supply_key_changed() { - let asset = ðereum_events::testing::DAI_ERC20_ETH_ADDRESS; - { - let keys_changed = BTreeSet::from_iter(vec![ - minted_balance_key(&wrapped_erc20s::token(asset)), - balance_key( - &wrapped_erc20s::token(asset), - &Address::decode(ARBITRARY_OWNER_B_ADDRESS) - .expect("Couldn't set up test"), - ), - ]); - - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } } From b348bdeb44f67b61f8096bca9c30ee48bc7e8728 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Thu, 24 Aug 2023 15:46:17 +0100 Subject: [PATCH 102/103] Allow an arbitrary nr of accounts to transfer to the Eth bridge addr --- shared/src/ledger/native_vp/ethereum_bridge/vp.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index af965da715..87be6d1510 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -153,11 +153,10 @@ fn validate_changed_keys( relevant_keys.len = keys_changed.len(), "Found keys changed under our account" ); - Ok(keys_changed.len() == 2 + Ok(keys_changed.contains(&escrow_key(nam_addr)) && keys_changed .iter() - .all(|key| is_balance_key(nam_addr, key).is_some()) - && keys_changed.contains(&escrow_key(nam_addr))) + .all(|key| is_balance_key(nam_addr, key).is_some())) } #[cfg(test)] From f63d364e0fe2eec555d758c32ecb77999c8bf316 Mon Sep 17 00:00:00 2001 From: Tiago Carvalho Date: Tue, 29 Aug 2023 13:46:24 +0100 Subject: [PATCH 103/103] Changelog for #1855 --- .changelog/unreleased/bug-fixes/1855-fix-ethbridge-vp.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changelog/unreleased/bug-fixes/1855-fix-ethbridge-vp.md diff --git a/.changelog/unreleased/bug-fixes/1855-fix-ethbridge-vp.md b/.changelog/unreleased/bug-fixes/1855-fix-ethbridge-vp.md new file mode 100644 index 0000000000..a3947e1f51 --- /dev/null +++ b/.changelog/unreleased/bug-fixes/1855-fix-ethbridge-vp.md @@ -0,0 +1,2 @@ +- Fix the Ethereum Bridge VP + ([\#1855](https://github.com/anoma/namada/pull/1855)) \ No newline at end of file