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 diff --git a/Cargo.lock b/Cargo.lock index 8ff5ec29f8..7e2f839e86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2106,8 +2106,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.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2117,8 +2117,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.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -2128,8 +2128,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.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", @@ -2139,8 +2139,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.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2150,8 +2150,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.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethbridge-structs", @@ -2161,8 +2161,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.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", diff --git a/Cargo.toml b/Cargo.toml index db6d130dca..2c25d859e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,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 992abbef33..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.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.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/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 2d28e878a8..dba3da587a 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -2563,6 +2563,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"); @@ -2838,6 +2839,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, @@ -2864,6 +2866,7 @@ pub mod args { }); let fee_payer = BRIDGE_GAS_PAYER.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); + let nut = NUT.parse(matches); Self { tx, asset, @@ -2873,6 +2876,7 @@ pub mod args { fee_amount, fee_payer, code_path, + nut, } } @@ -2906,6 +2910,10 @@ pub mod args { "The Namada address of the account paying the fee for the \ Ethereum transaction.", )) + .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/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 16fca7a834..0e5fced67e 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -924,10 +924,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; @@ -1132,6 +1135,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/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 28cd61f743..645b87d1c4 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -7,14 +7,14 @@ 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, + EthAddress, EthereumEvent, TransferToEthereum, TransferToEthereumKind, TransferToNamada, Uint, }; use namada::types::keccak::KeccakHash; @@ -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: _ }, @@ -180,6 +155,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 +174,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 +279,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 +287,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, @@ -332,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; @@ -524,6 +509,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(), @@ -542,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 = @@ -582,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/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 a900fd4a39..54e3d2a272 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1100,7 +1100,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; @@ -1786,6 +1786,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/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 5804b6ee79..419b17f94b 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -29,7 +29,7 @@ use std::rc::Rc; use borsh::{BorshDeserialize, BorshSerialize}; use masp_primitives::transaction::Transaction; 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::{Gas, TxGasMeter}; @@ -969,27 +969,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/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 5901ce510f..247c9213cc 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 _ => {} } @@ -471,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; @@ -604,6 +600,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(), @@ -617,6 +614,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(), @@ -668,6 +666,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(), @@ -730,6 +729,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]), @@ -824,6 +824,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]), @@ -931,6 +932,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]), @@ -1013,6 +1015,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/Cargo.toml b/core/Cargo.toml index 7c5addad7c..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.18.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/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/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..77b0860a8c --- /dev/null +++ b/core/src/ledger/eth_bridge/storage/whitelist.rs @@ -0,0 +1,164 @@ +//! 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, + } +} + +#[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)); + } +} 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 43b2ca32a3..262dde14b4 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/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index d70c55ab78..c7394af167 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -5,8 +5,10 @@ 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; use crate::types::ethereum_events::{ EthAddress, TransferToEthereum as TransferToEthereumEvent, }; @@ -33,6 +35,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 @@ -67,9 +71,25 @@ 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 { + 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 +118,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..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 { @@ -376,8 +366,9 @@ pub struct TransferToNamada { pub receiver: Address, } -/// An event transferring some kind of value from Namada to Ethereum +/// Transfer to Ethereum kinds. #[derive( + Copy, Clone, Debug, PartialEq, @@ -391,25 +382,45 @@ pub struct TransferToNamada { Serialize, Deserialize, )] -pub struct TransferToEthereum { - /// 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, +#[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; } -/// 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. +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, Debug, @@ -421,13 +432,24 @@ pub struct TransferToEthereum { BorshSerialize, BorshDeserialize, BorshSchema, + Serialize, + Deserialize, )] -#[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, +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, } #[cfg(test)] diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 96c3d776a6..c0d3b13ac0 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -1276,7 +1276,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/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/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 0052fb01b1..3cfbf69fd1 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( @@ -479,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) @@ -517,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| { @@ -582,20 +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, }, gas_fee: GasFee { amount: Amount::from(1), @@ -619,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::>(), ) } @@ -658,7 +705,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 @@ -705,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(), @@ -760,42 +803,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); - 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 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(); - 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); + 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 }; - assert_eq!( - stored_keys_count(&wl_storage), - initial_stored_keys_count + 2 - ); + let receiver_balance_key = balance_key(&wdai, &receiver); + let wdai_supply_key = minted_balance_key(&wdai); + + 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(); + } - 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); + /// 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] @@ -810,10 +925,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 = @@ -822,6 +946,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, @@ -854,7 +979,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())); @@ -876,7 +1010,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) @@ -885,7 +1019,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] @@ -912,6 +1047,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), @@ -985,7 +1121,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"); @@ -1063,13 +1199,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); @@ -1077,6 +1231,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, @@ -1106,6 +1261,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| { @@ -1118,29 +1274,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, @@ -1156,7 +1331,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)) diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs index a2b73f9f00..97fa999fb4 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 { diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 827745def0..0a738d96a4 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,112 @@ 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") + } + + /// 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 diff --git a/ethereum_bridge/src/test_utils.rs b/ethereum_bridge/src/test_utils.rs index e74c4803e0..aec6463396 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}; @@ -91,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 @@ -114,6 +117,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![] }; @@ -172,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/Cargo.toml b/shared/Cargo.toml index 28711a0329..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.18.0"} -ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.18.0"} +ethbridge-bridge-contract.workspace = true +ethbridge-governance-contract.workspace = true ethers.workspace = true eyre.workspace = true futures.workspace = true diff --git a/shared/src/ledger/args.rs b/shared/src/ledger/args.rs index d56c611b17..c1db91d67a 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -732,6 +732,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 fcbe799a73..b33eb273ad 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -31,7 +31,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}; @@ -48,6 +48,7 @@ pub async fn build_bridge_pool_tx< shielded: &mut ShieldedContext, args::EthereumBridgePool { tx: tx_args, + nut, asset, recipient, sender, @@ -69,6 +70,11 @@ pub async fn build_bridge_pool_tx< recipient, sender: sender.clone(), amount, + kind: if nut { + TransferToEthereumKind::Nut + } else { + TransferToEthereumKind::Erc20 + }, }, gas_fee: GasFee { amount: fee_amount, @@ -687,6 +693,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 5a5a084716..1933d325d7 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) @@ -348,6 +347,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; @@ -358,7 +358,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; @@ -369,23 +371,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(), } } @@ -397,6 +409,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") @@ -407,6 +435,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]), @@ -431,20 +460,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 } @@ -458,14 +499,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"); @@ -494,6 +540,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 { @@ -573,6 +620,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]), @@ -591,8 +639,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, @@ -604,8 +653,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, @@ -829,6 +879,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]), @@ -859,6 +910,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]), @@ -928,8 +980,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()), @@ -941,8 +994,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()), @@ -980,6 +1034,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]), @@ -1046,6 +1101,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]), @@ -1133,6 +1189,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]), @@ -1239,6 +1296,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]), @@ -1319,4 +1377,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) + } } 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..afea1da1d4 --- /dev/null +++ b/shared/src/ledger/native_vp/ethereum_bridge/nut.rs @@ -0,0 +1,239 @@ +//! 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) + } +} + +#[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)); + } + } +} diff --git a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs index 1941975740..e07710ff28 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -394,6 +394,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/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index f032ff3be0..9e16f84bcb 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -20,6 +20,7 @@ use crate::ledger::gas::{self, GasMetering, 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}; @@ -86,6 +87,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), } @@ -1013,6 +1016,15 @@ 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(_) => { // The address should be a part of a multitoken key 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..f972215c71 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}; @@ -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 { @@ -119,6 +120,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 +138,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 +156,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(), diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 9889cf8b16..a47d9fd222 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.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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers", diff --git a/wasm/wasm_source/src/tx_bridge_pool.rs b/wasm/wasm_source/src/tx_bridge_pool.rs index b34966f91e..46742afb3c 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, diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 20de6c9b7b..ff088f55ab 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.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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +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.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.21.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.21.0#781782307aac9c4529fe4c6600ea671ec98353d9" dependencies = [ "ethabi", "ethers",