diff --git a/.changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md b/.changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md new file mode 100644 index 0000000000..820642bd7a --- /dev/null +++ b/.changelog/unreleased/bug-fixes/1852-fix-eth-event-decoding.md @@ -0,0 +1,2 @@ +- Fix the decoding of events observed by the Ethereum oracle + ([\#1852](https://github.com/anoma/namada/pull/1852)) \ No newline at end of file diff --git a/.changelog/unreleased/bug-fixes/1854-hotfix-trigger-nut-vp.md b/.changelog/unreleased/bug-fixes/1854-hotfix-trigger-nut-vp.md new file mode 100644 index 0000000000..67737e03c2 --- /dev/null +++ b/.changelog/unreleased/bug-fixes/1854-hotfix-trigger-nut-vp.md @@ -0,0 +1,2 @@ +- Trigger the NUT VP when NUTs are moved between accounts during wasm + transaction execution ([\#1854](https://github.com/anoma/namada/pull/1854)) \ No newline at end of file diff --git a/.changelog/unreleased/bug-fixes/1855-fix-ethbridge-vp.md b/.changelog/unreleased/bug-fixes/1855-fix-ethbridge-vp.md new file mode 100644 index 0000000000..a3947e1f51 --- /dev/null +++ b/.changelog/unreleased/bug-fixes/1855-fix-ethbridge-vp.md @@ -0,0 +1,2 @@ +- Fix the Ethereum Bridge VP + ([\#1855](https://github.com/anoma/namada/pull/1855)) \ No newline at end of file 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/.changelog/unreleased/features/1781-cap-wnam.md b/.changelog/unreleased/features/1781-cap-wnam.md new file mode 100644 index 0000000000..aeba012c6b --- /dev/null +++ b/.changelog/unreleased/features/1781-cap-wnam.md @@ -0,0 +1,2 @@ +- Control the flow of NAM over the Ethereum bridge + ([\#1781](https://github.com/anoma/namada/pull/1781)) \ No newline at end of file diff --git a/.changelog/unreleased/features/1789-update-ethbridge-rs.md b/.changelog/unreleased/features/1789-update-ethbridge-rs.md new file mode 100644 index 0000000000..4fe862e522 --- /dev/null +++ b/.changelog/unreleased/features/1789-update-ethbridge-rs.md @@ -0,0 +1,2 @@ +- Update ethbridge-rs to v0.22.0 + ([\#1789](https://github.com/anoma/namada/pull/1789)) \ No newline at end of file diff --git a/.changelog/unreleased/features/1795-bridge-pool-fees.md b/.changelog/unreleased/features/1795-bridge-pool-fees.md new file mode 100644 index 0000000000..36a9a73c1c --- /dev/null +++ b/.changelog/unreleased/features/1795-bridge-pool-fees.md @@ -0,0 +1,2 @@ +- Allow Bridge pool transfer fees to be paid in arbitrary token types (except + NUTs) ([\#1795](https://github.com/anoma/namada/pull/1795)) \ No newline at end of file diff --git a/.changelog/unreleased/improvements/1811-fix-bp-recommendations.md b/.changelog/unreleased/improvements/1811-fix-bp-recommendations.md new file mode 100644 index 0000000000..4c718ef920 --- /dev/null +++ b/.changelog/unreleased/improvements/1811-fix-bp-recommendations.md @@ -0,0 +1,2 @@ +- Added various fee types to the output of the Bridge pool recommendations RPC + ([\#1811](https://github.com/anoma/namada/pull/1811)) \ No newline at end of file diff --git a/.changelog/unreleased/improvements/1851-bridge-pool-hashlist.md b/.changelog/unreleased/improvements/1851-bridge-pool-hashlist.md new file mode 100644 index 0000000000..4ff7002035 --- /dev/null +++ b/.changelog/unreleased/improvements/1851-bridge-pool-hashlist.md @@ -0,0 +1,2 @@ +- Split Bridge pool transfer hashes on all whitespace toks + ([\#1851](https://github.com/anoma/namada/pull/1851)) \ No newline at end of file diff --git a/.changelog/unreleased/improvements/1853-nut-denom.md b/.changelog/unreleased/improvements/1853-nut-denom.md new file mode 100644 index 0000000000..ba56463d25 --- /dev/null +++ b/.changelog/unreleased/improvements/1853-nut-denom.md @@ -0,0 +1,2 @@ +- Denominate non-whitelisted NUT amounts + ([\#1853](https://github.com/anoma/namada/pull/1853)) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3a069b0956..2f2b3197b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1999,8 +1999,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -2010,8 +2010,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -2021,19 +2021,18 @@ dependencies = [ [[package]] name = "ethbridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-governance-events", "ethers", - "smallvec", ] [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -2043,8 +2042,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -2054,8 +2053,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethers", diff --git a/Cargo.toml b/Cargo.toml index 2d50daf0b6..2398902368 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,12 @@ directories = "4.0.1" ed25519-consensus = "1.2.0" escargot = "0.5.7" ethabi = "18.0.0" +ethbridge-bridge-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-bridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-governance-contract = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-governance-events = {git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0"} +ethbridge-structs = { git = "https://github.com/heliaxdev/ethbridge-rs", tag = "v0.23.0" } ethers = "2.0.0" expectrl = "0.7.0" eyre = "0.6.5" diff --git a/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/bin/namada-relayer/main.rs b/apps/src/bin/namada-relayer/main.rs index 0b314cb9fa..52c15192dc 100644 --- a/apps/src/bin/namada-relayer/main.rs +++ b/apps/src/bin/namada-relayer/main.rs @@ -12,7 +12,7 @@ async fn main() -> Result<()> { // init logging logging::init_from_env_or(LevelFilter::INFO)?; - let (cmd, _) = cli::namada_relayer_cli()?; + let cmd = cli::namada_relayer_cli()?; // run the CLI CliApi::<()>::handle_relayer_command::(None, cmd).await } diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index 1ed2275f2c..a5550f0481 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -1971,46 +1971,63 @@ pub mod cmds { /// Used as sub-commands (`SubCmd` instance) in `namadar` binary. #[derive(Clone, Debug)] pub enum EthBridgePool { + /// The [`super::Context`] provides access to the wallet and the + /// config. It will generate a new wallet and config, if they + /// don't exist. + WithContext(EthBridgePoolWithCtx), + /// Utils don't have [`super::Context`], only the global arguments. + WithoutContext(EthBridgePoolWithoutCtx), + } + + /// Ethereum Bridge pool commands requiring [`super::Context`]. + #[derive(Clone, Debug)] + pub enum EthBridgePoolWithCtx { /// Get a recommendation on a batch of transfers /// to relay. - RecommendBatch(args::RecommendBatch), + RecommendBatch(RecommendBatch), + } + + /// Ethereum Bridge pool commands not requiring [`super::Context`]. + #[derive(Clone, Debug)] + pub enum EthBridgePoolWithoutCtx { /// Construct a proof that a set of transfers is in the pool. /// This can be used to relay transfers across the /// bridge to Ethereum. - ConstructProof(args::BridgePoolProof), - /// Construct and relay a bridge pool proof to + ConstructProof(ConstructProof), + /// Construct and relay a Bridge pool proof to /// Ethereum directly. - RelayProof(args::RelayBridgePoolProof), + RelayProof(RelayProof), /// Query the contents of the pool. - QueryPool(args::Query), + QueryPool(QueryEthBridgePool), /// Query to provable contents of the pool. - QuerySigned(args::Query), + QuerySigned(QuerySignedBridgePool), /// Check the confirmation status of `TransferToEthereum` /// events. - QueryRelays(args::Query), + QueryRelays(QueryRelayProgress), } impl Cmd for EthBridgePool { fn add_sub(app: App) -> App { - app.subcommand(ConstructProof::def().display_order(1)) + app.subcommand(RecommendBatch::def().display_order(1)) + .subcommand(ConstructProof::def().display_order(1)) + .subcommand(RelayProof::def().display_order(1)) .subcommand(QueryEthBridgePool::def().display_order(1)) .subcommand(QuerySignedBridgePool::def().display_order(1)) .subcommand(QueryRelayProgress::def().display_order(1)) } fn parse(matches: &ArgMatches) -> Option { - let recommend = RecommendBatch::parse(matches) - .map(|query| Self::RecommendBatch(query.0)); - let construct_proof = ConstructProof::parse(matches) - .map(|proof| Self::ConstructProof(proof.0)); - let relay_proof = RelayProof::parse(matches) - .map(|proof| Self::RelayProof(proof.0)); - let query_pool = QueryEthBridgePool::parse(matches) - .map(|q| Self::QueryPool(q.0)); - let query_signed = QuerySignedBridgePool::parse(matches) - .map(|q| Self::QuerySigned(q.0)); - let query_relays = QueryRelayProgress::parse(matches) - .map(|q| Self::QueryRelays(q.0)); + use EthBridgePoolWithCtx::*; + use EthBridgePoolWithoutCtx::*; + + let recommend = Self::parse_with_ctx(matches, RecommendBatch); + let construct_proof = + Self::parse_without_ctx(matches, ConstructProof); + let relay_proof = Self::parse_without_ctx(matches, RelayProof); + let query_pool = Self::parse_without_ctx(matches, QueryPool); + let query_signed = Self::parse_without_ctx(matches, QuerySigned); + let query_relays = Self::parse_without_ctx(matches, QueryRelays); + construct_proof .or(recommend) .or(relay_proof) @@ -2020,6 +2037,24 @@ pub mod cmds { } } + impl EthBridgePool { + /// A helper method to parse sub cmds with context + fn parse_with_ctx( + matches: &ArgMatches, + sub_to_self: impl Fn(T) -> EthBridgePoolWithCtx, + ) -> Option { + T::parse(matches).map(|sub| Self::WithContext(sub_to_self(sub))) + } + + /// A helper method to parse sub cmds without context + fn parse_without_ctx( + matches: &ArgMatches, + sub_to_self: impl Fn(T) -> EthBridgePoolWithoutCtx, + ) -> Option { + T::parse(matches).map(|sub| Self::WithoutContext(sub_to_self(sub))) + } + } + impl SubCmd for EthBridgePool { const CMD: &'static str = "ethereum-bridge-pool"; @@ -2058,7 +2093,7 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) - .about("Add a new transfer to the Ethereum bridge pool.") + .about("Add a new transfer to the Ethereum Bridge pool.") .arg_required_else_help(true) .add_args::>() } @@ -2134,7 +2169,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QueryEthBridgePool(args::Query); + pub struct QueryEthBridgePool(pub args::Query); impl SubCmd for QueryEthBridgePool { const CMD: &'static str = "query"; @@ -2147,13 +2182,13 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) - .about("Get the contents of the Ethereum bridge pool.") + .about("Get the contents of the Ethereum Bridge pool.") .add_args::>() } } #[derive(Clone, Debug)] - pub struct QuerySignedBridgePool(args::Query); + pub struct QuerySignedBridgePool(pub args::Query); impl SubCmd for QuerySignedBridgePool { const CMD: &'static str = "query-signed"; @@ -2167,7 +2202,7 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about( - "Get the contents of the Ethereum bridge pool with a \ + "Get the contents of the Ethereum Bridge pool with a \ signed Merkle root.", ) .add_args::>() @@ -2175,7 +2210,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QueryRelayProgress(args::Query); + pub struct QueryRelayProgress(pub args::Query); impl SubCmd for QueryRelayProgress { const CMD: &'static str = "query-relayed"; @@ -2199,14 +2234,14 @@ pub mod cmds { /// Query an Ethereum ABI encoding of the consensus validator /// set in Namada, at the given epoch, or the latest /// one, if none is provided. - ConsensusValidatorSet(args::ConsensusValidatorSet), + ConsensusValidatorSet(ConsensusValidatorSet), /// Query an Ethereum ABI encoding of a proof of the consensus /// validator set in Namada, at the given epoch, or the next /// one, if none is provided. - ValidatorSetProof(args::ValidatorSetProof), + ValidatorSetProof(ValidatorSetProof), /// Relay a validator set update to Namada's Ethereum bridge /// smart contracts. - ValidatorSetUpdateRelay(args::ValidatorSetUpdateRelay), + ValidatorSetUpdateRelay(ValidatorSetUpdateRelay), } impl SubCmd for ValidatorSet { @@ -2216,11 +2251,11 @@ pub mod cmds { matches.subcommand_matches(Self::CMD).and_then(|matches| { let consensus_validator_set = ConsensusValidatorSet::parse(matches) - .map(|args| Self::ConsensusValidatorSet(args.0)); + .map(Self::ConsensusValidatorSet); let validator_set_proof = ValidatorSetProof::parse(matches) - .map(|args| Self::ValidatorSetProof(args.0)); + .map(Self::ValidatorSetProof); let relay = ValidatorSetUpdateRelay::parse(matches) - .map(|args| Self::ValidatorSetUpdateRelay(args.0)); + .map(Self::ValidatorSetUpdateRelay); consensus_validator_set.or(validator_set_proof).or(relay) }) } @@ -2241,7 +2276,7 @@ pub mod cmds { #[derive(Clone, Debug)] pub struct ConsensusValidatorSet( - args::ConsensusValidatorSet, + pub args::ConsensusValidatorSet, ); impl SubCmd for ConsensusValidatorSet { @@ -2265,7 +2300,7 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct ValidatorSetProof(args::ValidatorSetProof); + pub struct ValidatorSetProof(pub args::ValidatorSetProof); impl SubCmd for ValidatorSetProof { const CMD: &'static str = "proof"; @@ -2289,7 +2324,7 @@ pub mod cmds { #[derive(Clone, Debug)] pub struct ValidatorSetUpdateRelay( - args::ValidatorSetUpdateRelay, + pub args::ValidatorSetUpdateRelay, ); impl SubCmd for ValidatorSetUpdateRelay { @@ -2358,6 +2393,7 @@ pub mod cmds { } pub mod args { + use std::collections::HashMap; use std::convert::TryFrom; use std::env; use std::net::SocketAddr; @@ -2421,6 +2457,22 @@ pub mod args { ); pub const BLOCK_HEIGHT: Arg = arg("block-height"); // pub const BLOCK_HEIGHT_OPT: ArgOpt = arg_opt("height"); + pub const BRIDGE_POOL_GAS_AMOUNT: ArgDefault = + arg_default( + "pool-gas-amount", + DefaultFn(|| token::DenominatedAmount { + amount: token::Amount::default(), + denom: NATIVE_MAX_DECIMAL_PLACES.into(), + }), + ); + pub const BRIDGE_POOL_GAS_PAYER: ArgOpt = + arg_opt("pool-gas-payer"); + pub const BRIDGE_POOL_GAS_TOKEN: ArgDefaultFromCtx = + arg_default_from_ctx( + "pool-gas-token", + DefaultFn(|| "NAM".parse().unwrap()), + ); + pub const BRIDGE_POOL_TARGET: Arg = arg("target"); pub const BROADCAST_ONLY: ArgFlag = flag("broadcast-only"); pub const CHAIN_ID: Arg = arg("chain-id"); pub const CHAIN_ID_OPT: ArgOpt = CHAIN_ID.opt(); @@ -2433,6 +2485,7 @@ pub mod args { "consensus-timeout-commit", DefaultFn(|| Timeout::from_str("1s").unwrap()), ); + pub const CONVERSION_TABLE: Arg = arg("conversion-table"); pub const DAEMON_MODE: ArgFlag = flag("daemon"); pub const DAEMON_MODE_RETRY_DUR: ArgOpt = arg_opt("retry-sleep"); pub const DAEMON_MODE_SUCCESS_DUR: ArgOpt = @@ -2450,7 +2503,7 @@ pub mod args { pub const ETH_GAS: ArgOpt = arg_opt("eth-gas"); pub const ETH_GAS_PRICE: ArgOpt = arg_opt("eth-gas-price"); pub const ETH_ADDRESS: Arg = arg("ethereum-address"); - pub const ETH_ADDRESS_OPT: ArgOpt = arg_opt("ethereum-address"); + pub const ETH_ADDRESS_OPT: ArgOpt = ETH_ADDRESS.opt(); pub const ETH_RPC_ENDPOINT: ArgDefault = arg_default( "eth-rpc-endpoint", DefaultFn(|| "http://localhost:8545".into()), @@ -2475,14 +2528,6 @@ pub mod args { ); pub const GAS_TOKEN: ArgDefaultFromCtx = arg_default_from_ctx("gas-token", DefaultFn(|| "NAM".parse().unwrap())); - pub const FEE_PAYER: Arg = arg("fee-payer"); - pub const FEE_AMOUNT: ArgDefault = arg_default( - "fee-amount", - DefaultFn(|| token::DenominatedAmount { - amount: token::Amount::default(), - denom: NATIVE_MAX_DECIMAL_PLACES.into(), - }), - ); pub const GENESIS_PATH: Arg = arg("genesis-path"); pub const GENESIS_VALIDATOR: ArgOpt = arg("genesis-validator").opt(); @@ -2509,10 +2554,10 @@ pub mod args { arg("max-commission-rate-change"); pub const MAX_ETH_GAS: ArgOpt = arg_opt("max_eth-gas"); pub const MODE: ArgOpt = arg_opt("mode"); - pub const NAM_PER_ETH: Arg = arg("nam-per-eth"); pub const NET_ADDRESS: Arg = arg("net-address"); pub const NAMADA_START_TIME: ArgOpt = arg_opt("time"); pub const NO_CONVERSIONS: ArgFlag = flag("no-conversions"); + pub const NUT: ArgFlag = flag("nut"); pub const OUT_FILE_PATH_OPT: ArgOpt = arg_opt("out-file-path"); pub const OUTPUT_FOLDER_PATH: ArgOpt = arg_opt("output-folder-path"); @@ -2787,13 +2832,15 @@ 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, sender: ctx.get(&self.sender), amount: self.amount, fee_amount: self.fee_amount, - fee_payer: ctx.get(&self.fee_payer), + fee_payer: self.fee_payer.map(|fee_payer| ctx.get(&fee_payer)), + fee_token: ctx.get(&self.fee_token), code_path: self.code_path, } } @@ -2803,12 +2850,15 @@ pub mod args { fn parse(matches: &ArgMatches) -> Self { let tx = Tx::parse(matches); let asset = ERC20.parse(matches); - let recipient = ETH_ADDRESS.parse(matches); - let sender = ADDRESS.parse(matches); + let recipient = BRIDGE_POOL_TARGET.parse(matches); + let sender = SOURCE.parse(matches); let amount = InputAmount::Unvalidated(AMOUNT.parse(matches)); - let fee_amount = FEE_AMOUNT.parse(matches).amount; - let fee_payer = FEE_PAYER.parse(matches); + let fee_amount = + InputAmount::Unvalidated(BRIDGE_POOL_GAS_AMOUNT.parse(matches)); + let fee_payer = BRIDGE_POOL_GAS_PAYER.parse(matches); + let fee_token = BRIDGE_POOL_GAS_TOKEN.parse(matches); let code_path = PathBuf::from(TX_BRIDGE_POOL_WASM); + let nut = NUT.parse(matches); Self { tx, asset, @@ -2817,7 +2867,9 @@ pub mod args { amount, fee_amount, fee_payer, + fee_token, code_path, + nut, } } @@ -2829,39 +2881,70 @@ pub mod args { .help("The Ethereum address of the ERC20 token."), ) .arg( - ETH_ADDRESS + BRIDGE_POOL_TARGET .def() .help("The Ethereum address receiving the tokens."), ) .arg( - ADDRESS - .def() - .help("The Namada address sending the tokens."), + SOURCE.def().help("The Namada address sending the tokens."), ) .arg( AMOUNT.def().help( "The amount of tokens being sent across the bridge.", ), ) - .arg(FEE_AMOUNT.def().help( - "The amount of NAM you wish to pay to have this transfer \ + .arg(BRIDGE_POOL_GAS_AMOUNT.def().help( + "The amount of gas you wish to pay to have this transfer \ relayed to Ethereum.", )) - .arg( - FEE_PAYER.def().help( - "The Namada address of the account paying the fee.", - ), - ) + .arg(BRIDGE_POOL_GAS_PAYER.def().help( + "The Namada address of the account paying the gas. By \ + default, it is the same as the source.", + )) + .arg(BRIDGE_POOL_GAS_TOKEN.def().help( + "The token for paying the Bridge pool gas fees. Defaults \ + to NAM.", + )) + .arg(NUT.def().help( + "Add Non Usable Tokens (NUTs) to the Bridge pool. These \ + are usually obtained from invalid transfers to Namada.", + )) } } - impl CliToSdkCtxless> for RecommendBatch { - fn to_sdk_ctxless(self) -> RecommendBatch { + impl CliToSdk> for RecommendBatch { + fn to_sdk(self, ctx: &mut Context) -> RecommendBatch { RecommendBatch:: { query: self.query.to_sdk_ctxless(), max_gas: self.max_gas, gas: self.gas, - nam_per_eth: self.nam_per_eth, + conversion_table: { + let file = std::io::BufReader::new( + std::fs::File::open(self.conversion_table).expect( + "Failed to open the provided file to the \ + conversion table", + ), + ); + let table: HashMap = + serde_json::from_reader(file) + .expect("Failed to parse conversion table"); + table + .into_iter() + .map(|(token, conversion_rate)| { + let token_from_ctx = + FromContext::
::new(token); + let address = ctx.get(&token_from_ctx); + let alias = token_from_ctx.into_raw(); + ( + address, + BpConversionTableEntry { + alias, + conversion_rate, + }, + ) + }) + .collect() + }, } } } @@ -2871,12 +2954,12 @@ pub mod args { let query = Query::parse(matches); let max_gas = MAX_ETH_GAS.parse(matches); let gas = ETH_GAS.parse(matches); - let nam_to_eth = NAM_PER_ETH.parse(matches); + let conversion_table = CONVERSION_TABLE.parse(matches); Self { query, max_gas, gas, - nam_per_eth: nam_to_eth, + conversion_table, } } @@ -2893,9 +2976,9 @@ pub mod args { costs as close to the given value as possible without \ exceeding it.", )) - .arg(NAM_PER_ETH.def().help( - "The amount of NAM that one ETH is worth, represented as \ - a decimal number.", + .arg(CONVERSION_TABLE.def().help( + "Path to a JSON object containing a mapping between token \ + aliases (or addresses) and their conversion rates in gwei", )) } } @@ -2918,7 +3001,7 @@ pub mod args { Self { query, transfers: hashes - .split(' ') + .split_whitespace() .map(|hash| { KeccakHash::try_from(hash).unwrap_or_else(|_| { tracing::info!( @@ -2936,7 +3019,8 @@ pub mod args { fn def(app: App) -> App { app.add_args::>() .arg(HASH_LIST.def().help( - "List of Keccak hashes of transfers in the bridge pool.", + "Whitespace separated Keccak hash list of transfers in \ + the Bridge pool.", )) .arg( RELAYER @@ -3009,7 +3093,8 @@ pub mod args { ensure Ethereum transfers aren't canceled midway through.", )) .arg(HASH_LIST.def().help( - "List of Keccak hashes of transfers in the bridge pool.", + "Whitespace separated Keccak hash list of transfers in \ + the Bridge pool.", )) .arg( RELAYER @@ -4568,6 +4653,7 @@ pub mod args { impl NamadaTypes for CliTypes { type Address = WalletAddress; type BalanceOwner = WalletBalanceOwner; + type BpConversionTable = PathBuf; type Data = PathBuf; type EthereumAddress = String; type Keypair = WalletKeypair; @@ -5418,9 +5504,39 @@ pub fn namada_wallet_cli() -> Result<(cmds::NamadaWallet, Context)> { cmds::NamadaWallet::parse_or_print_help(app) } -pub fn namada_relayer_cli() -> Result<(cmds::NamadaRelayer, Context)> { +pub enum NamadaRelayer { + EthBridgePoolWithCtx(Box<(cmds::EthBridgePoolWithCtx, Context)>), + EthBridgePoolWithoutCtx(cmds::EthBridgePoolWithoutCtx), + ValidatorSet(cmds::ValidatorSet), +} + +pub fn namada_relayer_cli() -> Result { let app = namada_relayer_app(); - cmds::NamadaRelayer::parse_or_print_help(app) + let matches = app.clone().get_matches(); + match Cmd::parse(&matches) { + Some(cmd) => match cmd { + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithContext(sub_cmd), + ) => { + let global_args = args::Global::parse(&matches); + let context = Context::new(global_args)?; + Ok(NamadaRelayer::EthBridgePoolWithCtx(Box::new(( + sub_cmd, context, + )))) + } + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithoutContext(sub_cmd), + ) => Ok(NamadaRelayer::EthBridgePoolWithoutCtx(sub_cmd)), + cmds::NamadaRelayer::ValidatorSet(sub_cmd) => { + Ok(NamadaRelayer::ValidatorSet(sub_cmd)) + } + }, + None => { + let mut app = app; + app.print_help().unwrap(); + safe_exit(2); + } + } } fn namada_app() -> App { diff --git a/apps/src/lib/cli/context.rs b/apps/src/lib/cli/context.rs index ee3a4a8dfa..106b3d00fc 100644 --- a/apps/src/lib/cli/context.rs +++ b/apps/src/lib/cli/context.rs @@ -239,6 +239,10 @@ impl FromContext { phantom: PhantomData, } } + + pub fn into_raw(self) -> String { + self.raw + } } impl FromContext { diff --git a/apps/src/lib/cli/relayer.rs b/apps/src/lib/cli/relayer.rs index 531051d27a..242a9ff061 100644 --- a/apps/src/lib/cli/relayer.rs +++ b/apps/src/lib/cli/relayer.rs @@ -5,9 +5,10 @@ use namada::eth_bridge::ethers::providers::{Http, Provider}; use namada::ledger::eth_bridge::{bridge_pool, validator_set}; use namada::types::control_flow::ProceedOrElse; +use crate::cli; use crate::cli::api::{CliApi, CliClient}; -use crate::cli::args::CliToSdkCtxless; -use crate::cli::cmds; +use crate::cli::args::{CliToSdk, CliToSdkCtxless}; +use crate::cli::cmds::*; fn error() -> Report { eyre!("Fatal error") @@ -16,29 +17,38 @@ fn error() -> Report { impl CliApi { pub async fn handle_relayer_command( client: Option, - cmd: cmds::NamadaRelayer, + cmd: cli::NamadaRelayer, ) -> Result<()> where C: CliClient, { match cmd { - cmds::NamadaRelayer::EthBridgePool(sub) => match sub { - cmds::EthBridgePool::RecommendBatch(mut args) => { - let client = client.unwrap_or_else(|| { - C::from_tendermint_address( - &mut args.query.ledger_address, - ) - }); - client - .wait_until_node_is_synced() - .await - .proceed_or_else(error)?; - let args = args.to_sdk_ctxless(); - bridge_pool::recommend_batch(&client, args) - .await - .proceed_or_else(error)?; + cli::NamadaRelayer::EthBridgePoolWithCtx(boxed) => { + let (sub, mut ctx) = *boxed; + match sub { + EthBridgePoolWithCtx::RecommendBatch(RecommendBatch( + mut args, + )) => { + let client = client.unwrap_or_else(|| { + C::from_tendermint_address( + &mut args.query.ledger_address, + ) + }); + client + .wait_until_node_is_synced() + .await + .proceed_or_else(error)?; + let args = args.to_sdk(&mut ctx); + bridge_pool::recommend_batch(&client, args) + .await + .proceed_or_else(error)?; + } } - cmds::EthBridgePool::ConstructProof(mut args) => { + } + cli::NamadaRelayer::EthBridgePoolWithoutCtx(sub) => match sub { + EthBridgePoolWithoutCtx::ConstructProof(ConstructProof( + mut args, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -53,7 +63,7 @@ impl CliApi { .await .proceed_or_else(error)?; } - cmds::EthBridgePool::RelayProof(mut args) => { + EthBridgePoolWithoutCtx::RelayProof(RelayProof(mut args)) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -74,7 +84,9 @@ impl CliApi { .await .proceed_or_else(error)?; } - cmds::EthBridgePool::QueryPool(mut query) => { + EthBridgePoolWithoutCtx::QueryPool(QueryEthBridgePool( + mut query, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address(&mut query.ledger_address) }); @@ -84,7 +96,9 @@ impl CliApi { .proceed_or_else(error)?; bridge_pool::query_bridge_pool(&client).await; } - cmds::EthBridgePool::QuerySigned(mut query) => { + EthBridgePoolWithoutCtx::QuerySigned( + QuerySignedBridgePool(mut query), + ) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address(&mut query.ledger_address) }); @@ -96,7 +110,9 @@ impl CliApi { .await .proceed_or_else(error)?; } - cmds::EthBridgePool::QueryRelays(mut query) => { + EthBridgePoolWithoutCtx::QueryRelays(QueryRelayProgress( + mut query, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address(&mut query.ledger_address) }); @@ -107,8 +123,10 @@ impl CliApi { bridge_pool::query_relay_progress(&client).await; } }, - cmds::NamadaRelayer::ValidatorSet(sub) => match sub { - cmds::ValidatorSet::ConsensusValidatorSet(mut args) => { + cli::NamadaRelayer::ValidatorSet(sub) => match sub { + ValidatorSet::ConsensusValidatorSet(ConsensusValidatorSet( + mut args, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -122,7 +140,9 @@ impl CliApi { validator_set::query_validator_set_args(&client, args) .await; } - cmds::ValidatorSet::ValidatorSetProof(mut args) => { + ValidatorSet::ValidatorSetProof(ValidatorSetProof( + mut args, + )) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, @@ -138,7 +158,9 @@ impl CliApi { ) .await; } - cmds::ValidatorSet::ValidatorSetUpdateRelay(mut args) => { + ValidatorSet::ValidatorSetUpdateRelay( + ValidatorSetUpdateRelay(mut args), + ) => { let client = client.unwrap_or_else(|| { C::from_tendermint_address( &mut args.query.ledger_address, diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index e9abf6c26d..2b920839f0 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -909,10 +909,13 @@ pub fn genesis( } #[cfg(any(test, feature = "dev"))] pub fn genesis(num_validators: u64) -> Genesis { - use namada::ledger::eth_bridge::{Contracts, UpgradeableContract}; + use namada::ledger::eth_bridge::{ + Contracts, Erc20WhitelistEntry, UpgradeableContract, + }; use namada::types::address::{ self, apfel, btc, dot, eth, kartoffel, nam, schnitzel, wnam, }; + use namada::types::ethereum_events::testing::DAI_ERC20_ETH_ADDRESS; use namada::types::ethereum_events::EthAddress; use crate::wallet; @@ -1115,6 +1118,13 @@ pub fn genesis(num_validators: u64) -> Genesis { gov_params: GovernanceParameters::default(), pgf_params: PgfParameters::default(), ethereum_bridge_params: Some(EthereumBridgeConfig { + erc20_whitelist: vec![Erc20WhitelistEntry { + token_address: DAI_ERC20_ETH_ADDRESS, + token_cap: token::DenominatedAmount { + amount: token::Amount::max(), + denom: 18.into(), + }, + }], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { diff --git a/apps/src/lib/node/ledger/ethereum_oracle/events.rs b/apps/src/lib/node/ledger/ethereum_oracle/events.rs index 28cd61f743..bd11cf09e8 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/events.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/events.rs @@ -7,16 +7,16 @@ 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, - TransferToNamada, Uint, + EthAddress, EthereumEvent, TransferToEthereum, TransferToNamada, Uint, }; + use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::token::Amount; use num256::Uint256; @@ -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: _ }, @@ -178,24 +153,33 @@ pub mod eth_events { }; } - /// Trait to add parsing methods to foreign types. - trait Parse: Sized { - parse_method! { parse_eth_address -> EthAddress } - parse_method! { parse_address -> Address } - parse_method! { parse_amount -> Amount } - parse_method! { parse_u32 -> u32 } - parse_method! { parse_uint256 -> Uint } - parse_method! { parse_bool -> bool } - parse_method! { parse_string -> String } - parse_method! { parse_keccak -> KeccakHash } - parse_method! { parse_amount_array -> Vec } - parse_method! { parse_eth_address_array -> Vec } - parse_method! { parse_address_array -> Vec
} - parse_method! { parse_string_array -> Vec } - parse_method! { parse_transfer_to_namada_array -> Vec } - parse_method! { parse_transfer_to_namada -> TransferToNamada } - parse_method! { parse_transfer_to_eth_array -> Vec } - parse_method! { parse_transfer_to_eth -> TransferToEthereum } + macro_rules! trait_parse_def { + ($($name:ident -> $type:ty;)*) => { + /// Trait to add parsing methods to foreign types. + trait Parse: Sized { + $( parse_method!($name -> $type); )* + } + } + } + + trait_parse_def! { + parse_address -> Address; + parse_address_array -> Vec
; + parse_amount -> Amount; + parse_amount_array -> Vec; + parse_bool -> bool; + parse_eth_address -> EthAddress; + parse_eth_address_array -> Vec; + parse_hash -> Hash; + parse_keccak -> KeccakHash; + parse_string -> String; + parse_string_array -> Vec; + parse_transfer_to_eth -> TransferToEthereum; + parse_transfer_to_eth_array -> Vec; + parse_transfer_to_namada -> TransferToNamada; + parse_transfer_to_namada_array -> Vec; + parse_u32 -> u32; + parse_uint256 -> Uint; } impl Parse for ethabi::Address { @@ -217,7 +201,13 @@ pub mod eth_events { impl Parse for ethabi::Uint { fn parse_amount(self) -> Result { - Ok(Amount::from(self.as_u64())) + let uint = { + use namada::core::types::uint::Uint as NamadaUint; + let mut num_buf = [0; 32]; + self.to_little_endian(&mut num_buf); + NamadaUint::from_little_endian(&num_buf) + }; + Amount::from_uint(uint, 0).map_err(|e| Error::Decode(e.to_string())) } fn parse_u32(self) -> Result { @@ -239,6 +229,10 @@ pub mod eth_events { fn parse_keccak(self) -> Result { Ok(KeccakHash(self)) } + + fn parse_hash(self) -> Result { + Ok(Hash(self)) + } } impl Parse for Vec { @@ -298,17 +292,13 @@ pub mod eth_events { fn parse_transfer_to_eth(self) -> Result { let asset = self.from.parse_eth_address()?; let receiver = self.to.parse_eth_address()?; - let sender = self.sender.parse_address()?; let amount = self.amount.parse_amount()?; - let gas_payer = self.fee_from.parse_address()?; - let gas_amount = self.fee.parse_amount()?; + let checksum = self.namada_data_digest.parse_hash()?; Ok(TransferToEthereum { asset, amount, - sender, receiver, - gas_amount, - gas_payer, + checksum, }) } } @@ -332,11 +322,11 @@ 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; use super::*; + use crate::node::ledger::ethereum_oracle::test_tools::event_log::GetLog; /// Test that for Ethereum events for which a custom number of /// confirmations may be specified, if a value lower than the @@ -360,7 +350,7 @@ pub mod eth_events { let pending_event = PendingEvent::decode( codec, arbitrary_block_height, - &get_log(event.encode()), + &event.get_log(), min_confirmations.clone(), )?; @@ -407,7 +397,10 @@ pub mod eth_events { ]; let raw: TransferToNamadaFilter = TRANSFER_TO_NAMADA_CODEC - .decode(&get_log(data)) + .decode(ðabi::RawLog { + topics: vec![TransferToNamadaFilter::signature()], + data, + }) .expect("Test failed") .try_into() .expect("Test failed"); @@ -443,7 +436,7 @@ pub mod eth_events { let pending_event = PendingEvent::decode( codec, arbitrary_block_height, - &get_log(event.encode()), + &event.get_log(), min_confirmations, ) .unwrap(); @@ -526,10 +519,8 @@ pub mod eth_events { ethereum_structs::Erc20Transfer { from: H160([1; 20]), to: H160([2; 20]), - sender: address.clone(), amount: 0u64.into(), - fee_from: address.clone(), - fee: 0u64.into(), + namada_data_digest: [0; 32], }; 2 ], @@ -542,16 +533,11 @@ 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 = TRANSFER_TO_NAMADA_CODEC - .decode(&get_log(nam_transfers.clone().encode())) + .decode(&nam_transfers.clone().get_log()) .expect("Test failed") .try_into() .expect("Test failed"); @@ -562,7 +548,7 @@ pub mod eth_events { assert_eq!( { let decoded: TransferToErcFilter = TRANSFER_TO_ERC_CODEC - .decode(&get_log(eth_transfers.clone().encode())) + .decode(ð_transfers.clone().get_log()) .expect("Test failed") .try_into() .expect("Test failed"); @@ -574,7 +560,7 @@ pub mod eth_events { { let decoded: ValidatorSetUpdateFilter = VALIDATOR_SET_UPDATE_CODEC - .decode(&get_log(update.clone().encode())) + .decode(&update.clone().get_log()) .expect("Test failed") .try_into() .expect("Test failed"); @@ -582,27 +568,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 - /// data. - fn get_log(data: Vec) -> ethabi::RawLog { - ethabi::RawLog { - data, - topics: vec![], - } } } } diff --git a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs index 1e967b12b4..a8db91831c 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/mod.rs @@ -565,15 +565,16 @@ mod test_oracle { use ethbridge_bridge_events::{ TransferToErcFilter, TransferToNamadaFilter, }; - use namada::eth_bridge::ethers::abi::AbiEncode; use namada::eth_bridge::ethers::types::H160; use namada::eth_bridge::structs::Erc20Transfer; use namada::types::address::testing::gen_established_address; use namada::types::ethereum_events::{EthAddress, TransferToEthereum}; + use namada::types::hash::Hash; use tokio::sync::oneshot::channel; use tokio::time::timeout; use super::*; + use crate::node::ledger::ethereum_oracle::test_tools::event_log::GetLog; use crate::node::ledger::ethereum_oracle::test_tools::mock_web3_client::{ event_signature, TestCmd, TestOracle, Web3Client, Web3Controller, }; @@ -715,11 +716,11 @@ mod test_oracle { valid_map: vec![], confirmations: 100.into(), } - .encode(); + .get_log(); let (sender, _) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: new_event, + log: new_event, height: 101, seen: sender, }); @@ -765,11 +766,11 @@ mod test_oracle { valid_map: vec![], confirmations: 100.into(), } - .encode(); + .get_log(); let (sender, mut seen) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: new_event, + log: new_event, height: 150, seen: sender, }); @@ -820,7 +821,7 @@ mod test_oracle { valid_map: vec![], confirmations: 100.into(), } - .encode(); + .get_log(); // confirmed after 125 blocks let gas_payer = gen_established_address(); @@ -828,29 +829,27 @@ mod test_oracle { transfers: vec![Erc20Transfer { amount: 0.into(), from: H160([0; 20]), - sender: gas_payer.to_string(), to: H160([1; 20]), - fee: 0.into(), - fee_from: gas_payer.to_string(), + namada_data_digest: [0; 32], }], valid_map: vec![true], relayer_address: gas_payer.to_string(), nonce: 0.into(), } - .encode(); + .get_log(); // send in the events to the logs let (sender, seen_second) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: second_event, + log: second_event, height: 125, seen: sender, }); let (sender, _recv) = channel(); controller.apply_cmd(TestCmd::NewEvent { event_type: event_signature::(), - data: first_event, + log: first_event, height: 100, seen: sender, }); @@ -897,10 +896,8 @@ mod test_oracle { TransferToEthereum { amount: Default::default(), asset: EthAddress([0; 20]), - sender: gas_payer.clone(), receiver: EthAddress([1; 20]), - gas_amount: Default::default(), - gas_payer: gas_payer.clone(), + checksum: Hash::default(), } ); } else { diff --git a/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs b/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs index dd948a4032..672a1546f8 100644 --- a/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs +++ b/apps/src/lib/node/ledger/ethereum_oracle/test_tools/mod.rs @@ -1,5 +1,64 @@ pub mod events_endpoint; +#[cfg(test)] +pub mod event_log { + // praise be unto thee whom'st've read and understand this code + // p.s.: https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378 + + use ethbridge_bridge_events::{ + TransferToErcFilter, TransferToNamadaFilter, + }; + use ethbridge_governance_events::ValidatorSetUpdateFilter; + use namada::eth_bridge::ethers::abi::AbiEncode; + use namada::eth_bridge::ethers::contract::EthEvent; + + /// Get an [`ethabi::RawLog`] from a given Ethereum event. + pub trait GetLog { + /// Return an [`ethabi::RawLog`]. + fn get_log(self) -> ethabi::RawLog; + } + + impl GetLog for TransferToNamadaFilter { + fn get_log(self) -> ethabi::RawLog { + ethabi::RawLog { + topics: vec![Self::signature()], + data: self.encode(), + } + } + } + + impl GetLog for TransferToErcFilter { + fn get_log(self) -> ethabi::RawLog { + ethabi::RawLog { + topics: vec![Self::signature(), { + let mut buf = [0; 32]; + self.nonce.to_big_endian(&mut buf); + ethabi::ethereum_types::H256(buf) + }], + data: (self.transfers, self.valid_map, self.relayer_address) + .encode(), + } + } + } + + impl GetLog for ValidatorSetUpdateFilter { + fn get_log(self) -> ethabi::RawLog { + ethabi::RawLog { + topics: vec![Self::signature(), { + let mut buf = [0; 32]; + self.validator_set_nonce.to_big_endian(&mut buf); + ethabi::ethereum_types::H256(buf) + }], + data: ( + self.bridge_validator_set_hash, + self.governance_validator_set_hash, + ) + .encode(), + } + } + } +} + #[cfg(test)] pub mod mock_web3_client { use std::borrow::Cow; @@ -32,7 +91,7 @@ pub mod mock_web3_client { NewHeight(Uint256), NewEvent { event_type: MockEventType, - data: Vec, + log: ethabi::RawLog, height: u32, seen: Sender<()>, }, @@ -60,10 +119,10 @@ pub mod mock_web3_client { } TestCmd::NewEvent { event_type: ty, - data, + log, height, seen, - } => oracle.events.push((ty, data, height, seen)), + } => oracle.events.push((ty, log, height, seen)), } } } @@ -82,7 +141,7 @@ pub mod mock_web3_client { pub struct Web3ClientInner { active: bool, latest_block_height: Uint256, - events: Vec<(MockEventType, Vec, u32, Sender<()>)>, + events: Vec<(MockEventType, ethabi::RawLog, u32, Sender<()>)>, blocks_processed: UnboundedSender, last_block_processed: Option, } @@ -116,16 +175,13 @@ pub mod mock_web3_client { let mut logs = vec![]; let mut events = vec![]; std::mem::swap(&mut client.events, &mut events); - for (event_ty, data, height, seen) in events.into_iter() { + for (event_ty, log, height, seen) in events.into_iter() { if event_ty == ty && block_to_check >= Uint256::from(height) { seen.send(()).unwrap(); - logs.push(ethabi::RawLog { - data, - topics: vec![], - }); + logs.push(log); } else { - client.events.push((event_ty, data, height, seen)); + client.events.push((event_ty, log, height, seen)); } } if client.last_block_processed.as_ref() < Some(&block_to_check) diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 8af2d1a746..55a1c63314 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1038,9 +1038,7 @@ mod test_finalize_block { }; use namada::proto::{Code, Data, Section, Signature}; use namada::types::dec::POS_DECIMAL_PRECISION; - use namada::types::ethereum_events::{ - EthAddress, TransferToEthereum, Uint as ethUint, - }; + use namada::types::ethereum_events::{EthAddress, Uint as ethUint}; use namada::types::hash::Hash; use namada::types::keccak::KeccakHash; use namada::types::key::tm_consensus_key_raw_hash; @@ -1658,6 +1656,7 @@ mod test_finalize_block { /// /// Sets the validity of the transfer on Ethereum's side. fn test_bp_nonce_is_incremented_aux(valid_transfer: bool) { + use crate::node::ledger::shell::address::nam; test_bp(|shell: &mut TestShell| { let asset = EthAddress([0xff; 20]); let receiver = EthAddress([0xaa; 20]); @@ -1682,7 +1681,6 @@ mod test_finalize_block { } // add bertha's gas fees the pool { - use crate::node::ledger::shell::address::nam; let amt: Amount = 999_999_u64.into(); let pool_balance_key = token::balance_key( &nam(), @@ -1695,16 +1693,25 @@ mod test_finalize_block { } // write transfer to storage let transfer = { - use namada::core::types::eth_bridge_pool::PendingTransfer; - let transfer = TransferToEthereum { - amount: 10u64.into(), - asset, - receiver, - gas_amount: 10u64.into(), - sender: bertha.clone(), - gas_payer: bertha.clone(), + use namada::core::types::eth_bridge_pool::{ + GasFee, PendingTransfer, TransferToEthereum, + TransferToEthereumKind, + }; + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + amount: 10u64.into(), + asset, + recipient: receiver, + sender: bertha.clone(), + }, + gas_fee: GasFee { + token: nam(), + amount: 10u64.into(), + payer: bertha.clone(), + }, }; - let pending = PendingTransfer::from(&transfer); + let transfer = (&pending).into(); shell .wl_storage .write(&bridge_pool::get_pending_key(&pending), pending) diff --git a/apps/src/lib/node/ledger/shell/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index c65fbb3f99..c5e89225e3 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -28,7 +28,7 @@ use std::rc::Rc; use borsh::{BorshDeserialize, BorshSerialize}; use namada::core::ledger::eth_bridge; -use namada::ledger::eth_bridge::{EthBridgeQueries, EthereumBridgeConfig}; +use namada::ledger::eth_bridge::{EthBridgeQueries, EthereumOracleConfig}; use namada::ledger::events::log::EventLog; use namada::ledger::events::Event; use namada::ledger::gas::BlockGasMeter; @@ -968,27 +968,16 @@ where ); return; } - let Some(config) = EthereumBridgeConfig::read(&self.wl_storage) else { - tracing::info!( - "Not starting oracle as the Ethereum bridge config couldn't be found in storage" - ); - return; - }; + let config = EthereumOracleConfig::read(&self.wl_storage).expect( + "The oracle config must be present in storage, since the \ + bridge is enabled", + ); let start_block = self .wl_storage .storage .ethereum_height .clone() - .unwrap_or_else(|| { - self.wl_storage - .read(ð_bridge::storage::eth_start_height_key()) - .expect( - "Failed to read Ethereum start height from storage", - ) - .expect( - "The Ethereum start height should be in storage", - ) - }); + .unwrap_or(config.eth_start_height); tracing::info!( ?start_block, "Found Ethereum height from which the Ethereum oracle should \ diff --git a/apps/src/lib/node/ledger/shell/testing/client.rs b/apps/src/lib/node/ledger/shell/testing/client.rs index 164e4a03ea..c07735d0b9 100644 --- a/apps/src/lib/node/ledger/shell/testing/client.rs +++ b/apps/src/lib/node/ledger/shell/testing/client.rs @@ -8,7 +8,7 @@ use tendermint_config::net::Address as TendermintAddress; use super::node::MockNode; use crate::cli::api::{CliApi, CliClient}; use crate::cli::args::Global; -use crate::cli::{args, cmds, Cmd, Context, NamadaClient}; +use crate::cli::{args, cmds, Cmd, Context, NamadaClient, NamadaRelayer}; use crate::node::ledger::shell::testing::utils::Bin; pub fn run( @@ -63,8 +63,21 @@ pub fn run( let app = App::new("test"); let app = cmds::NamadaRelayer::add_sub(args::Global::def(app)); let matches = app.get_matches_from(args.clone()); - let cmd = cmds::NamadaRelayer::parse(&matches) - .expect("Could not parse wallet command"); + let cmd = match cmds::NamadaRelayer::parse(&matches) + .expect("Could not parse relayer command") + { + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithContext(sub_cmd), + ) => NamadaRelayer::EthBridgePoolWithCtx(Box::new(( + sub_cmd, ctx, + ))), + cmds::NamadaRelayer::EthBridgePool( + cmds::EthBridgePool::WithoutContext(sub_cmd), + ) => NamadaRelayer::EthBridgePoolWithoutCtx(sub_cmd), + cmds::NamadaRelayer::ValidatorSet(sub_cmd) => { + NamadaRelayer::ValidatorSet(sub_cmd) + } + }; rt.block_on(CliApi::<()>::handle_relayer_command(Some(node), cmd)) } } diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs index edd83cdb3a..4decd44127 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/bridge_pool_vext.rs @@ -134,19 +134,16 @@ where VoteExtensionError::VerifySigFailed })?; - let bp_root = if cfg!(feature = "abcipp") { - self.wl_storage.ethbridge_queries().get_bridge_pool_root().0 - } else { - self.wl_storage - .ethbridge_queries() - .get_bridge_pool_root_at_height(ext.data.block_height) - .expect("We asserted that the queried height is correct") - .0 - }; + let bp_root = self + .wl_storage + .ethbridge_queries() + .get_bridge_pool_root_at_height(ext.data.block_height) + .expect("We asserted that the queried height is correct") + .0; let nonce = self .wl_storage .ethbridge_queries() - .get_bridge_pool_nonce() + .get_bridge_pool_nonce_at_height(ext.data.block_height) .to_bytes(); let signed = Signed::<_, SignableEthMessage>::new_from( keccak_hash([bp_root, nonce].concat()), diff --git a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs index 891a403f90..cec4158940 100644 --- a/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs +++ b/apps/src/lib/node/ledger/shell/vote_extensions/eth_events.rs @@ -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 _ => {} } @@ -468,6 +463,7 @@ mod test_vote_extensions { use namada::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, Uint, }; + use namada::types::hash::Hash; #[cfg(feature = "abcipp")] use namada::types::keccak::keccak_hash; #[cfg(feature = "abcipp")] @@ -600,10 +596,8 @@ mod test_vote_extensions { transfers: vec![TransferToEthereum { amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -613,10 +607,8 @@ mod test_vote_extensions { transfers: vec![TransferToEthereum { amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -664,10 +656,8 @@ mod test_vote_extensions { transfers: vec![TransferToEthereum { amount: 100.into(), asset: EthAddress([1; 20]), - sender: gen_established_address(), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -725,11 +715,9 @@ mod test_vote_extensions { nonce: 0.into(), transfers: vec![TransferToEthereum { amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -819,11 +807,9 @@ mod test_vote_extensions { nonce: 0.into(), transfers: vec![TransferToEthereum { amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -896,11 +882,9 @@ mod test_vote_extensions { nonce: 0.into(), transfers: vec![TransferToEthereum { amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), @@ -978,11 +962,9 @@ mod test_vote_extensions { nonce: 0.into(), transfers: vec![TransferToEthereum { amount: 100.into(), - sender: gen_established_address(), asset: EthAddress([1; 20]), receiver: EthAddress([2; 20]), - gas_amount: 10.into(), - gas_payer: gen_established_address(), + checksum: Hash::default(), }], valid_transfers_map: vec![true], relayer: gen_established_address(), diff --git a/core/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..167e23e779 100644 --- a/core/src/ledger/eth_bridge/storage/bridge_pool.rs +++ b/core/src/ledger/eth_bridge/storage/bridge_pool.rs @@ -415,7 +415,10 @@ mod test_bridge_pool_tree { use proptest::prelude::*; use super::*; - use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; + use crate::types::address::nam; + use crate::types::eth_bridge_pool::{ + GasFee, TransferToEthereum, TransferToEthereumKind, + }; use crate::types::ethereum_events::EthAddress; /// An established user address for testing & development @@ -432,12 +435,14 @@ 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]), amount: 1.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -458,12 +463,14 @@ 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]), amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -485,12 +492,14 @@ 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]), amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -522,12 +531,14 @@ mod test_bridge_pool_tree { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([2; 20]), amount: 1.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -549,12 +560,14 @@ 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]), amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -579,12 +592,14 @@ 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]), amount: 1u64.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -602,12 +617,14 @@ 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]), amount: 1u64.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -637,12 +654,14 @@ 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]), amount: 1.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -655,12 +674,14 @@ mod test_bridge_pool_tree { ); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([1; 20]), sender: bertha_address(), recipient: EthAddress([0; 20]), amount: 1u64.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -686,12 +707,14 @@ 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]), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -714,12 +737,14 @@ 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]), amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -743,12 +768,14 @@ 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]), amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -772,12 +799,14 @@ 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]), amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -799,12 +828,14 @@ 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]), amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -826,12 +857,14 @@ 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]), amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -853,12 +886,14 @@ 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]), amount: (i as u64).into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -884,12 +919,14 @@ mod test_bridge_pool_tree { .into_iter() .map(|addr| PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress(addr), sender: bertha_address(), recipient: EthAddress(addr), amount: Default::default(), }, gas_fee: GasFee { + token: nam(), amount: Default::default(), payer: bertha_address(), }, diff --git a/core/src/ledger/eth_bridge/storage/mod.rs b/core/src/ledger/eth_bridge/storage/mod.rs index 0906be3d5d..e728603fb7 100644 --- a/core/src/ledger/eth_bridge/storage/mod.rs +++ b/core/src/ledger/eth_bridge/storage/mod.rs @@ -1,12 +1,13 @@ //! Functionality for accessing the storage subspace pub mod bridge_pool; +pub mod whitelist; pub mod wrapped_erc20s; use super::ADDRESS; use crate::ledger::parameters::storage::*; use crate::ledger::parameters::ADDRESS as PARAM_ADDRESS; use crate::types::address::Address; -use crate::types::storage::{Key, KeySeg}; +use crate::types::storage::{DbKeySeg, Key, KeySeg}; use crate::types::token::balance_key; /// Key prefix for the storage subspace @@ -25,6 +26,15 @@ pub fn escrow_key(nam_addr: &Address) -> Key { balance_key(nam_addr, &ADDRESS) } +/// Check if the given `key` contains an Ethereum +/// bridge address segment. +#[inline] +pub fn has_eth_addr_segment(key: &Key) -> bool { + key.segments + .iter() + .any(|s| matches!(s, DbKeySeg::AddressSeg(ADDRESS))) +} + /// Returns whether a key belongs to this account or not pub fn is_eth_bridge_key(nam_addr: &Address, key: &Key) -> bool { key == &escrow_key(nam_addr) 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/ledger/storage/write_log.rs b/core/src/ledger/storage/write_log.rs index 641fa7fc19..958021cc31 100644 --- a/core/src/ledger/storage/write_log.rs +++ b/core/src/ledger/storage/write_log.rs @@ -474,7 +474,15 @@ impl WriteLog { // get changed keys grouped by the address for key in changed_keys.iter() { // for token keys, trigger Multitoken VP and the owner's VP - if let Some([_, owner]) = is_any_token_balance_key(key) { + // + // TODO: this should not be a special case, as it is error prone. + // any internal addresses corresponding to tokens which have + // native vp equivalents should be automatically added as verifiers + if let Some([token, owner]) = is_any_token_balance_key(key) { + if matches!(&token, Address::Internal(InternalAddress::Nut(_))) + { + verifiers.insert(token.clone()); + } verifiers .insert(Address::Internal(InternalAddress::Multitoken)); verifiers.insert(owner.clone()); @@ -484,7 +492,7 @@ impl WriteLog { verifiers .insert(Address::Internal(InternalAddress::Multitoken)); } else { - for addr in &key.find_addresses() { + for addr in key.iter_addresses() { if verifiers_from_tx.contains(addr) || initialized_accounts.contains(addr) { diff --git a/core/src/ledger/storage_api/token.rs b/core/src/ledger/storage_api/token.rs index 95987d6a83..02adcc32be 100644 --- a/core/src/ledger/storage_api/token.rs +++ b/core/src/ledger/storage_api/token.rs @@ -2,7 +2,7 @@ use super::{StorageRead, StorageWrite}; use crate::ledger::storage_api; -use crate::types::address::Address; +use crate::types::address::{Address, InternalAddress}; use crate::types::token; pub use crate::types::token::{ balance_key, is_any_minted_balance_key, is_balance_key, minted_balance_key, @@ -46,12 +46,29 @@ pub fn read_denom( where S: StorageRead, { - let key = token::denom_key(token); + let (key, nut) = match token { + Address::Internal(InternalAddress::Nut(erc20)) => { + let token = Address::Internal(InternalAddress::Erc20(*erc20)); + (token::denom_key(&token), true) + } + token => (token::denom_key(token), false), + }; storage.read(&key).map(|opt_denom| { - Some( - opt_denom - .unwrap_or_else(|| token::NATIVE_MAX_DECIMAL_PLACES.into()), - ) + Some(opt_denom.unwrap_or_else(|| { + if nut { + // NB: always use the equivalent ERC20's smallest + // denomination to specify amounts, if we cannot + // find a denom in storage + 0u8.into() + } else { + // FIXME: perhaps when we take this branch, we should + // assume the same behavior as NUTs? maybe this branch + // is unreachable, anyway. when would regular tokens + // ever not be denominated? + crate::hints::cold(); + token::NATIVE_MAX_DECIMAL_PLACES.into() + } + })) }) } diff --git a/core/src/types/address.rs b/core/src/types/address.rs index d587d20280..4f300c76ed 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -99,6 +99,8 @@ const PREFIX_INTERNAL: &str = "ano"; const PREFIX_IBC: &str = "ibc"; /// Fixed-length address strings prefix for Ethereum addresses. const PREFIX_ETH: &str = "eth"; +/// Fixed-length address strings prefix for Non-Usable-Token addresses. +const PREFIX_NUT: &str = "nut"; #[allow(missing_docs)] #[derive(Error, Debug)] @@ -234,6 +236,11 @@ impl Address { eth_addr.to_canonical().replace("0x", ""); format!("{}::{}", PREFIX_ETH, eth_addr) } + InternalAddress::Nut(eth_addr) => { + let eth_addr = + eth_addr.to_canonical().replace("0x", ""); + format!("{PREFIX_NUT}::{eth_addr}") + } InternalAddress::ReplayProtection => { internal::REPLAY_PROTECTION.to_string() } @@ -330,12 +337,18 @@ impl Address { "Invalid IBC internal address", )), }, - Some((PREFIX_ETH, raw)) => match string { + Some((prefix @ (PREFIX_ETH | PREFIX_NUT), raw)) => match string { _ if raw.len() == HASH_HEX_LEN => { match EthAddress::from_str(&format!("0x{}", raw)) { - Ok(eth_addr) => Ok(Address::Internal( - InternalAddress::Erc20(eth_addr), - )), + Ok(eth_addr) => Ok(match prefix { + PREFIX_ETH => Address::Internal( + InternalAddress::Erc20(eth_addr), + ), + PREFIX_NUT => Address::Internal( + InternalAddress::Nut(eth_addr), + ), + _ => unreachable!(), + }), Err(e) => Err(Error::new( ErrorKind::InvalidData, e.to_string(), @@ -543,6 +556,8 @@ pub enum InternalAddress { EthBridgePool, /// ERC20 token for Ethereum bridge Erc20(EthAddress), + /// Non-usable ERC20 tokens + Nut(EthAddress), /// Replay protection contains transactions' hash ReplayProtection, /// Multitoken @@ -566,6 +581,7 @@ impl Display for InternalAddress { Self::EthBridge => "EthBridge".to_string(), Self::EthBridgePool => "EthBridgePool".to_string(), Self::Erc20(eth_addr) => format!("Erc20: {}", eth_addr), + Self::Nut(eth_addr) => format!("Non-usable token: {eth_addr}"), Self::ReplayProtection => "ReplayProtection".to_string(), Self::Multitoken => "Multitoken".to_string(), Self::Pgf => "PublicGoodFundings".to_string(), @@ -861,6 +877,7 @@ pub mod testing { InternalAddress::EthBridge => {} InternalAddress::EthBridgePool => {} InternalAddress::Erc20(_) => {} + InternalAddress::Nut(_) => {} InternalAddress::ReplayProtection => {} InternalAddress::Pgf => {} InternalAddress::Multitoken => {} /* Add new addresses in the @@ -876,6 +893,7 @@ pub mod testing { Just(InternalAddress::EthBridge), Just(InternalAddress::EthBridgePool), Just(arb_erc20()), + Just(arb_nut()), Just(InternalAddress::ReplayProtection), Just(InternalAddress::Multitoken), Just(InternalAddress::Pgf), @@ -900,6 +918,13 @@ pub mod testing { fn arb_erc20() -> InternalAddress { use crate::types::ethereum_events::testing::arbitrary_eth_address; + // TODO: generate random erc20 addr data InternalAddress::Erc20(arbitrary_eth_address()) } + + fn arb_nut() -> InternalAddress { + use crate::types::ethereum_events::testing::arbitrary_eth_address; + // TODO: generate random erc20 addr data + InternalAddress::Nut(arbitrary_eth_address()) + } } diff --git a/core/src/types/eth_bridge_pool.rs b/core/src/types/eth_bridge_pool.rs index d70c55ab78..86b0815509 100644 --- a/core/src/types/eth_bridge_pool.rs +++ b/core/src/types/eth_bridge_pool.rs @@ -1,21 +1,117 @@ //! The necessary type definitions for the contents of the //! Ethereum bridge pool +use std::borrow::Cow; + use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use ethabi::token::Token; use serde::{Deserialize, Serialize}; +use crate::ledger::eth_bridge::storage::wrapped_erc20s; use crate::types::address::Address; use crate::types::eth_abi::Encode; use crate::types::ethereum_events::{ EthAddress, TransferToEthereum as TransferToEthereumEvent, }; +use crate::types::hash::Hash as HashDigest; use crate::types::storage::{DbKeySeg, Key}; use crate::types::token::Amount; +/// A version used in our Ethereuem smart contracts +const VERSION: u8 = 1; + /// A namespace used in our Ethereuem smart contracts const NAMESPACE: &str = "transfer"; +/// Transfer to Ethereum kinds. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Serialize, + Deserialize, +)] +pub enum TransferToEthereumKind { + /// Transfer ERC20 assets from Namada to Ethereum. + /// + /// These transfers burn wrapped ERC20 assets in Namada, once + /// they have been confirmed. + Erc20, + /// Refund non-usable tokens. + /// + /// These Bridge pool transfers should be crafted for assets + /// that have been transferred to Namada, that had either not + /// been whitelisted or whose token caps had been exceeded in + /// Namada at the time of the transfer. + Nut, +} + +/// Additional data appended to a [`TransferToEthereumEvent`] to +/// construct a [`PendingTransfer`]. +#[derive( + Debug, + Clone, + Hash, + PartialOrd, + PartialEq, + Ord, + Eq, + Serialize, + Deserialize, + BorshSerialize, + BorshDeserialize, + BorshSchema, +)] +pub struct PendingTransferAppendix<'transfer> { + /// The kind of the pending transfer to Ethereum. + pub kind: Cow<'transfer, TransferToEthereumKind>, + /// The sender of the transfer. + pub sender: Cow<'transfer, Address>, + /// The amount of gas fees paid by the user + /// sending this transfer. + pub gas_fee: Cow<'transfer, GasFee>, +} + +impl From for PendingTransferAppendix<'static> { + #[inline] + fn from(pending: PendingTransfer) -> Self { + Self { + kind: Cow::Owned(pending.transfer.kind), + sender: Cow::Owned(pending.transfer.sender), + gas_fee: Cow::Owned(pending.gas_fee), + } + } +} + +impl<'t> From<&'t PendingTransfer> for PendingTransferAppendix<'t> { + #[inline] + fn from(pending: &'t PendingTransfer) -> Self { + Self { + kind: Cow::Borrowed(&pending.transfer.kind), + sender: Cow::Borrowed(&pending.transfer.sender), + gas_fee: Cow::Borrowed(&pending.gas_fee), + } + } +} + +impl<'transfer> PendingTransferAppendix<'transfer> { + /// Calculate the checksum of this [`PendingTransferAppendix`]. + pub fn checksum(&self) -> HashDigest { + let serialized = self + .try_to_vec() + .expect("Serializing a PendingTransferAppendix should not fail"); + HashDigest::sha256(serialized) + } +} + /// A transfer message to be submitted to Ethereum /// to move assets from Namada across the bridge. #[derive( @@ -33,6 +129,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 @@ -60,54 +158,102 @@ pub struct TransferToEthereum { BorshSchema, )] pub struct PendingTransfer { - /// The message to send to Ethereum to + /// Transfer to Ethereum data. pub transfer: TransferToEthereum, - /// The amount of gas fees (in NAM) - /// paid by the user sending this transfer + /// Amount of gas fees paid by the user + /// sending the transfer. pub gas_fee: GasFee, } -impl From for ethbridge_structs::Erc20Transfer { - fn from(pending: PendingTransfer) -> Self { +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) + } + } + } + + /// Retrieve a reference to the appendix of this [`PendingTransfer`]. + #[inline] + pub fn appendix(&self) -> PendingTransferAppendix<'_> { + self.into() + } + + /// Retrieve the owned appendix of this [`PendingTransfer`]. + #[inline] + pub fn into_appendix(self) -> PendingTransferAppendix<'static> { + self.into() + } + + /// Craft a [`PendingTransfer`] from its constituents. + pub fn from_parts( + event: &TransferToEthereumEvent, + appendix: PendingTransferAppendix<'_>, + ) -> Self { + let transfer = TransferToEthereum { + kind: *appendix.kind, + asset: event.asset, + recipient: event.receiver, + sender: (*appendix.sender).clone(), + amount: event.amount, + }; + let gas_fee = (*appendix.gas_fee).clone(); + Self { transfer, gas_fee } + } +} + +impl From<&PendingTransfer> for ethbridge_structs::Erc20Transfer { + fn from(pending: &PendingTransfer) -> Self { + let HashDigest(namada_data_digest) = pending.appendix().checksum(); Self { from: pending.transfer.asset.0.into(), to: pending.transfer.recipient.0.into(), amount: pending.transfer.amount.into(), - fee_from: pending.gas_fee.payer.to_string(), - fee: pending.gas_fee.amount.into(), - sender: pending.transfer.sender.to_string(), + namada_data_digest, } } } -impl Encode<8> for PendingTransfer { - fn tokenize(&self) -> [Token; 8] { +impl From<&PendingTransfer> for TransferToEthereumEvent { + fn from(pending: &PendingTransfer) -> Self { + Self { + amount: pending.transfer.amount, + asset: pending.transfer.asset, + receiver: pending.transfer.recipient, + checksum: pending.appendix().checksum(), + } + } +} + +impl Encode<6> for PendingTransfer { + fn tokenize(&self) -> [Token; 6] { // TODO: This version should be looked up from storage - let version = Token::Uint(1.into()); + let version = Token::Uint(VERSION.into()); let namespace = Token::String(NAMESPACE.into()); let from = Token::Address(self.transfer.asset.0.into()); - let fee = Token::Uint(self.gas_fee.amount.into()); let to = Token::Address(self.transfer.recipient.0.into()); let amount = Token::Uint(self.transfer.amount.into()); - let fee_from = Token::String(self.gas_fee.payer.to_string()); - let sender = Token::String(self.transfer.sender.to_string()); - [version, namespace, from, to, amount, fee_from, fee, sender] + let checksum = Token::FixedBytes(self.appendix().checksum().0.into()); + [version, namespace, from, to, amount, checksum] } } -impl From<&TransferToEthereumEvent> for PendingTransfer { - fn from(event: &TransferToEthereumEvent) -> Self { - let transfer = TransferToEthereum { - asset: event.asset, - recipient: event.receiver, - sender: event.sender.clone(), - amount: event.amount, - }; - let gas_fee = GasFee { - amount: event.gas_amount, - payer: event.gas_payer.clone(), - }; - Self { transfer, gas_fee } +impl Encode<6> for TransferToEthereumEvent { + fn tokenize(&self) -> [Token; 6] { + // TODO: This version should be looked up from storage + let version = Token::Uint(VERSION.into()); + let namespace = Token::String(NAMESPACE.into()); + let from = Token::Address(self.asset.0.into()); + let to = Token::Address(self.receiver.0.into()); + let amount = Token::Uint(self.amount.into()); + let checksum = Token::FixedBytes(self.checksum.0.into()); + [version, namespace, from, to, amount, checksum] } } @@ -121,9 +267,9 @@ impl From<&PendingTransfer> for Key { } } -/// The amount of NAM to be payed to the relayer of -/// a transfer across the Ethereum Bridge to compensate -/// for Ethereum gas fees. +/// The amount of fees to be payed, in Namada, to the relayer +/// of a transfer across the Ethereum Bridge, compensating +/// for Ethereum gas costs. #[derive( Debug, Clone, @@ -139,8 +285,40 @@ impl From<&PendingTransfer> for Key { BorshSchema, )] pub struct GasFee { - /// The amount of fees (in NAM) + /// The amount of fees. pub amount: Amount, /// The account of fee payer. pub payer: Address, + /// The address of the fungible token to draw + /// gas fees from. + pub token: Address, +} + +#[cfg(test)] +mod test_eth_bridge_pool_types { + use super::*; + use crate::types::address::nam; + use crate::types::address::testing::established_address_1; + + /// Test that [`PendingTransfer`] and [`TransferToEthereum`] + /// have the same keccak hash, after being ABI encoded. + #[test] + fn test_same_keccak_hash() { + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + amount: 10u64.into(), + asset: EthAddress([0xaa; 20]), + recipient: EthAddress([0xbb; 20]), + sender: established_address_1(), + }, + gas_fee: GasFee { + token: nam(), + amount: 10u64.into(), + payer: established_address_1(), + }, + }; + let event: TransferToEthereumEvent = (&pending).into(); + assert_eq!(pending.keccak256(), event.keccak256()); + } } diff --git a/core/src/types/ethereum_events.rs b/core/src/types/ethereum_events.rs index bab4d46bd2..f7097e9749 100644 --- a/core/src/types/ethereum_events.rs +++ b/core/src/types/ethereum_events.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; use crate::types::address::Address; use crate::types::eth_abi::Encode; +use crate::types::ethereum_structs::Erc20Transfer; use crate::types::hash::Hash; use crate::types::keccak::KeccakHash; use crate::types::storage::{DbKeySeg, KeySeg}; @@ -334,16 +335,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 { @@ -398,36 +389,34 @@ pub struct TransferToEthereum { pub asset: EthAddress, /// The address receiving assets on Ethereum pub receiver: EthAddress, - /// The amount of fees (in NAM) - pub gas_amount: Amount, - /// The address sending assets to Ethereum. - pub sender: Address, - /// The account of fee payer. - pub gas_payer: Address, + /// Checksum of all Namada specific fields, including, + /// but not limited to, whether it is a NUT transfer, + /// the address of the sender, etc + /// + /// It serves to uniquely identify an event stored under + /// the Bridge pool, in Namada + pub checksum: Hash, } -/// struct for whitelisting a token from Ethereum. -/// Includes the address of issuing contract and -/// a cap on the max amount of this token allowed to be -/// held by the bridge. -#[derive( - Clone, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - BorshSerialize, - BorshDeserialize, - BorshSchema, -)] -#[allow(dead_code)] -pub struct TokenWhitelist { - /// Address of Ethereum smart contract issuing token - pub token: EthAddress, - /// Maximum amount of token allowed on the bridge - pub cap: Amount, +impl From for TransferToEthereum { + #[inline] + fn from(transfer: Erc20Transfer) -> Self { + Self { + amount: { + let uint = { + use crate::types::uint::Uint as NamadaUint; + let mut num_buf = [0; 32]; + transfer.amount.to_little_endian(&mut num_buf); + NamadaUint::from_little_endian(&num_buf) + }; + // this is infallible for a denom of 0 + Amount::from_uint(uint, 0).unwrap() + }, + asset: EthAddress(transfer.from.0), + receiver: EthAddress(transfer.to.0), + checksum: Hash(transfer.namada_data_digest), + } + } } #[cfg(test)] diff --git a/core/src/types/storage.rs b/core/src/types/storage.rs index 0f0a6032f0..1767715e3b 100644 --- a/core/src/types/storage.rs +++ b/core/src/types/storage.rs @@ -503,14 +503,17 @@ impl Key { /// Returns the addresses from the key segments pub fn find_addresses(&self) -> Vec
{ - let mut addresses = Vec::new(); - for s in &self.segments { - match s { - DbKeySeg::AddressSeg(addr) => addresses.push(addr.clone()), - _ => continue, - } - } - addresses + self.iter_addresses().cloned().collect() + } + + /// Iterates over all addresses in the key segments + pub fn iter_addresses<'k, 'this: 'k>( + &'this self, + ) -> impl Iterator + 'k { + self.segments.iter().filter_map(|s| match s { + DbKeySeg::AddressSeg(addr) => Some(addr), + _ => None, + }) } /// Return the segment at the index parameter @@ -1254,7 +1257,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/core/src/types/token.rs b/core/src/types/token.rs index 6056495cd1..a72bce448b 100644 --- a/core/src/types/token.rs +++ b/core/src/types/token.rs @@ -309,6 +309,12 @@ impl DenominatedAmount { } } + /// Check if the inner [`Amount`] is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.amount.is_zero() + } + /// A precise string representation. The number of /// decimal places in this string gives the denomination. /// This not true of the string produced by the `Display` diff --git a/core/src/types/uint.rs b/core/src/types/uint.rs index f7c390e3c4..3ba8754713 100644 --- a/core/src/types/uint.rs +++ b/core/src/types/uint.rs @@ -208,7 +208,6 @@ const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); #[derive( Copy, Clone, - Debug, Default, PartialEq, Eq, @@ -219,6 +218,13 @@ const MINUS_ZERO: Uint = Uint([0u64, 0u64, 0u64, 9223372036854775808]); )] pub struct I256(pub Uint); +impl fmt::Debug for I256 { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(self, f) + } +} + impl fmt::Display for I256 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.is_negative() { diff --git a/ethereum_bridge/src/parameters.rs b/ethereum_bridge/src/parameters.rs index 4f3b2f1bc5..6395dd6cab 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, NATIVE_MAX_DECIMAL_PLACES}; 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,80 @@ 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 + { + if addr == native_erc20 + && denom != &NATIVE_MAX_DECIMAL_PLACES.into() + { + panic!( + "Error writing Ethereum bridge config: The native token \ + should have {NATIVE_MAX_DECIMAL_PLACES} decimal places" + ); + } + + let key = whitelist::Key { + asset: *addr, + suffix: whitelist::KeyType::Whitelisted, + } + .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 +295,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 +394,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 +420,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 +437,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 +446,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 +456,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 +478,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 +487,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/bridge_pool_roots.rs b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs index 7262c20a31..8571881453 100644 --- a/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs +++ b/ethereum_bridge/src/protocol/transactions/bridge_pool_roots.rs @@ -45,6 +45,7 @@ where bridge pool root and nonce." ); let voting_powers = utils::get_voting_powers(wl_storage, &vext)?; + let root_height = vext.iter().next().unwrap().data.block_height; let (partial_proof, seen_by) = parse_vexts(wl_storage, vext); // apply updates to the bridge pool root. @@ -65,7 +66,7 @@ where wl_storage .write_bytes( &get_signed_root_key(), - (proof, wl_storage.storage.get_last_block_height()) + (proof, root_height) .try_to_vec() .expect("Serializing a Bridge pool root shouldn't fail."), ) @@ -195,11 +196,17 @@ mod test_apply_bp_roots_to_storage { use namada_core::ledger::storage_api::StorageRead; use namada_core::proto::{SignableEthMessage, Signed}; use namada_core::types::address; + use namada_core::types::dec::Dec; use namada_core::types::ethereum_events::Uint; use namada_core::types::keccak::{keccak_hash, KeccakHash}; + use namada_core::types::key::RefTo; use namada_core::types::storage::Key; use namada_core::types::token::Amount; use namada_core::types::vote_extensions::bridge_pool_roots; + use namada_proof_of_stake::parameters::PosParams; + use namada_proof_of_stake::{ + become_validator, bond_tokens, write_pos_params, BecomeValidator, + }; use super::*; use crate::protocol::transactions::votes::{ @@ -687,4 +694,127 @@ mod test_apply_bp_roots_to_storage { assert_eq!(proof.signatures, expected.signatures); assert_eq!(proof.data, expected.data); } + + /// Test that when we acquire a complete BP roots proof, + /// the block height stored in storage is that of the + /// tree root that was decided. + #[test] + fn test_bp_roots_across_epoch_boundaries() { + // the validators that will vote in the tally + let validator_1 = address::testing::established_address_1(); + let validator_1_stake = Amount::native_whole(100); + + let validator_2 = address::testing::established_address_2(); + let validator_2_stake = Amount::native_whole(100); + + let validator_3 = address::testing::established_address_3(); + let validator_3_stake = Amount::native_whole(100); + + // start epoch 0 with validator 1 + let (mut wl_storage, keys) = test_utils::setup_storage_with_validators( + HashMap::from([(validator_1.clone(), validator_1_stake)]), + ); + + // update the pos params + let params = PosParams { + pipeline_len: 1, + ..Default::default() + }; + write_pos_params(&mut wl_storage, params.clone()).expect("Test failed"); + + // insert validators 2 and 3 at epoch 1 + for (validator, stake) in [ + (&validator_2, validator_2_stake), + (&validator_3, validator_3_stake), + ] { + let keys = test_utils::TestValidatorKeys::generate(); + let consensus_key = &keys.consensus.ref_to(); + let eth_cold_key = &keys.eth_gov.ref_to(); + let eth_hot_key = &keys.eth_bridge.ref_to(); + become_validator(BecomeValidator { + storage: &mut wl_storage, + params: ¶ms, + address: validator, + consensus_key, + eth_cold_key, + eth_hot_key, + current_epoch: 0.into(), + commission_rate: Dec::new(5, 2).unwrap(), + max_commission_rate_change: Dec::new(1, 2).unwrap(), + }) + .expect("Test failed"); + bond_tokens(&mut wl_storage, None, validator, stake, 0.into()) + .expect("Test failed"); + } + + // query validators to make sure they were inserted correctly + macro_rules! query_validators { + () => { + |epoch: u64| { + wl_storage + .pos_queries() + .get_consensus_validators(Some(epoch.into())) + .iter() + .map(|validator| { + (validator.address, validator.bonded_stake) + }) + .collect::>() + } + }; + } + let query_validators = query_validators!(); + let epoch_0_validators = query_validators(0); + let epoch_1_validators = query_validators(1); + _ = query_validators; + assert_eq!( + epoch_0_validators, + HashMap::from([(validator_1.clone(), validator_1_stake)]) + ); + assert_eq!( + epoch_1_validators, + HashMap::from([ + (validator_1.clone(), validator_1_stake), + (validator_2, validator_2_stake), + (validator_3, validator_3_stake), + ]) + ); + + // set up the bridge pool's storage + bridge_pool_vp::init_storage(&mut wl_storage); + test_utils::commit_bridge_pool_root_at_height( + &mut wl_storage.storage, + &KeccakHash([1; 32]), + 3.into(), + ); + + // construct proof + let root = wl_storage.ethbridge_queries().get_bridge_pool_root(); + let nonce = wl_storage.ethbridge_queries().get_bridge_pool_nonce(); + let to_sign = keccak_hash([root.0, nonce.to_bytes()].concat()); + let hot_key = &keys[&validator_1].eth_bridge; + let vext = bridge_pool_roots::Vext { + validator_addr: validator_1.clone(), + block_height: 3.into(), + sig: Signed::<_, SignableEthMessage>::new(hot_key, to_sign).sig, + } + .sign(&keys[&validator_1].protocol); + + _ = apply_derived_tx(&mut wl_storage, vext.into()) + .expect("Test failed"); + + // query validator set of the proof + // (should be the one from epoch 0) + let (_, root_height) = wl_storage + .ethbridge_queries() + .get_signed_bridge_pool_root() + .expect("Test failed"); + let root_epoch = wl_storage + .pos_queries() + .get_epoch(root_height) + .expect("Test failed"); + + let query_validators = query_validators!(); + let root_epoch_validators = query_validators(root_epoch.0); + assert_eq!(epoch_0_validators, root_epoch_validators); + } } diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs index 0052fb01b1..48aac0b8de 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/events.rs @@ -7,8 +7,7 @@ use borsh::BorshDeserialize; use eyre::{Result, WrapErr}; use namada_core::hints; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ - get_nonce_key, get_pending_key, is_pending_transfer_key, - BRIDGE_POOL_ADDRESS, + get_nonce_key, is_pending_transfer_key, BRIDGE_POOL_ADDRESS, }; use namada_core::ledger::eth_bridge::storage::{ self as bridge_storage, wrapped_erc20s, @@ -19,7 +18,9 @@ use namada_core::ledger::storage::traits::StorageHasher; use namada_core::ledger::storage::{DBIter, WlStorage, DB}; use namada_core::ledger::storage_api::{StorageRead, StorageWrite}; use namada_core::types::address::Address; -use namada_core::types::eth_bridge_pool::PendingTransfer; +use namada_core::types::eth_bridge_pool::{ + PendingTransfer, TransferToEthereumKind, +}; use namada_core::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, TransferToNamada, TransfersToNamada, @@ -30,7 +31,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,18 +141,33 @@ 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.should_mint_erc20s() { + tracing::info!( + "Minted wrapped ERC20s - (asset - {asset}, receiver - \ + {receiver}, amount - {})", + asset_count.erc20_amount.to_string_native(), + ); + } + if asset_count.should_mint_nuts() { + 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)? + redeem_native_token( + wl_storage, + &wrapped_native_erc20, + receiver, + amount, + )? }; changed_keys.append(&mut changed) } @@ -161,6 +177,7 @@ where /// Redeems `amount` of the native token for `receiver` from escrow. fn redeem_native_token( wl_storage: &mut WlStorage, + native_erc20: &EthAddress, receiver: &Address, amount: &token::Amount, ) -> Result> @@ -172,103 +189,141 @@ where token::balance_key(&wl_storage.storage.native_token, &BRIDGE_ADDRESS); let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, receiver); + let native_werc20_supply_key = + minted_balance_key(&wrapped_erc20s::token(native_erc20)); - let eth_bridge_native_token_balance_pre: token::Amount = - StorageRead::read(wl_storage, ð_bridge_native_token_balance_key)? - .expect( - "Ethereum bridge must always have an explicit balance of the \ - native token", - ); - let receiver_native_token_balance_pre: token::Amount = - StorageRead::read(wl_storage, &receiver_native_token_balance_key)? - .unwrap_or_default(); - - let eth_bridge_native_token_balance_post = - eth_bridge_native_token_balance_pre - .checked_sub(*amount) - .expect( - "Ethereum bridge should always have enough native tokens to \ - redeem any confirmed transfers", - ); - let receiver_native_token_balance_post = receiver_native_token_balance_pre - .checked_add(*amount) - .expect("Receiver's balance is full"); - - StorageWrite::write( + update::amount( wl_storage, ð_bridge_native_token_balance_key, - eth_bridge_native_token_balance_post, + |balance| { + tracing::debug!( + %eth_bridge_native_token_balance_key, + ?balance, + "Existing value found", + ); + balance.spend(amount); + tracing::debug!( + %eth_bridge_native_token_balance_key, + ?balance, + "New value calculated", + ); + }, )?; - StorageWrite::write( + update::amount( wl_storage, &receiver_native_token_balance_key, - receiver_native_token_balance_post, + |balance| { + tracing::debug!( + %receiver_native_token_balance_key, + ?balance, + "Existing value found", + ); + balance.receive(amount); + tracing::debug!( + %receiver_native_token_balance_key, + ?balance, + "New value calculated", + ); + }, )?; + update::amount(wl_storage, &native_werc20_supply_key, |balance| { + tracing::debug!( + %native_werc20_supply_key, + ?balance, + "Existing value found", + ); + balance.spend(amount); + tracing::debug!( + %native_werc20_supply_key, + ?balance, + "New value calculated", + ); + })?; tracing::info!( amount = %amount.to_string_native(), %receiver, - eth_bridge_native_token_balance_pre = %eth_bridge_native_token_balance_pre.to_string_native(), - eth_bridge_native_token_balance_post = %eth_bridge_native_token_balance_post.to_string_native(), - receiver_native_token_balance_pre = %receiver_native_token_balance_pre.to_string_native(), - receiver_native_token_balance_post = %receiver_native_token_balance_post.to_string_native(), "Redeemed native token for wrapped ERC20 token" ); Ok(BTreeSet::from([ eth_bridge_native_token_balance_key, receiver_native_token_balance_key, + native_werc20_supply_key, ])) } +/// 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 + .should_mint_nuts() + .then(|| (wrapped_erc20s::nut(asset), asset_count.nut_amount)), + // check if we should mint erc20s + asset_count + .should_mint_erc20s() + .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( @@ -306,26 +361,25 @@ where }) .filter(is_pending_transfer_key) .collect(); - let pool_balance_key = - balance_key(&wl_storage.storage.native_token, &BRIDGE_POOL_ADDRESS); - let relayer_rewards_key = - balance_key(&wl_storage.storage.native_token, relayer); // Remove the completed transfers from the bridge pool for (event, is_valid) in transfers.iter().zip(valid_transfers.iter().copied()) { - let pending_transfer = event.into(); - let key = get_pending_key(&pending_transfer); - if hints::unlikely(!wl_storage.has_key(&key)?) { + let (pending_transfer, key) = if let Some((pending, key)) = + wl_storage.ethbridge_queries().lookup_transfer_to_eth(event) + { + (pending, key) + } else { + hints::cold(); unreachable!("The transfer should exist in the bridge pool"); - } + }; if hints::likely(is_valid) { tracing::debug!( ?pending_transfer, "Valid transfer to Ethereum detected, compensating the \ relayer and burning any Ethereum assets in Namada" ); - changed_keys.append(&mut burn_transferred_assets( + changed_keys.append(&mut update_transferred_asset_balances( wl_storage, &pending_transfer, )?); @@ -340,6 +394,10 @@ where &pending_transfer, )?); } + let pool_balance_key = + balance_key(&pending_transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); + let relayer_rewards_key = + balance_key(&pending_transfer.gas_fee.token, relayer); // give the relayer the gas fee for this transfer. update::amount(wl_storage, &relayer_rewards_key, |balance| { balance.receive(&pending_transfer.gas_fee.amount); @@ -351,10 +409,8 @@ where wl_storage.delete(&key)?; _ = pending_keys.remove(&key); _ = changed_keys.insert(key); - } - if !transfers.is_empty() { - changed_keys.insert(relayer_rewards_key); - changed_keys.insert(pool_balance_key); + _ = changed_keys.insert(pool_balance_key); + _ = changed_keys.insert(relayer_rewards_key); } if pending_keys.is_empty() { @@ -436,9 +492,9 @@ where let mut changed_keys = BTreeSet::default(); let payer_balance_key = - balance_key(&wl_storage.storage.native_token, &transfer.gas_fee.payer); + balance_key(&transfer.gas_fee.token, &transfer.gas_fee.payer); let pool_balance_key = - balance_key(&wl_storage.storage.native_token, &BRIDGE_POOL_ADDRESS); + balance_key(&transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); update::amount(wl_storage, &payer_balance_key, |balance| { balance.receive(&transfer.gas_fee.amount); })?; @@ -479,7 +535,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) @@ -497,7 +553,9 @@ where Ok(changed_keys) } -fn burn_transferred_assets( +/// Burns any transferred ERC20s other than wNAM. If NAM is transferred, +/// update the wNAM supply key. +fn update_transferred_asset_balances( wl_storage: &mut WlStorage, transfer: &PendingTransfer, ) -> Result> @@ -512,12 +570,26 @@ where return Err(eyre::eyre!("Could not read wNam key from storage")); }; + let token = transfer.token_address(); + + // the wrapped NAM supply increases when we transfer to Ethereum if transfer.transfer.asset == native_erc20_addr { - tracing::debug!(?transfer, "Keeping wrapped NAM in escrow"); + if hints::unlikely(matches!( + &transfer.transfer.kind, + TransferToEthereumKind::Nut + )) { + unreachable!("Attempted to mint wNAM NUTs!"); + } + let supply_key = minted_balance_key(&token); + update::amount(wl_storage, &supply_key, |supply| { + supply.receive(&transfer.transfer.amount); + })?; + _ = changed_keys.insert(supply_key); + tracing::debug!(?transfer, "Updated wrapped NAM supply"); return Ok(changed_keys); } - let token = wrapped_erc20s::token(&transfer.transfer.asset); + // other asset kinds must be burned let escrow_balance_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); update::amount(wl_storage, &escrow_balance_key, |balance| { @@ -537,9 +609,12 @@ where #[cfg(test)] mod tests { + use std::collections::HashMap; + use assert_matches::assert_matches; use borsh::BorshSerialize; use eyre::Result; + use namada_core::ledger::eth_bridge::storage::bridge_pool::get_pending_key; use namada_core::ledger::parameters::{ update_epoch_parameter, EpochDuration, }; @@ -547,7 +622,7 @@ mod tests { use namada_core::ledger::storage::testing::TestWlStorage; use namada_core::ledger::storage::types::encode; use namada_core::types::address::testing::gen_implicit_address; - use namada_core::types::address::{gen_established_address, nam}; + use namada_core::types::address::{gen_established_address, nam, wnam}; use namada_core::types::eth_bridge_pool::GasFee; use namada_core::types::ethereum_events::testing::{ arbitrary_eth_address, arbitrary_keccak_hash, arbitrary_nonce, @@ -570,34 +645,105 @@ mod tests { update_epoch_parameter(wl_storage, &epoch_duration) .expect("Test failed"); // set native ERC20 token - let native_erc20_key = bridge_storage::native_erc20_key(); - let native_erc20 = EthAddress([0; 20]); wl_storage - .write_bytes(&native_erc20_key, encode(&native_erc20)) + .write_bytes(&bridge_storage::native_erc20_key(), encode(&wnam())) .expect("Test failed"); } + /// Helper data structure to feed to [`init_bridge_pool_transfers`]. + struct TransferData { + kind: eth_bridge_pool::TransferToEthereumKind, + gas_token: Address, + } + + impl Default for TransferData { + fn default() -> Self { + Self { + kind: eth_bridge_pool::TransferToEthereumKind::Erc20, + gas_token: nam(), + } + } + } + + /// Build [`TransferData`] values. + struct TransferDataBuilder { + kind: Option, + gas_token: Option
, + } + + #[allow(dead_code)] + impl TransferDataBuilder { + fn new() -> Self { + Self { + kind: None, + gas_token: None, + } + } + + fn kind( + mut self, + kind: eth_bridge_pool::TransferToEthereumKind, + ) -> Self { + self.kind = Some(kind); + self + } + + fn kind_erc20(self) -> Self { + self.kind(eth_bridge_pool::TransferToEthereumKind::Erc20) + } + + fn kind_nut(self) -> Self { + self.kind(eth_bridge_pool::TransferToEthereumKind::Nut) + } + + fn gas_token(mut self, address: Address) -> Self { + self.gas_token = Some(address); + self + } + + fn gas_erc20(self, address: &EthAddress) -> Self { + self.gas_token(wrapped_erc20s::token(address)) + } + + fn gas_nut(self, address: &EthAddress) -> Self { + self.gas_token(wrapped_erc20s::nut(address)) + } + + fn build(self) -> TransferData { + TransferData { + kind: self.kind.unwrap_or_else(|| TransferData::default().kind), + gas_token: self + .gas_token + .unwrap_or_else(|| TransferData::default().gas_token), + } + } + } + fn init_bridge_pool_transfers( wl_storage: &mut TestWlStorage, assets_transferred: A, ) -> Vec where - A: Into>, + A: Into>, { let sender = address::testing::established_address_1(); let payer = address::testing::established_address_2(); // set pending transfers let mut pending_transfers = vec![]; - for (i, asset) in assets_transferred.into().into_iter().enumerate() { + for (i, (asset, TransferData { kind, gas_token })) 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 { + token: gas_token, amount: Amount::from(1), payer: payer.clone(), }, @@ -619,7 +765,20 @@ mod tests { ) -> Vec { init_bridge_pool_transfers( wl_storage, - (0..2).map(|i| EthAddress([i; 20])).collect::>(), + (0..2) + .map(|i| { + ( + EthAddress([i; 20]), + TransferDataBuilder::new() + .kind(if i & 1 == 0 { + eth_bridge_pool::TransferToEthereumKind::Erc20 + } else { + eth_bridge_pool::TransferToEthereumKind::Nut + }) + .build(), + ) + }) + .collect::>(), ) } @@ -627,19 +786,26 @@ mod tests { wl_storage: &mut TestWlStorage, pending_transfers: &Vec, ) { - // Gas payer - let payer = address::testing::established_address_2(); - let payer_key = balance_key(&nam(), &payer); - let payer_balance = Amount::from(0); - wl_storage - .write_bytes( - &payer_key, - payer_balance.try_to_vec().expect("Test failed"), - ) + for transfer in pending_transfers { + // Gas + let payer = address::testing::established_address_2(); + let payer_key = balance_key(&transfer.gas_fee.token, &payer); + let payer_balance = Amount::from(0); + wl_storage + .write_bytes( + &payer_key, + payer_balance.try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); + let escrow_key = + balance_key(&transfer.gas_fee.token, &BRIDGE_POOL_ADDRESS); + update::amount(wl_storage, &escrow_key, |balance| { + let gas_fee = Amount::from_u64(1); + balance.receive(&gas_fee); + }) .expect("Test failed"); - for transfer in pending_transfers { - if transfer.transfer.asset == EthAddress([0; 20]) { + if transfer.transfer.asset == wnam() { // native ERC20 let sender_key = balance_key(&nam(), &transfer.transfer.sender); let sender_balance = Amount::from(0); @@ -658,7 +824,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 @@ -684,12 +850,6 @@ mod tests { ) .expect("Test failed"); }; - let gas_fee = Amount::from(1); - let escrow_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - update::amount(wl_storage, &escrow_key, |balance| { - balance.receive(&gas_fee); - }) - .expect("Test failed"); } } @@ -705,10 +865,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,44 +916,116 @@ 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); - let expected_amount = amount.try_to_vec().unwrap(); - for key in vec![receiver_balance_key, wdai_supply_key] { - let value = wl_storage.read_bytes(&key).unwrap(); - assert_matches!(value, Some(bytes) if bytes == expected_amount); + for key in vec![receiver_balance_key, wdai_supply_key] { + let value: Option = + wl_storage.read(&key).unwrap(); + if expected_amount.is_zero() { + assert_matches!(value, None); + } else { + assert_matches!(value, Some(amount) if amount == expected_amount); + } + } + } } } + /// Test that if DAI is never whitelisted, we only mint NUTs. + #[test] + fn test_minting_dai_when_not_whitelisted() { + TestMintDai { + dai_token_cap: None, + transferred_amount: Amount::from(100), + } + .run_test(); + } + + /// Test that overrunning the token caps results in minting DAI NUTs, + /// along with wDAI. + #[test] + fn test_minting_dai_on_cap_overrun() { + TestMintDai { + dai_token_cap: Some(Amount::from(80)), + transferred_amount: Amount::from(100), + } + .run_test(); + } + + /// Test acting on a single "transfer to Namada" Ethereum event + /// and minting the first ever wDAI. + #[test] + fn test_minting_dai_wrapped() { + TestMintDai { + dai_token_cap: Some(Amount::max()), + transferred_amount: Amount::from(100), + } + .run_test(); + } + #[test] /// When we act on an [`EthereumEvent::TransfersToEthereum`], test /// that pending transfers are deleted from the Bridge pool, the @@ -810,53 +1038,103 @@ 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 random_erc20_3 = EthAddress([0xdd; 20]); + let random_erc20_token_3 = wrapped_erc20s::token(&random_erc20_3); + let random_erc20_4 = EthAddress([0xcc; 20]); + let random_erc20_token_4 = wrapped_erc20s::nut(&random_erc20_4); + let erc20_gas_addr = EthAddress([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, + ]); let pending_transfers = init_bridge_pool_transfers( &mut wl_storage, - [native_erc20, random_erc20], + [ + (native_erc20, TransferData::default()), + (random_erc20, TransferDataBuilder::new().kind_nut().build()), + ( + random_erc20_2, + TransferDataBuilder::new().kind_erc20().build(), + ), + ( + random_erc20_3, + TransferDataBuilder::new() + .kind_erc20() + .gas_erc20(&erc20_gas_addr) + .build(), + ), + ( + random_erc20_4, + TransferDataBuilder::new() + .kind_nut() + .gas_erc20(&erc20_gas_addr) + .build(), + ), + ], ); init_balance(&mut wl_storage, &pending_transfers); let pending_keys: HashSet = pending_transfers.iter().map(get_pending_key).collect(); let relayer = gen_established_address("random"); - let mut transfers = vec![]; - for transfer in pending_transfers { - let transfer_to_eth = TransferToEthereum { - amount: transfer.transfer.amount, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - gas_amount: transfer.gas_fee.amount, - gas_payer: transfer.gas_fee.payer, - sender: transfer.transfer.sender, - }; - transfers.push(transfer_to_eth); - } + let transfers: Vec<_> = pending_transfers + .iter() + .map(TransferToEthereum::from) + .collect(); let event = EthereumEvent::TransfersToEthereum { nonce: arbitrary_nonce(), valid_transfers_map: transfers.iter().map(|_| true).collect(), transfers, relayer: relayer.clone(), }; - let payer_balance_key = balance_key(&nam(), &relayer); - let pool_balance_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); - let mut bp_balance_pre = Amount::try_from_slice( + let payer_nam_balance_key = balance_key(&nam(), &relayer); + let payer_erc_balance_key = + balance_key(&wrapped_erc20s::token(&erc20_gas_addr), &relayer); + let pool_nam_balance_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); + let pool_erc_balance_key = balance_key( + &wrapped_erc20s::token(&erc20_gas_addr), + &BRIDGE_POOL_ADDRESS, + ); + let mut bp_nam_balance_pre = Amount::try_from_slice( &wl_storage - .read_bytes(&pool_balance_key) + .read_bytes(&pool_nam_balance_key) + .expect("Test failed") + .expect("Test failed"), + ) + .expect("Test failed"); + let mut bp_erc_balance_pre = Amount::try_from_slice( + &wl_storage + .read_bytes(&pool_erc_balance_key) .expect("Test failed") .expect("Test failed"), ) .expect("Test failed"); let mut changed_keys = act_on(&mut wl_storage, event).unwrap(); + for erc20 in [ + random_erc20_token, + random_erc20_token_2, + random_erc20_token_3, + random_erc20_token_4, + ] { + assert!( + changed_keys.remove(&balance_key(&erc20, &BRIDGE_POOL_ADDRESS)), + "Expected {erc20:?} Bridge pool balance to change" + ); + assert!( + changed_keys.remove(&minted_balance_key(&erc20)), + "Expected {erc20:?} minted supply to change" + ); + } assert!( - changed_keys.remove(&balance_key( - &random_erc20_token, - &BRIDGE_POOL_ADDRESS - )) + changed_keys + .remove(&minted_balance_key(&wrapped_erc20s::token(&wnam()))) ); - assert!(changed_keys.remove(&minted_balance_key(&random_erc20_token))); - assert!(changed_keys.remove(&payer_balance_key)); - assert!(changed_keys.remove(&pool_balance_key)); + assert!(changed_keys.remove(&payer_nam_balance_key)); + assert!(changed_keys.remove(&payer_erc_balance_key)); + assert!(changed_keys.remove(&pool_nam_balance_key)); + assert!(changed_keys.remove(&pool_erc_balance_key)); assert!(changed_keys.remove(&get_nonce_key())); assert!(changed_keys.iter().all(|k| pending_keys.contains(k))); @@ -869,23 +1147,45 @@ mod tests { // NOTE: we should have one write -- the bridge pool nonce update 1 ); - let relayer_balance = Amount::try_from_slice( + let relayer_nam_balance = Amount::try_from_slice( &wl_storage - .read_bytes(&payer_balance_key) + .read_bytes(&payer_nam_balance_key) .expect("Test failed: read error") .expect("Test failed: no value in storage"), ) .expect("Test failed"); - assert_eq!(relayer_balance, Amount::from(2)); - let bp_balance_post = Amount::try_from_slice( + assert_eq!(relayer_nam_balance, Amount::from(3)); + let relayer_erc_balance = Amount::try_from_slice( &wl_storage - .read_bytes(&pool_balance_key) + .read_bytes(&payer_erc_balance_key) .expect("Test failed: read error") .expect("Test failed: no value in storage"), ) .expect("Test failed"); - bp_balance_pre.spend(&bp_balance_post); - assert_eq!(bp_balance_pre, Amount::from(2)); + assert_eq!(relayer_erc_balance, Amount::from(2)); + + let bp_nam_balance_post = Amount::try_from_slice( + &wl_storage + .read_bytes(&pool_nam_balance_key) + .expect("Test failed: read error") + .expect("Test failed: no value in storage"), + ) + .expect("Test failed"); + let bp_erc_balance_post = Amount::try_from_slice( + &wl_storage + .read_bytes(&pool_erc_balance_key) + .expect("Test failed: read error") + .expect("Test failed: no value in storage"), + ) + .expect("Test failed"); + + bp_nam_balance_pre.spend(&bp_nam_balance_post); + assert_eq!(bp_nam_balance_pre, Amount::from(3)); + assert_eq!(bp_nam_balance_post, Amount::from(0)); + + bp_erc_balance_pre.spend(&bp_erc_balance_post); + assert_eq!(bp_erc_balance_pre, Amount::from(2)); + assert_eq!(bp_erc_balance_post, Amount::from(0)); } #[test] @@ -912,8 +1212,10 @@ 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 { + token: nam(), amount: Amount::from(1), payer: address::testing::established_address_1(), }, @@ -969,7 +1271,7 @@ mod tests { // Check the balances for transfer in pending_transfers { - if transfer.transfer.asset == EthAddress([0; 20]) { + if transfer.transfer.asset == wnam() { let sender_key = balance_key(&nam(), &transfer.transfer.sender); let value = wl_storage.read_bytes(&sender_key).expect("Test failed"); @@ -985,7 +1287,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"); @@ -1011,27 +1313,45 @@ mod tests { let receiver = address::testing::established_address_1(); let amount = Amount::from(100); + // pre wNAM balance - 0 + let receiver_wnam_balance_key = + token::balance_key(&wrapped_erc20s::token(&wnam()), &receiver); + assert!( + wl_storage + .read_bytes(&receiver_wnam_balance_key) + .unwrap() + .is_none() + ); + let bridge_pool_initial_balance = Amount::from(100_000_000); let bridge_pool_native_token_balance_key = token::balance_key( &wl_storage.storage.native_token, &BRIDGE_ADDRESS, ); + let bridge_pool_native_erc20_supply_key = + minted_balance_key(&wrapped_erc20s::token(&wnam())); StorageWrite::write( &mut wl_storage, &bridge_pool_native_token_balance_key, bridge_pool_initial_balance, )?; + StorageWrite::write( + &mut wl_storage, + &bridge_pool_native_erc20_supply_key, + amount, + )?; let receiver_native_token_balance_key = token::balance_key(&wl_storage.storage.native_token, &receiver); let changed_keys = - redeem_native_token(&mut wl_storage, &receiver, &amount)?; + redeem_native_token(&mut wl_storage, &wnam(), &receiver, &amount)?; assert_eq!( changed_keys, BTreeSet::from([ bridge_pool_native_token_balance_key.clone(), - receiver_native_token_balance_key.clone() + receiver_native_token_balance_key.clone(), + bridge_pool_native_erc20_supply_key.clone(), ]) ); assert_eq!( @@ -1045,6 +1365,23 @@ mod tests { StorageRead::read(&wl_storage, &receiver_native_token_balance_key)?, Some(amount) ); + assert_eq!( + StorageRead::read( + &wl_storage, + &bridge_pool_native_erc20_supply_key + )?, + Some(Amount::zero()) + ); + + // post wNAM balance - 0 + // + // wNAM is never minted, it's converted back to NAM + assert!( + wl_storage + .read_bytes(&receiver_wnam_balance_key) + .unwrap() + .is_none() + ); Ok(()) } @@ -1063,27 +1400,38 @@ 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, TransferData::default()), + ( + EthAddress([0xaa; 20]), + TransferDataBuilder::new().kind_erc20().build(), + ), + ( + EthAddress([0xbb; 20]), + TransferDataBuilder::new().kind_nut().build(), + ), + ( + EthAddress([0xcc; 20]), + TransferDataBuilder::new().kind_erc20().build(), + ), + ( + EthAddress([0xdd; 20]), + TransferDataBuilder::new().kind_nut().build(), + ), + ( + EthAddress([0xee; 20]), + TransferDataBuilder::new().kind_erc20().build(), + ), + ( + EthAddress([0xff; 20]), + TransferDataBuilder::new().kind_nut().build(), + ), ], ); init_balance(&mut wl_storage, &pending_transfers); let (transfers, valid_transfers_map) = pending_transfers .into_iter() - .map(|transfer| { - let transfer_to_eth = TransferToEthereum { - amount: transfer.transfer.amount, - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - gas_amount: transfer.gas_fee.amount, - gas_payer: transfer.gas_fee.payer, - sender: transfer.transfer.sender, - }; + .map(|ref transfer| { + let transfer_to_eth: TransferToEthereum = transfer.into(); (transfer_to_eth, true) }) .unzip(); @@ -1106,6 +1454,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 +1467,50 @@ 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( + |event @ TransferToEthereum { asset, amount, .. }| { + if asset == &native_erc20 { + return None; + } + let kind = { + let (pending, _) = wl_storage + .ethbridge_queries() + .lookup_transfer_to_eth(event) + .expect("Test failed"); + pending.transfer.kind + }; + let erc20_token = match &kind { + eth_bridge_pool::TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(asset) + } + 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, + 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 +1526,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)) @@ -1207,18 +1584,20 @@ mod tests { _ = act_on(wl_storage, event).unwrap(); - // check post supply + // check post supply - the wNAM minted supply should increase + // by the transferred amount assert!( wl_storage .read_bytes(&balance_key(&wnam, &BRIDGE_POOL_ADDRESS)) .expect("Test failed") .is_none() ); - assert!( + assert_eq!( wl_storage - .read_bytes(&minted_balance_key(&wnam)) - .expect("Test failed") - .is_none() + .read::(&minted_balance_key(&wnam)) + .expect("Reading from storage should not fail") + .expect("The wNAM supply should have been updated"), + Amount::from_u64(10), ); // check post balance @@ -1230,4 +1609,31 @@ mod tests { assert_eq!(pre_escrowed_balance, post_escrowed_balance); }) } + + /// Test that the ledger appropriately panics when we try to mint + /// wrapped NAM NUTs. Under normal circumstances, this should never + /// happen. + #[test] + #[should_panic(expected = "Attempted to mint wNAM NUTs!")] + fn test_wnam_doesnt_mint_nuts() { + let mut wl_storage = TestWlStorage::default(); + test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + + let transfer = PendingTransfer { + transfer: eth_bridge_pool::TransferToEthereum { + asset: wnam(), + sender: address::testing::established_address_1(), + recipient: EthAddress([5; 20]), + amount: Amount::from(10), + kind: eth_bridge_pool::TransferToEthereumKind::Nut, + }, + gas_fee: GasFee { + token: nam(), + amount: Amount::from(1), + payer: address::testing::established_address_1(), + }, + }; + + _ = update_transferred_asset_balances(&mut wl_storage, &transfer); + } } diff --git a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs index d3cd32972d..c8bd21a4bf 100644 --- a/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs +++ b/ethereum_bridge/src/protocol/transactions/ethereum_events/mod.rs @@ -330,6 +330,16 @@ mod tests { )]); let mut wl_storage = TestWlStorage::default(); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let changed_keys = apply_updates(&mut wl_storage, updates, voting_powers)?; @@ -405,6 +415,16 @@ mod tests { vec![(sole_validator.clone(), Amount::native_whole(100))], )); test_utils::bootstrap_ethereum_bridge(&mut wl_storage); + test_utils::whitelist_tokens( + &mut wl_storage, + [( + DAI_ERC20_ETH_ADDRESS, + test_utils::WhitelistMeta { + cap: Amount::max(), + denom: 18, + }, + )], + ); let receiver = address::testing::established_address_1(); let event = EthereumEvent::TransfersToNamada { diff --git a/ethereum_bridge/src/storage/eth_bridge_queries.rs b/ethereum_bridge/src/storage/eth_bridge_queries.rs index 827745def0..5d068d2e38 100644 --- a/ethereum_bridge/src/storage/eth_bridge_queries.rs +++ b/ethereum_bridge/src/storage/eth_bridge_queries.rs @@ -1,16 +1,19 @@ use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::hints; -use namada_core::ledger::eth_bridge::storage::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, bridge_pool, whitelist, }; use namada_core::ledger::storage; use namada_core::ledger::storage::{StoreType, WlStorage}; use namada_core::ledger::storage_api::StorageRead; use namada_core::types::address::Address; -use namada_core::types::ethereum_events::{EthAddress, GetEventNonce, Uint}; +use namada_core::types::eth_abi::Encode; +use namada_core::types::eth_bridge_pool::PendingTransfer; +use namada_core::types::ethereum_events::{ + EthAddress, GetEventNonce, TransferToEthereum, Uint, +}; use namada_core::types::keccak::KeccakHash; -use namada_core::types::storage::{BlockHeight, Epoch}; +use namada_core::types::storage::{BlockHeight, Epoch, Key as StorageKey}; use namada_core::types::token; use namada_core::types::vote_extensions::validator_set_update::{ EthAddrBook, ValidatorSetArgs, VotingPowersMap, VotingPowersMapExt, @@ -175,7 +178,7 @@ where &self .wl_storage .storage - .read(&get_nonce_key()) + .read(&bridge_pool::get_nonce_key()) .expect("Reading Bridge pool nonce shouldn't fail.") .0 .expect("Reading Bridge pool nonce shouldn't fail."), @@ -191,7 +194,7 @@ where .storage .db .read_subspace_val_with_height( - &get_nonce_key(), + &bridge_pool::get_nonce_key(), height, self.wl_storage.storage.get_last_block_height(), ) @@ -217,7 +220,7 @@ where /// root and nonce. /// /// Also returns the block height at which the - /// a quorum of signatures was collected. + /// Bridge pool root was originally signed. /// /// No value exists when the bridge if first /// started. @@ -225,7 +228,7 @@ where self, ) -> Option<(BridgePoolRootProof, BlockHeight)> { self.wl_storage - .read_bytes(&get_signed_root_key()) + .read_bytes(&bridge_pool::get_signed_root_key()) .expect("Reading signed Bridge pool root shouldn't fail.") .map(|bytes| { BorshDeserialize::try_from_slice(&bytes).expect( @@ -392,6 +395,139 @@ 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(), + } + } + + /// Given a [`TransferToEthereum`] event, look-up the corresponding + /// [`PendingTransfer`]. + pub fn lookup_transfer_to_eth( + self, + transfer: &TransferToEthereum, + ) -> Option<(PendingTransfer, StorageKey)> { + let pending_key = bridge_pool::get_key_from_hash(&transfer.keccak256()); + self.wl_storage + .read(&pending_key) + .expect("Reading from storage should not fail") + .zip(Some(pending_key)) + } +} + +/// Number of tokens to mint after receiving a "transfer +/// 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, +} + +impl EthAssetMint { + /// Check if NUTs should be minted. + #[inline] + pub fn should_mint_nuts(&self) -> bool { + !self.nut_amount.is_zero() + } + + /// Check if ERC20s should be minted. + #[inline] + pub fn should_mint_erc20s(&self) -> bool { + !self.erc20_amount.is_zero() + } } /// A handle to the Ethereum addresses of the set of consensus 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 2d426d5510..bc7e0c0342 100644 --- a/shared/src/ledger/args.rs +++ b/shared/src/ledger/args.rs @@ -1,5 +1,6 @@ //! Structures encapsulating SDK arguments +use std::collections::HashMap; use std::path::PathBuf; use std::time::Duration as StdDuration; @@ -7,6 +8,7 @@ use namada_core::types::chain::ChainId; use namada_core::types::dec::Dec; use namada_core::types::ethereum_events::EthAddress; use namada_core::types::time::DateTimeUtc; +use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; use crate::ibc::core::ics24_host::identifier::{ChannelId, PortId}; @@ -57,15 +59,29 @@ pub trait NamadaTypes: Clone + std::fmt::Debug { type TransferTarget: Clone + std::fmt::Debug; /// Represents some data that is used in a transaction type Data: Clone + std::fmt::Debug; + /// Bridge pool recommendations conversion rates table. + type BpConversionTable: Clone + std::fmt::Debug; } /// The concrete types being used in Namada SDK #[derive(Clone, Debug)] pub struct SdkTypes; +/// An entry in the Bridge pool recommendations conversion +/// rates table. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BpConversionTableEntry { + /// An alias for the token, or the string representation + /// of its address if none is available. + pub alias: String, + /// Conversion rate from the given token to gwei. + pub conversion_rate: f64, +} + impl NamadaTypes for SdkTypes { type Address = Address; type BalanceOwner = namada_core::types::masp::BalanceOwner; + type BpConversionTable = HashMap; type Data = Vec; type EthereumAddress = (); type Keypair = namada_core::types::key::common::SecretKey; @@ -694,13 +710,18 @@ pub struct RecommendBatch { /// An optional parameter indicating how much net /// gas the relayer is willing to pay. pub gas: Option, - /// Estimate of amount of NAM a single ETH is worth. - pub nam_per_eth: f64, + /// Bridge pool recommendations conversion rates table. + pub conversion_table: C::BpConversionTable, } /// 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 @@ -711,10 +732,14 @@ pub struct EthereumBridgePool { pub sender: C::Address, /// The amount to be transferred pub amount: InputAmount, - /// The amount of fees (in NAM) - pub fee_amount: token::Amount, + /// The amount of gas fees + pub fee_amount: InputAmount, /// The account of fee payer. - pub fee_payer: C::Address, + /// + /// If unset, it is the same as the sender. + pub fee_payer: Option, + /// The token in which the gas is being paid + pub fee_token: C::Address, /// Path to the tx WASM code file pub code_path: PathBuf, } diff --git a/shared/src/ledger/eth_bridge/bridge_pool.rs b/shared/src/ledger/eth_bridge/bridge_pool.rs index 5b8a8eaa5e..829c868e27 100644 --- a/shared/src/ledger/eth_bridge/bridge_pool.rs +++ b/shared/src/ledger/eth_bridge/bridge_pool.rs @@ -1,5 +1,6 @@ //! Bridge pool SDK functionality. +use std::borrow::Cow; use std::cmp::Ordering; use std::collections::HashMap; use std::io::Write; @@ -8,7 +9,7 @@ use std::sync::Arc; use borsh::BorshSerialize; use ethbridge_bridge_contract::Bridge; use ethers::providers::Middleware; -use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; +use namada_core::ledger::eth_bridge::storage::wrapped_erc20s; use namada_core::types::key::common; use owo_colors::OwoColorize; use serde::{Deserialize, Serialize}; @@ -17,7 +18,9 @@ use super::{block_on_eth_sync, eth_sync_or_exit, BlockOnEthSync}; use crate::eth_bridge::ethers::abi::AbiDecode; use crate::eth_bridge::structs::RelayProof; use crate::ledger::args; -use crate::ledger::queries::{Client, RPC}; +use crate::ledger::queries::{ + Client, GenBridgePoolProofReq, GenBridgePoolProofRsp, RPC, +}; use crate::ledger::rpc::{query_wasm_code_hash, validate_amount}; use crate::ledger::tx::{prepare_tx, Error}; use crate::proto::Tx; @@ -28,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}; @@ -39,29 +42,48 @@ pub async fn build_bridge_pool_tx( client: &C, args::EthereumBridgePool { tx: tx_args, + nut, asset, recipient, sender, amount, fee_amount, fee_payer, + fee_token, code_path, }: args::EthereumBridgePool, gas_payer: common::PublicKey, ) -> Result { - let DenominatedAmount { amount, .. } = - validate_amount(client, amount, &BRIDGE_ADDRESS, tx_args.force) - .await - .expect("Failed to validate amount"); - + let fee_payer = fee_payer.unwrap_or_else(|| sender.clone()); + let DenominatedAmount { amount, .. } = validate_amount( + client, + amount, + &wrapped_erc20s::token(&asset), + tx_args.force, + ) + .await + .ok_or_else(|| Error::Other("Failed to validate amount".into()))?; + let DenominatedAmount { + amount: fee_amount, .. + } = validate_amount(client, fee_amount, &fee_token, tx_args.force) + .await + .ok_or_else(|| { + Error::Other("Failed to validate Bridge pool fee amount".into()) + })?; let transfer = PendingTransfer { transfer: TransferToEthereum { asset, recipient, - sender: sender.clone(), + sender, amount, + kind: if nut { + TransferToEthereumKind::Nut + } else { + TransferToEthereumKind::Erc20 + }, }, gas_fee: GasFee { + token: fee_token, amount: fee_amount, payer: fee_payer, }, @@ -173,9 +195,8 @@ where /// bridge pool. async fn construct_bridge_pool_proof( client: &C, - transfers: &[KeccakHash], - relayer: Address, -) -> Halt> + args: GenBridgePoolProofReq<'_, '_>, +) -> Halt where C: Client + Sync, { @@ -189,9 +210,9 @@ where let warnings: Vec<_> = in_progress .into_iter() .filter_map(|(ref transfer, voting_power)| { - if voting_power > FractionalVotingPower::ONE_THIRD { - let hash = PendingTransfer::from(transfer).keccak256(); - transfers.contains(&hash).then_some(hash) + if voting_power >= FractionalVotingPower::ONE_THIRD { + let hash = transfer.keccak256(); + args.transfers.contains(&hash).then_some(hash) } else { None } @@ -228,7 +249,7 @@ where } } - let data = (transfers, relayer).try_to_vec().unwrap(); + let data = args.try_to_vec().unwrap(); let response = RPC .shell() .eth_bridge() @@ -236,7 +257,7 @@ where .await; response.map(|response| response.data).try_halt(|e| { - println!("Encountered error constructing proof:\n{:?}", e); + println!("Encountered error constructing proof:\n{e}"); }) } @@ -245,7 +266,7 @@ where struct BridgePoolProofResponse { hashes: Vec, relayer_address: Address, - total_fees: Amount, + total_fees: HashMap, abi_encoded_proof: Vec, } @@ -259,25 +280,37 @@ pub async fn construct_proof( where C: Client + Sync, { - let bp_proof_bytes = construct_bridge_pool_proof( + let GenBridgePoolProofRsp { + abi_encoded_proof: bp_proof_bytes, + appendices, + } = construct_bridge_pool_proof( client, - &args.transfers, - args.relayer.clone(), + GenBridgePoolProofReq { + transfers: args.transfers.as_slice().into(), + relayer: Cow::Borrowed(&args.relayer), + with_appendix: true, + }, ) .await?; - let bp_proof: RelayProof = - AbiDecode::decode(&bp_proof_bytes).try_halt(|error| { - println!("Unable to decode the generated proof: {:?}", error); - })?; let resp = BridgePoolProofResponse { hashes: args.transfers, relayer_address: args.relayer, - total_fees: bp_proof - .transfers - .iter() - .map(|t| t.fee.as_u64()) - .sum::() - .into(), + total_fees: appendices + .map(|appendices| { + appendices.into_iter().fold( + HashMap::new(), + |mut total_fees, app| { + let GasFee { token, amount, .. } = + app.gas_fee.into_owned(); + let fees = total_fees + .entry(token) + .or_insert_with(Amount::zero); + fees.receive(&amount); + total_fees + }, + ) + }) + .unwrap_or_default(), abi_encoded_proof: bp_proof_bytes, }; println!("{}", serde_json::to_string(&resp).unwrap()); @@ -310,9 +343,18 @@ where eth_sync_or_exit(&*eth_client).await?; } - let bp_proof = - construct_bridge_pool_proof(nam_client, &args.transfers, args.relayer) - .await?; + let GenBridgePoolProofRsp { + abi_encoded_proof: bp_proof, + .. + } = construct_bridge_pool_proof( + nam_client, + GenBridgePoolProofReq { + transfers: Cow::Owned(args.transfers), + relayer: Cow::Owned(args.relayer), + with_appendix: false, + }, + ) + .await?; let bridge = match RPC .shell() .eth_bridge() @@ -394,12 +436,17 @@ where } mod recommendations { + use std::collections::BTreeSet; + use borsh::BorshDeserialize; use namada_core::types::uint::{self, Uint, I256}; use super::*; - use crate::eth_bridge::storage::bridge_pool::get_signed_root_key; + use crate::eth_bridge::storage::bridge_pool::{ + get_nonce_key, get_signed_root_key, + }; use crate::eth_bridge::storage::proof::BridgePoolRootProof; + use crate::types::ethereum_events::Uint as EthUint; use crate::types::storage::BlockHeight; use crate::types::vote_extensions::validator_set_update::{ EthAddrBook, VotingPowersMap, VotingPowersMapExt, @@ -444,6 +491,40 @@ mod recommendations { Generous, } + /// Transfer to Ethereum that is eligible to be recommended + /// for a relay operation, generating a profit. + /// + /// This means that the underlying Ethereum event has not + /// been "seen" yet, and that the user provided appropriate + /// conversion rates to gwei for the gas fee token in + /// the transfer. + #[derive(Debug, Eq, PartialEq)] + struct EligibleRecommendation { + /// Pending transfer to Ethereum. + pending_transfer: PendingTransfer, + /// Hash of the [`PendingTransfer`]. + transfer_hash: String, + /// Cost of relaying the transfer, in gwei. + cost: I256, + } + + /// Batch of recommended transfers to Ethereum that generate + /// a profit after a relay operation. + #[derive(Debug, Eq, PartialEq)] + struct RecommendedBatch { + /// Hashes of the recommended transfers to be relayed. + transfer_hashes: Vec, + /// Estimate of the total fees, measured in gwei, that will be paid + /// on Ethereum. + ethereum_gas_fees: Uint, + /// Net profitt in gwei, based on the conversion rates provided + /// to the algorithm. + net_profit: I256, + /// Gas fees paid by the transfers considered for relaying, + /// paid in various token types. + bridge_pool_gas_fees: HashMap, + } + /// Recommend the most economical batch of transfers to relay based /// on a conversion rate estimates from NAM to ETH and gas usage /// heuristics. @@ -462,9 +543,9 @@ mod recommendations { .transfer_to_ethereum_progress(client) .await .unwrap() - .keys() - .map(PendingTransfer::from) - .collect::>(); + .into_keys() + .map(|pending| pending.keccak256().to_string()) + .collect::>(); // get the signed bridge pool root so we can analyze the signatures // the estimate the gas cost of verifying them. @@ -474,15 +555,41 @@ mod recommendations { .storage_value( client, None, - Some(0.into()), + None, false, &get_signed_root_key(), ) .await - .unwrap() + .try_halt(|err| { + eprintln!("Failed to query Bridge pool proof: {err}"); + })? .data, ) - .unwrap(); + .try_halt(|err| { + eprintln!("Failed to decode Bridge pool proof: {err}"); + })?; + + // get the latest bridge pool nonce + let latest_bp_nonce = EthUint::try_from_slice( + &RPC.shell() + .storage_value(client, None, None, false, &get_nonce_key()) + .await + .try_halt(|err| { + eprintln!("Failed to query Bridge pool nonce: {err}"); + })? + .data, + ) + .try_halt(|err| { + eprintln!("Failed to decode Bridge pool nonce: {err}"); + })?; + + if latest_bp_nonce != bp_root.data.1 { + eprintln!( + "The signed Bridge pool nonce is not up to date, repeat this \ + query at a later time" + ); + return control_flow::halt(); + } // Get the voting powers of each of validator who signed // the above root. @@ -499,43 +606,47 @@ mod recommendations { let validator_gas = signature_fee() * signature_checks(voting_powers, &bp_root.signatures) + valset_fee() * valset_size; - // This is the amount of gwei a single name is worth - let gwei_per_nam = Uint::from_u64( - (10u64.pow(9) as f64 / args.nam_per_eth).floor() as u64, - ); // we don't recommend transfers that have already been relayed - let mut contents: Vec<(String, I256, PendingTransfer)> = - query_signed_bridge_pool(client) - .await? - .into_iter() - .filter_map(|(k, v)| { - if !in_progress.contains(&v) { - Some(( - k, - I256::try_from(v.gas_fee.amount * gwei_per_nam) - .map(|cost| transfer_fee() - cost) - .try_halt(|err| { - tracing::debug!(%err, "Failed to convert value to I256"); - }), - v, - )) - } else { - None - } - }) - .try_fold(Vec::new(), |mut accum, (hash, cost, transf)| { - accum.push((hash, cost?, transf)); - control_flow::proceed(accum) - })?; - - // sort transfers in decreasing amounts of profitability - contents.sort_by_key(|(_, cost, _)| *cost); + let eligible = generate_eligible( + &args.conversion_table, + &in_progress, + query_signed_bridge_pool(client).await?, + )?; let max_gas = args.max_gas.map(Uint::from_u64).unwrap_or(uint::MAX_VALUE); let max_cost = args.gas.map(I256::from).unwrap_or_default(); - generate(contents, validator_gas, max_gas, max_cost)?; + + generate_recommendations( + eligible, + &args.conversion_table, + validator_gas, + max_gas, + max_cost, + )? + .map( + |RecommendedBatch { + transfer_hashes, + ethereum_gas_fees, + net_profit, + bridge_pool_gas_fees, + }| { + println!("Recommended batch: {transfer_hashes:#?}"); + println!( + "Estimated Ethereum transaction gas (in gwei): \ + {ethereum_gas_fees}", + ); + println!("Estimated net profit (in gwei): {net_profit}"); + println!("Total fees: {bridge_pool_gas_fees:#?}"); + }, + ) + .unwrap_or_else(|| { + println!( + "Unable to find a recommendation satisfying the input \ + parameters." + ); + }); control_flow::proceed(()) } @@ -574,14 +685,94 @@ mod recommendations { ) } + /// Generate eligible recommendations. + fn generate_eligible( + conversion_table: &HashMap, + in_progress: &BTreeSet, + signed_pool: HashMap, + ) -> Halt> { + let mut eligible: Vec<_> = signed_pool + .into_iter() + .filter_map(|(pending_hash, pending)| { + if in_progress.contains(&pending_hash) { + return None; + } + + let conversion_rate = conversion_table + .get(&pending.gas_fee.token) + .and_then(|entry| match entry.conversion_rate { + r if r == 0.0f64 => { + eprintln!( + "{}: Ignoring null conversion rate", + pending.gas_fee.token, + ); + None + } + r if r < 0.0f64 => { + eprintln!( + "{}: Ignoring negative conversion rate: {r:.1}", + pending.gas_fee.token, + ); + None + } + r if r > 1e9 => { + eprintln!( + "{}: Ignoring high conversion rate: {r:.1} > \ + 10^9", + pending.gas_fee.token, + ); + None + } + r => Some(r), + })?; + + // This is the amount of gwei a single gas token is worth + let gwei_per_gas_token = + Uint::from_u64((1e9 / conversion_rate).floor() as u64); + + Some( + Uint::from(pending.gas_fee.amount) + .checked_mul(gwei_per_gas_token) + .ok_or_else(|| { + "Overflowed calculating earned gwei".into() + }) + .and_then(I256::try_from) + .map_err(|err| err.to_string()) + .and_then(|amt_of_earned_gwei| { + transfer_fee() + .checked_sub(&amt_of_earned_gwei) + .ok_or_else(|| { + "Underflowed calculating relaying cost" + .into() + }) + }) + .map(|cost| EligibleRecommendation { + cost, + pending_transfer: pending, + transfer_hash: pending_hash, + }), + ) + }) + .collect::, _>>() + .try_halt(|err| { + tracing::debug!(%err, "Failed to calculate relaying cost"); + })?; + + // sort transfers in increasing amounts of profitability + eligible.sort_by_key(|EligibleRecommendation { cost, .. }| *cost); + + control_flow::proceed(eligible) + } + /// Generates the actual recommendation from restrictions given by the /// input parameters. - fn generate( - contents: Vec<(String, I256, PendingTransfer)>, + fn generate_recommendations( + contents: Vec, + conversion_table: &HashMap, validator_gas: Uint, max_gas: Uint, max_cost: I256, - ) -> Halt>> { + ) -> Halt> { let mut state = AlgorithState { profitable: true, feasible_region: false, @@ -597,13 +788,16 @@ mod recommendations { let mut total_cost = I256::try_from(validator_gas).try_halt(|err| { tracing::debug!(%err, "Failed to convert value to I256"); })?; - let mut total_fees = uint::ZERO; + let mut total_fees = HashMap::new(); let mut recommendation = vec![]; - for (hash, cost, transfer) in contents.into_iter() { + for EligibleRecommendation { + cost, + transfer_hash: hash, + pending_transfer: transfer, + } in contents.into_iter() + { let next_total_gas = total_gas + unsigned_transfer_fee(); let next_total_cost = total_cost + cost; - let next_total_fees = - total_fees + Uint::from(transfer.gas_fee.amount); if cost.is_negative() { if next_total_gas <= max_gas && next_total_cost <= max_cost { state.feasible_region = true; @@ -628,29 +822,40 @@ mod recommendations { } total_cost = next_total_cost; total_gas = next_total_gas; - total_fees = next_total_fees; + update_total_fees(&mut total_fees, transfer, conversion_table); } control_flow::proceed( if state.feasible_region && !recommendation.is_empty() { - println!("Recommended batch: {:#?}", recommendation); - println!( - "Estimated Ethereum transaction gas (in gwei): {}", - total_gas - ); - println!("Estimated net profit (in gwei): {}", -total_cost); - println!("Total fees (in NAM): {}", total_fees); - Some(recommendation) + Some(RecommendedBatch { + transfer_hashes: recommendation, + ethereum_gas_fees: total_gas, + net_profit: -total_cost, + bridge_pool_gas_fees: total_fees, + }) } else { - println!( - "Unable to find a recommendation satisfying the input \ - parameters." - ); None }, ) } + fn update_total_fees( + total_fees: &mut HashMap, + transfer: PendingTransfer, + conversion_table: &HashMap, + ) { + let GasFee { token, amount, .. } = transfer.gas_fee; + let fees = total_fees + .entry( + conversion_table + .get(&token) + .map(|entry| entry.alias.clone()) + .unwrap_or_else(|| token.to_string()), + ) + .or_insert(uint::ZERO); + *fees += Uint::from(amount); + } + #[cfg(test)] mod test_recommendations { use namada_core::types::address::Address; @@ -672,31 +877,31 @@ 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(), amount: Default::default(), }, gas_fee: GasFee { + token: namada_core::types::address::nam(), amount: gas_amount.into(), payer: bertha_address(), }, } } - /// Convert transfers into a format that the `generate` function - /// understands. + /// Convert transfers into a format that the + /// [`generate_recommendations`] function understands. fn process_transfers( transfers: Vec, - ) -> Vec<(String, I256, PendingTransfer)> { + ) -> Vec { transfers .into_iter() - .map(|t| { - ( - t.keccak256().to_string(), - transfer_fee() - t.gas_fee.amount.change(), - t, - ) + .map(|t| EligibleRecommendation { + cost: transfer_fee() - t.gas_fee.amount.change(), + transfer_hash: t.keccak256().to_string(), + pending_transfer: t, }) .collect() } @@ -708,6 +913,114 @@ mod recommendations { } } + /// Data to pass to the [`test_generate_eligible_aux`] callback. + struct TestGenerateEligible<'a> { + pending: &'a PendingTransfer, + conversion_table: + &'a mut HashMap, + in_progress: &'a mut BTreeSet, + signed_pool: &'a mut HashMap, + expected_eligible: &'a mut Vec, + } + + impl TestGenerateEligible<'_> { + /// Add ETH to a conversion table. + fn add_eth_to_conversion_table(&mut self) { + self.conversion_table.insert( + namada_core::types::address::eth(), + args::BpConversionTableEntry { + alias: "ETH".into(), + conversion_rate: 1e9, // 1 ETH = 1e9 GWEI + }, + ); + } + } + + /// Helper function to test [`generate_eligible`]. + fn test_generate_eligible_aux( + mut callback: F, + ) -> Vec + where + F: FnMut(TestGenerateEligible<'_>), + { + let pending = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: EthAddress([1; 20]), + recipient: EthAddress([2; 20]), + sender: bertha_address(), + amount: Default::default(), + }, + gas_fee: GasFee { + token: namada_core::types::address::eth(), + amount: 1_000_000_000_u64.into(), // 1 GWEI + payer: bertha_address(), + }, + }; + let mut table = HashMap::new(); + let mut in_progress = BTreeSet::new(); + let mut signed_pool = HashMap::new(); + let mut expected = vec![]; + callback(TestGenerateEligible { + pending: &pending, + conversion_table: &mut table, + in_progress: &mut in_progress, + signed_pool: &mut signed_pool, + expected_eligible: &mut expected, + }); + let eligible = + generate_eligible(&table, &in_progress, signed_pool).proceed(); + assert_eq!(eligible, expected); + eligible + } + + /// Test the happy path of generating eligible recommendations + /// for Bridge pool relayed transfers. + #[test] + fn test_generate_eligible_happy_path() { + test_generate_eligible_aux(|mut ctx| { + ctx.add_eth_to_conversion_table(); + ctx.signed_pool.insert( + ctx.pending.keccak256().to_string(), + ctx.pending.clone(), + ); + ctx.expected_eligible.push(EligibleRecommendation { + transfer_hash: ctx.pending.keccak256().to_string(), + cost: transfer_fee() + - I256::try_from(ctx.pending.gas_fee.amount) + .expect("Test failed"), + pending_transfer: ctx.pending.clone(), + }); + }); + } + + /// Test that a transfer is not recommended if it + /// is in the process of being relayed (has >0 voting + /// power behind it). + #[test] + fn test_generate_eligible_with_in_progress() { + test_generate_eligible_aux(|mut ctx| { + ctx.add_eth_to_conversion_table(); + ctx.signed_pool.insert( + ctx.pending.keccak256().to_string(), + ctx.pending.clone(), + ); + ctx.in_progress.insert(ctx.pending.keccak256().to_string()); + }); + } + + /// Test that a transfer is not recommended if its gas + /// token is not found in the conversion table. + #[test] + fn test_generate_eligible_no_gas_token() { + test_generate_eligible_aux(|ctx| { + ctx.signed_pool.insert( + ctx.pending.keccak256().to_string(), + ctx.pending.clone(), + ); + }); + } + #[test] fn test_signature_count() { let voting_powers = VotingPowersMap::from([ @@ -746,14 +1059,16 @@ mod recommendations { let profitable = vec![transfer(100_000); 17]; let hash = profitable[0].keccak256().to_string(); let expected = vec![hash; 17]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(profitable), + &Default::default(), Uint::from_u64(800_000), uint::MAX_VALUE, I256::zero(), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -763,14 +1078,16 @@ mod recommendations { let hash = transfers[0].keccak256().to_string(); transfers.push(transfer(0)); let expected: Vec<_> = vec![hash; 17]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), + &Default::default(), Uint::from_u64(800_000), uint::MAX_VALUE, I256::zero(), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -779,14 +1096,16 @@ mod recommendations { let transfers = vec![transfer(75_000); 4]; let hash = transfers[0].keccak256().to_string(); let expected = vec![hash; 2]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), + &Default::default(), Uint::from_u64(50_000), Uint::from_u64(150_000), I256(uint::MAX_SIGNED_VALUE), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -799,14 +1118,16 @@ mod recommendations { .map(|t| t.keccak256().to_string()) .take(5) .collect(); - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), + &Default::default(), Uint::from_u64(150_000), uint::MAX_VALUE, I256::from(20_000), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } @@ -816,22 +1137,25 @@ mod recommendations { let hash = transfers[0].keccak256().to_string(); let expected = vec![hash; 4]; transfers.extend([transfer(17_500), transfer(17_500)]); - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), + &Default::default(), Uint::from_u64(150_000), Uint::from_u64(330_000), I256::from(20_000), ) .proceed() - .expect("Test failed"); + .expect("Test failed") + .transfer_hashes; assert_eq!(recommendation, expected); } #[test] fn test_wholly_infeasible() { let transfers = vec![transfer(75_000); 4]; - let recommendation = generate( + let recommendation = generate_recommendations( process_transfers(transfers), + &Default::default(), Uint::from_u64(300_000), uint::MAX_VALUE, I256::from(20_000), @@ -839,6 +1163,97 @@ mod recommendations { .proceed(); assert!(recommendation.is_none()) } + + /// Test the profit margin obtained from relaying two + /// Bridge pool transfers with two distinct token types, + /// whose relation is 1:2 in value. + #[test] + fn test_conversion_table_profit_margin() { + // apfel is worth twice as much as schnitzel + const APF_RATE: f64 = 5e8; + const SCH_RATE: f64 = 1e9; + const APFEL: &str = "APF"; + const SCHNITZEL: &str = "SCH"; + + let conversion_table = { + let mut t = HashMap::new(); + t.insert( + namada_core::types::address::apfel(), + args::BpConversionTableEntry { + alias: APFEL.into(), + conversion_rate: APF_RATE, + }, + ); + t.insert( + namada_core::types::address::schnitzel(), + args::BpConversionTableEntry { + alias: SCHNITZEL.into(), + conversion_rate: SCH_RATE, + }, + ); + t + }; + + let eligible = test_generate_eligible_aux(|ctx| { + ctx.conversion_table.clone_from(&conversion_table); + // tune the pending transfer provided by the ctx + let transfer_paid_in_apfel = { + let mut pending = ctx.pending.clone(); + pending.transfer.amount = 1.into(); + pending.gas_fee.token = + namada_core::types::address::apfel(); + pending + }; + let transfer_paid_in_schnitzel = { + let mut pending = ctx.pending.clone(); + pending.transfer.amount = 2.into(); + pending.gas_fee.token = + namada_core::types::address::schnitzel(); + pending + }; + // add the transfers to the pool, and expect them to + // be eligible transfers + for (pending, rate) in [ + (transfer_paid_in_apfel, APF_RATE), + (transfer_paid_in_schnitzel, SCH_RATE), + ] { + ctx.signed_pool.insert( + pending.keccak256().to_string(), + pending.clone(), + ); + ctx.expected_eligible.push(EligibleRecommendation { + transfer_hash: pending.keccak256().to_string(), + cost: transfer_fee() + - I256::from((1e9 / rate).floor() as u64) + * I256::try_from(pending.gas_fee.amount) + .expect("Test failed"), + pending_transfer: pending, + }); + } + }); + + const VALIDATOR_GAS_FEE: Uint = Uint::from_u64(100_000); + + let recommended_batch = generate_recommendations( + eligible, + &conversion_table, + // gas spent by validator signature checks + VALIDATOR_GAS_FEE, + // unlimited amount of gas + uint::MAX_VALUE, + // only profitable + I256::zero(), + ) + .proceed() + .expect("Test failed"); + + assert_eq!( + recommended_batch.net_profit, + I256::from(1_000_000_000_u64) + I256::from(2_000_000_000_u64) + - I256(VALIDATOR_GAS_FEE) + - transfer_fee() * I256::from(2_u64) + ); + } } } diff --git a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs index 323633b563..3ae649e4be 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/bridge_pool_vp.rs @@ -10,24 +10,28 @@ //! correctly. This means that the appropriate data is //! added to the pool and gas fees are submitted appropriately //! and that tokens to be transferred are escrowed. + +use std::borrow::Cow; use std::collections::BTreeSet; +use std::marker::PhantomData; use borsh::BorshDeserialize; use eyre::eyre; +use namada_core::hints; use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_pending_key, is_bridge_pool_key, BRIDGE_POOL_ADDRESS, }; +use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::eth_bridge::ADDRESS as BRIDGE_ADDRESS; use namada_ethereum_bridge::parameters::read_native_erc20_address; 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}; use crate::ledger::storage::traits::StorageHasher; use crate::ledger::storage::{DBIter, DB}; use crate::proto::Tx; use crate::types::address::{Address, InternalAddress}; -use crate::types::eth_bridge_pool::PendingTransfer; +use crate::types::eth_bridge_pool::{PendingTransfer, TransferToEthereumKind}; use crate::types::ethereum_events::EthAddress; use crate::types::storage::Key; use crate::types::token::{balance_key, Amount}; @@ -39,11 +43,32 @@ use crate::vm::WasmCacheAccess; pub struct Error(#[from] eyre::Error); /// A positive or negative amount +#[derive(Copy, Clone)] enum SignedAmount { Positive(Amount), Negative(Amount), } +/// An [`Amount`] that has been updated with some delta value. +#[derive(Copy, Clone)] +struct AmountDelta { + /// The base [`Amount`], before applying the delta. + base: Amount, + /// The delta to be applied to the base amount. + delta: SignedAmount, +} + +impl AmountDelta { + /// Resolve the updated amount by applying the delta value. + #[inline] + fn resolve(self) -> Amount { + match self.delta { + SignedAmount::Positive(delta) => self.base + delta, + SignedAmount::Negative(delta) => self.base - delta, + } + } +} + /// Validity predicate for the Ethereum bridge pub struct BridgePoolVp<'ctx, D, H, CA> where @@ -63,92 +88,114 @@ where { /// Get the change in the balance of an account /// associated with an address - fn account_balance_delta(&self, address: &Address) -> Option { - let account_key = balance_key(&self.ctx.storage.native_token, address); + fn account_balance_delta( + &self, + token: &Address, + address: &Address, + ) -> Option { + let account_key = balance_key(token, address); let before: Amount = (&self.ctx) .read_pre_value(&account_key) - .unwrap_or_else(|error| { + .map_err(|error| { tracing::warn!(?error, %account_key, "reading pre value"); - None - })?; + }) + .ok()? + // NB: the previous balance of the given account might + // have been null. this is valid if the account is + // being credited, such as when we escrow gas under + // the Bridge pool + .unwrap_or_default(); let after: Amount = (&self.ctx) .read_post_value(&account_key) .unwrap_or_else(|error| { tracing::warn!(?error, %account_key, "reading post value"); None })?; - if before > after { - Some(SignedAmount::Negative(before - after)) - } else { - Some(SignedAmount::Positive(after - before)) - } + Some(AmountDelta { + base: before, + delta: if before > after { + SignedAmount::Negative(before - after) + } else { + SignedAmount::Positive(after - before) + }, + }) } - /// Check that the correct amount of erc20 assets were - /// sent from the correct account into escrow. - fn check_erc20s_escrowed( + /// Check that the correct amount of tokens were sent + /// from the correct account into escrow. + #[inline] + fn check_escrowed_toks( &self, - keys_changed: &BTreeSet, - transfer: &PendingTransfer, + delta: EscrowDelta, ) -> Result { - // check that the assets to be transferred were escrowed - let token = wrapped_erc20s::token(&transfer.transfer.asset); - let owner_key = balance_key(&token, &transfer.transfer.sender); - let escrow_key = balance_key(&token, &BRIDGE_POOL_ADDRESS); - if keys_changed.contains(&owner_key) - && keys_changed.contains(&escrow_key) - { - match check_balance_changes(&self.ctx, &owner_key, &escrow_key)? { - Some(amount) if amount == transfer.transfer.amount => Ok(true), - _ => { - tracing::debug!( - "The assets of the transfer were not properly \ - escrowed into the Ethereum bridge pool" - ); - Ok(false) - } - } - } else { - tracing::debug!( - "The assets of the transfer were not properly escrowed into \ - the Ethereum bridge pool." - ); - Ok(false) - } + self.check_escrowed_toks_balance(delta) + .map(|balance| balance.is_some()) } - /// Check that the correct amount of Nam was sent - /// from the correct account into escrow - fn check_nam_escrowed(&self, delta: EscrowDelta) -> Result { + /// Check that the correct amount of tokens were sent + /// from the correct account into escrow, and return + /// the updated escrow balance. + fn check_escrowed_toks_balance( + &self, + delta: EscrowDelta, + ) -> Result, Error> { let EscrowDelta { + token, payer_account, escrow_account, expected_debit, expected_credit, + .. } = delta; - let debited = self.account_balance_delta(payer_account); - let credited = self.account_balance_delta(escrow_account); + let debit = self.account_balance_delta(&token, payer_account); + let credit = self.account_balance_delta(&token, escrow_account); - match (debited, credited) { + match (debit, credit) { + // success case ( - Some(SignedAmount::Negative(debit)), - Some(SignedAmount::Positive(credit)), - ) => Ok(debit == expected_debit && credit == expected_credit), - (Some(SignedAmount::Positive(_)), _) => { + Some(AmountDelta { + delta: SignedAmount::Negative(debit), + .. + }), + Some( + escrow_balance @ AmountDelta { + delta: SignedAmount::Positive(credit), + .. + }, + ), + ) => Ok((debit == expected_debit && credit == expected_credit) + .then_some(escrow_balance)), + // user did not debit from their account + ( + Some(AmountDelta { + delta: SignedAmount::Positive(_), + .. + }), + _, + ) => { tracing::debug!( "The account {} was not debited.", payer_account ); - Ok(false) + Ok(None) } - (_, Some(SignedAmount::Negative(_))) => { + // user did not credit escrow account + ( + _, + Some(AmountDelta { + delta: SignedAmount::Negative(_), + .. + }), + ) => { tracing::debug!( "The Ethereum bridge pool's escrow was not credited from \ account {}.", payer_account ); - Ok(false) + Ok(None) } + // some other error occurred while calculating + // balance deltas (None, _) | (_, None) => Err(Error(eyre!( "Could not calculate the balance delta for {}", payer_account @@ -156,84 +203,266 @@ where } } - /// Deteremine the debit and credit amounts that should be checked. - fn escrow_check<'trans>( + /// Check that the gas was correctly escrowed. + fn check_gas_escrow( &self, wnam_address: &EthAddress, + transfer: &PendingTransfer, + gas_check: EscrowDelta<'_, GasCheck>, + ) -> Result { + if hints::unlikely( + *gas_check.token == wrapped_erc20s::token(wnam_address), + ) { + // NB: this should never be possible: protocol tx state updates + // never result in wNAM ERC20s being minted + tracing::error!( + ?transfer, + "Attempted to pay Bridge pool fees with wrapped NAM." + ); + return Ok(false); + } + if matches!( + &*gas_check.token, + Address::Internal(InternalAddress::Nut(_)) + ) { + tracing::debug!( + ?transfer, + "The gas fees of the transfer cannot be paid in NUTs." + ); + return Ok(false); + } + if !self.check_escrowed_toks(gas_check)? { + tracing::debug!( + ?transfer, + "The gas fees of the transfer were not properly escrowed into \ + the Ethereum bridge pool." + ); + return Ok(false); + } + Ok(true) + } + + /// Validate a wrapped NAM transfer to Ethereum. + fn check_wnam_escrow( + &self, + &wnam_address: &EthAddress, + transfer: &PendingTransfer, + token_check: EscrowDelta<'_, TokenCheck>, + ) -> Result { + if hints::unlikely(matches!( + &transfer.transfer.kind, + TransferToEthereumKind::Nut + )) { + // NB: this should never be possible: protocol tx state updates + // never result in wNAM NUTs being minted. in turn, this means + // that users should never hold wNAM NUTs. doesn't hurt to add + // the extra check to the vp, though + tracing::error!( + ?transfer, + "Attempted to add a wNAM NUT transfer to the Bridge pool" + ); + return Ok(false); + } + + let wnam_whitelisted = { + let key = whitelist::Key { + asset: wnam_address, + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + (&self.ctx).read_pre_value(&key)?.unwrap_or(false) + }; + if !wnam_whitelisted { + tracing::debug!( + ?transfer, + "Wrapped NAM transfers are currently disabled" + ); + return Ok(false); + } + + // if we are going to mint wNam on Ethereum, the appropriate + // amount of Nam must be escrowed in the Ethereum bridge VP's + // storage. + let escrowed_balance = + match self.check_escrowed_toks_balance(token_check)? { + Some(balance) => balance.resolve(), + None => return Ok(false), + }; + + let wnam_cap = { + let key = whitelist::Key { + asset: wnam_address, + suffix: whitelist::KeyType::Cap, + } + .into(); + (&self.ctx).read_pre_value(&key)?.unwrap_or_default() + }; + if escrowed_balance > wnam_cap { + tracing::debug!( + ?transfer, + escrowed_nam = %escrowed_balance.to_string_native(), + wnam_cap = %wnam_cap.to_string_native(), + "The balance of the escrow account exceeds the amount \ + of NAM that is allowed to cross the Ethereum bridge" + ); + return Ok(false); + } + + Ok(true) + } + + /// Deteremine the debit and credit amounts that should be checked. + fn determine_escrow_checks<'trans, 'this: 'trans>( + &'this self, + wnam_address: &EthAddress, transfer: &'trans PendingTransfer, ) -> Result, Error> { - let is_native_asset = &transfer.transfer.asset == wnam_address; - // there is a corner case where the gas fees and escrowed Nam - // are debited from the same address when mint wNam. - Ok( - if transfer.gas_fee.payer == transfer.transfer.sender - && is_native_asset - { - let debit = transfer - .gas_fee - .amount - .checked_add(transfer.transfer.amount) - .ok_or_else(|| { - Error(eyre!( - "Addition oveflowed adding gas fee + transfer \ - amount." - )) - })?; - EscrowCheck { - gas_check: EscrowDelta { - payer_account: &transfer.gas_fee.payer, - escrow_account: &BRIDGE_POOL_ADDRESS, - expected_debit: debit, - expected_credit: transfer.gas_fee.amount, - }, - token_check: EscrowDelta { - payer_account: &transfer.transfer.sender, - escrow_account: &Address::Internal( - InternalAddress::EthBridge, - ), - expected_debit: debit, - expected_credit: transfer.transfer.amount, - }, - } + let tok_is_native_asset = &transfer.transfer.asset == wnam_address; + + // NB: this comparison is not enough to check + // if NAM is being used for both tokens and gas + // fees, since wrapped NAM will have a different + // token address + let same_token_and_gas_erc20 = + transfer.token_address() == transfer.gas_fee.token; + + let (expected_gas_debit, expected_token_debit) = { + // NB: there is a corner case where the gas fees and escrowed + // tokens are debited from the same address, when the gas fee + // payer and token sender are the same, and the underlying + // transferred assets are the same + let same_sender_and_fee_payer = + transfer.gas_fee.payer == transfer.transfer.sender; + let gas_is_native_asset = + transfer.gas_fee.token == self.ctx.storage.native_token; + let gas_and_token_is_native_asset = + gas_is_native_asset && tok_is_native_asset; + let same_token_and_gas_asset = + gas_and_token_is_native_asset || same_token_and_gas_erc20; + let same_debited_address = + same_sender_and_fee_payer && same_token_and_gas_asset; + + if same_debited_address { + let debit = sum_gas_and_token_amounts(transfer)?; + (debit, debit) } else { - EscrowCheck { - gas_check: EscrowDelta { - payer_account: &transfer.gas_fee.payer, - escrow_account: &BRIDGE_POOL_ADDRESS, - expected_debit: transfer.gas_fee.amount, - expected_credit: transfer.gas_fee.amount, - }, - token_check: EscrowDelta { - payer_account: &transfer.transfer.sender, - escrow_account: if is_native_asset { - &BRIDGE_ADDRESS - } else { - &BRIDGE_POOL_ADDRESS - }, - expected_debit: transfer.transfer.amount, - expected_credit: transfer.transfer.amount, - }, - } + (transfer.gas_fee.amount, transfer.transfer.amount) + } + }; + let (expected_gas_credit, expected_token_credit) = { + // NB: there is a corner case where the gas fees and escrowed + // tokens are credited to the same address, when the underlying + // transferred assets are the same (unless the asset is NAM) + let same_credited_address = same_token_and_gas_erc20; + + if same_credited_address { + let credit = sum_gas_and_token_amounts(transfer)?; + (credit, credit) + } else { + (transfer.gas_fee.amount, transfer.transfer.amount) + } + }; + let (token_check_addr, token_check_escrow_acc) = if tok_is_native_asset + { + // when minting wrapped NAM on Ethereum, escrow to the Ethereum + // bridge address, and draw from NAM token accounts + let token = Cow::Borrowed(&self.ctx.storage.native_token); + let escrow_account = &BRIDGE_ADDRESS; + (token, escrow_account) + } else { + // otherwise, draw from ERC20/NUT wrapped asset token accounts, + // and escrow to the Bridge pool address + let token = Cow::Owned(transfer.token_address()); + let escrow_account = &BRIDGE_POOL_ADDRESS; + (token, escrow_account) + }; + + Ok(EscrowCheck { + gas_check: EscrowDelta { + // NB: it's fine to not check for wrapped NAM here, + // as users won't hold wrapped NAM tokens in practice, + // anyway + token: Cow::Borrowed(&transfer.gas_fee.token), + payer_account: &transfer.gas_fee.payer, + escrow_account: &BRIDGE_POOL_ADDRESS, + expected_debit: expected_gas_debit, + expected_credit: expected_gas_credit, + _kind: PhantomData, }, - ) + token_check: EscrowDelta { + token: token_check_addr, + payer_account: &transfer.transfer.sender, + escrow_account: token_check_escrow_acc, + expected_debit: expected_token_debit, + expected_credit: expected_token_credit, + _kind: PhantomData, + }, + }) } } /// Helper struct for handling the different escrow /// checking scenarios. -struct EscrowDelta<'a> { +struct EscrowDelta<'a, KIND> { + token: Cow<'a, Address>, payer_account: &'a Address, escrow_account: &'a Address, expected_debit: Amount, expected_credit: Amount, + _kind: PhantomData<*const KIND>, +} + +impl EscrowDelta<'_, KIND> { + fn validate_changed_keys(&self, changed_keys: &BTreeSet) -> bool { + let EscrowDelta { + token, + payer_account, + escrow_account, + .. + } = self; + let owner_key = balance_key(token, payer_account); + let escrow_key = balance_key(token, escrow_account); + changed_keys.contains(&owner_key) && changed_keys.contains(&escrow_key) + } } /// There are two checks we must do when minting wNam. +/// /// 1. Check that gas fees were escrowed. /// 2. Check that the Nam to back wNam was escrowed. struct EscrowCheck<'a> { - gas_check: EscrowDelta<'a>, - token_check: EscrowDelta<'a>, + gas_check: EscrowDelta<'a, GasCheck>, + token_check: EscrowDelta<'a, TokenCheck>, +} + +impl EscrowCheck<'_> { + #[inline] + fn validate_changed_keys(&self, changed_keys: &BTreeSet) -> bool { + self.gas_check.validate_changed_keys(changed_keys) + && self.token_check.validate_changed_keys(changed_keys) + } +} + +/// Perform a gas check. +enum GasCheck {} + +/// Perform a token check. +enum TokenCheck {} + +/// Sum gas and token amounts on a pending transfer, checking for overflows. +#[inline] +fn sum_gas_and_token_amounts( + transfer: &PendingTransfer, +) -> Result { + transfer + .gas_fee + .amount + .checked_add(transfer.transfer.amount) + .ok_or_else(|| { + Error(eyre!( + "Addition oveflowed adding gas fee + transfer amount." + )) + }) } impl<'a, D, H, CA> NativeVp for BridgePoolVp<'a, D, H, CA> @@ -310,30 +539,48 @@ where } // The deltas in the escrowed amounts we must check. let wnam_address = read_native_erc20_address(&self.ctx.pre())?; - let escrow_checks = self.escrow_check(&wnam_address, &transfer)?; + let escrow_checks = + self.determine_escrow_checks(&wnam_address, &transfer)?; + if !escrow_checks.validate_changed_keys(keys_changed) { + tracing::debug!( + ?transfer, + "Missing storage modifications in the Bridge pool" + ); + return Ok(false); + } // check that gas was correctly escrowed. - if !self.check_nam_escrowed(escrow_checks.gas_check)? { + if !self.check_gas_escrow( + &wnam_address, + &transfer, + escrow_checks.gas_check, + )? { return Ok(false); } // check the escrowed assets if transfer.transfer.asset == wnam_address { - // if we are going to mint wNam on Ethereum, the appropriate - // amount of Nam must be escrowed in the Ethereum bridge VP's - // storage. - self.check_nam_escrowed(escrow_checks.token_check) - .map(|ok| { - if ok { - tracing::info!( - "The Ethereum bridge pool VP accepted the \ - transfer {:?}.", - transfer - ); - } - ok - }) + self.check_wnam_escrow( + &wnam_address, + &transfer, + escrow_checks.token_check, + ) } else { - self.check_erc20s_escrowed(keys_changed, &transfer) + self.check_escrowed_toks(escrow_checks.token_check) } + .map(|ok| { + if ok { + tracing::info!( + "The Ethereum bridge pool VP accepted the transfer {:?}.", + transfer + ); + } else { + tracing::debug!( + ?transfer, + "The assets of the transfer were not properly escrowed \ + into the Ethereum bridge pool." + ); + } + ok + }) } } @@ -355,7 +602,7 @@ mod test_bridge_pool_vp { use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{Storage, WlStorage}; use crate::ledger::storage_api::StorageWrite; - use crate::types::address::{nam, wnam}; + use crate::types::address::{nam, wnam, InternalAddress}; use crate::types::chain::ChainId; use crate::types::eth_bridge_pool::{GasFee, TransferToEthereum}; use crate::types::hash::Hash; @@ -368,23 +615,36 @@ 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 { + /// The address of the Ethereum asset. + asset: EthAddress, + /// 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, + asset: ASSET, owner: address, - balance: 0.into(), + gas: 0.into(), token: 0.into(), } } @@ -396,6 +656,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") @@ -406,12 +682,14 @@ mod test_bridge_pool_vp { fn initial_pool() -> PendingTransfer { PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, sender: bertha_address(), recipient: EthAddress([0; 20]), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -430,20 +708,57 @@ 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 + // whitelist wnam + let key = whitelist::Key { + asset: wnam(), + suffix: whitelist::KeyType::Whitelisted, + } + .into(); + writelog + .write(&key, true.try_to_vec().unwrap()) + .expect("Test failed"); + let key = whitelist::Key { + asset: wnam(), + suffix: whitelist::KeyType::Cap, + } + .into(); + writelog + .write(&key, Amount::max().try_to_vec().unwrap()) + .expect("Test failed"); + // set up users with ERC20 and NUT balances update_balances( &mut writelog, - 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()), + ); + // set up the initial balances of the ethereum bridge account + update_balances( + &mut writelog, + Balance::new(TransferToEthereumKind::Erc20, BRIDGE_ADDRESS), + SignedAmount::Positive(ESCROWED_AMOUNT.into()), + // we only care about escrowing NAM + SignedAmount::Positive(0.into()), + ); writelog.commit_tx(); writelog } @@ -456,43 +771,86 @@ mod test_bridge_pool_vp { gas_delta: SignedAmount, token_delta: SignedAmount, ) -> BTreeSet { - // get the balance keys - let token_key = - balance_key(&wrapped_erc20s::token(&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, - } - .try_to_vec() - .expect("Test failed"); + // wnam is drawn from the same account + if balance.asset == wnam() + && !matches!(&balance.owner, Address::Internal(_)) + { + use SignedAmount::*; + + // update the balance of nam + let original_balance = std::cmp::max(balance.token, balance.gas); + let updated_balance = match (gas_delta, token_delta) { + (Negative(x), Negative(y)) => original_balance - x - y, + (Negative(x), Positive(y)) => original_balance - x + y, + (Positive(x), Negative(y)) => original_balance + x - y, + (Positive(x), Positive(y)) => original_balance + x + y, + }; + + // write the changes to the log + let account_key = balance_key(&nam(), &balance.owner); + write_log + .write( + &account_key, + updated_balance.try_to_vec().expect("Test failed"), + ) + .expect("Test failed"); - // update the balance of tokens - let new_token_balance = match token_delta { - SignedAmount::Positive(amount) => balance.token + amount, - SignedAmount::Negative(amount) => balance.token - amount, + // changed keys + [account_key].into() + } else { + // get the balance keys + let token_key = if balance.asset == wnam() { + // the match above guards against non-internal addresses, + // so the only logical owner here is the Ethereum bridge + // address, where we escrow NAM to, when minting wNAM on + // Ethereum + assert_eq!(balance.owner, BRIDGE_POOL_ADDRESS); + balance_key(&nam(), &BRIDGE_ADDRESS) + } else { + balance_key( + &match balance.kind { + TransferToEthereumKind::Erc20 => { + wrapped_erc20s::token(&balance.asset) + } + TransferToEthereumKind::Nut => { + wrapped_erc20s::nut(&balance.asset) + } + }, + &balance.owner, + ) + }; + let account_key = balance_key(&nam(), &balance.owner); + + // update the balance of nam + let new_gas_balance = match gas_delta { + SignedAmount::Positive(amount) => balance.gas + amount, + SignedAmount::Negative(amount) => balance.gas - amount, + }; + + // update the balance of tokens + let new_token_balance = match token_delta { + SignedAmount::Positive(amount) => balance.token + amount, + SignedAmount::Negative(amount) => balance.token - amount, + }; + + // write the changes to the log + write_log + .write(&account_key, new_gas_balance.try_to_vec().unwrap()) + .expect("Test failed"); + write_log + .write(&token_key, new_token_balance.try_to_vec().unwrap()) + .expect("Test failed"); + + // return the keys changed + [account_key, token_key].into() } - .try_to_vec() - .expect("Test failed"); - - // write the changes to the log - write_log - .write(&account_key, new_balance) - .expect("Test failed"); - write_log - .write(&token_key, new_token_balance) - .expect("Test failed"); - - // return the keys changed - [account_key, token_key].into() } /// Initialize some dummy storage for testing 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 { @@ -561,35 +919,39 @@ mod test_bridge_pool_vp { insert_transfer: F, expect: Expect, ) where - F: FnOnce(PendingTransfer, &mut WriteLog) -> BTreeSet, + F: FnOnce(&mut PendingTransfer, &mut WriteLog) -> BTreeSet, { // setup let mut wl_storage = setup_storage(); let tx = Tx::from_type(TxType::Raw); // the transfer to be added to the pool - let transfer = PendingTransfer { + let mut transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: ASSET, sender: bertha_address(), recipient: EthAddress([1; 20]), amount: TOKENS.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: bertha_address(), }, }; // add transfer to pool let mut keys_changed = - insert_transfer(transfer.clone(), &mut wl_storage.write_log); + insert_transfer(&mut transfer, &mut wl_storage.write_log); // change Bertha's balances let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: transfer.transfer.asset, + kind: TransferToEthereumKind::Erc20, owner: bertha_address(), - balance: BERTHA_WEALTH.into(), + gas: BERTHA_WEALTH.into(), token: BERTHA_TOKENS.into(), }, payer_gas_delta, @@ -601,8 +963,10 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: transfer.transfer.asset, + kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, - balance: ESCROWED_AMOUNT.into(), + gas: ESCROWED_AMOUNT.into(), token: ESCROWED_TOKENS.into(), }, gas_escrow_delta, @@ -642,11 +1006,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::True, ); @@ -663,11 +1027,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -684,11 +1048,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -705,11 +1069,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -727,11 +1091,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(10.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -748,11 +1112,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(10.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -769,11 +1133,11 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -790,11 +1154,11 @@ mod test_bridge_pool_vp { SignedAmount::Negative(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -809,7 +1173,7 @@ mod test_bridge_pool_vp { SignedAmount::Positive(GAS_FEE.into()), SignedAmount::Negative(TOKENS.into()), SignedAmount::Positive(TOKENS.into()), - |transfer, _| BTreeSet::from([get_pending_key(&transfer)]), + |transfer, _| BTreeSet::from([get_pending_key(transfer)]), Expect::Error, ); } @@ -826,19 +1190,21 @@ 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]), amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: bertha_address(), }, }; - log.write(&get_pending_key(&transfer), t.try_to_vec().unwrap()) + log.write(&get_pending_key(transfer), t.try_to_vec().unwrap()) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::False, ); @@ -856,19 +1222,21 @@ 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]), amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: GAS_FEE.into(), payer: bertha_address(), }, }; log.write(&get_pending_key(&t), transfer.try_to_vec().unwrap()) .unwrap(); - BTreeSet::from([get_pending_key(&transfer)]) + BTreeSet::from([get_pending_key(transfer)]) }, Expect::Error, ); @@ -885,12 +1253,12 @@ mod test_bridge_pool_vp { SignedAmount::Positive(TOKENS.into()), |transfer, log| { log.write( - &get_pending_key(&transfer), + &get_pending_key(transfer), transfer.try_to_vec().unwrap(), ) .unwrap(); BTreeSet::from([ - get_pending_key(&transfer), + get_pending_key(transfer), get_signed_root_key(), ]) }, @@ -925,8 +1293,10 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: ASSET, + kind: TransferToEthereumKind::Erc20, owner: bertha_address(), - balance: BERTHA_WEALTH.into(), + gas: BERTHA_WEALTH.into(), token: BERTHA_TOKENS.into(), }, SignedAmount::Negative(GAS_FEE.into()), @@ -938,8 +1308,10 @@ mod test_bridge_pool_vp { let mut new_keys_changed = update_balances( &mut wl_storage.write_log, Balance { + asset: ASSET, + kind: TransferToEthereumKind::Erc20, owner: BRIDGE_POOL_ADDRESS, - balance: ESCROWED_AMOUNT.into(), + gas: ESCROWED_AMOUNT.into(), token: ESCROWED_TOKENS.into(), }, SignedAmount::Positive(GAS_FEE.into()), @@ -977,12 +1349,14 @@ 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]), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1033,7 +1407,7 @@ mod test_bridge_pool_vp { /// Test that we can escrow Nam if we /// want to mint wNam on Ethereum. #[test] - fn test_mint_wnam() { + fn test_minting_wnam() { // setup let mut wl_storage = setup_storage(); let eb_account_key = @@ -1043,19 +1417,21 @@ 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]), amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: 100.into(), payer: bertha_address(), }, }; // add transfer to pool - let keys_changed = { + let mut keys_changed = { wl_storage .write_log .write( @@ -1077,6 +1453,7 @@ mod test_bridge_pool_vp { .expect("Test failed"), ) .expect("Test failed"); + assert!(keys_changed.insert(account_key)); let bp_account_key = balance_key(&nam(), &BRIDGE_POOL_ADDRESS); wl_storage .write_log @@ -1087,13 +1464,17 @@ mod test_bridge_pool_vp { .expect("Test failed"), ) .expect("Test failed"); + assert!(keys_changed.insert(bp_account_key)); wl_storage .write_log .write( &eb_account_key, - Amount::from(100).try_to_vec().expect("Test failed"), + Amount::from(ESCROWED_AMOUNT + 100) + .try_to_vec() + .expect("Test failed"), ) .expect("Test failed"); + assert!(keys_changed.insert(eb_account_key)); let verifiers = BTreeSet::default(); // create the data to be given to the vp @@ -1130,12 +1511,14 @@ 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]), amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: 100.into(), payer: bertha_address(), }, @@ -1236,12 +1619,14 @@ 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]), amount: 100.into(), }, gas_fee: GasFee { + token: nam(), amount: 100.into(), payer: established_address_1(), }, @@ -1316,4 +1701,149 @@ 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 { + token: nam(), + 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, + asset: ASSET, + 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, + asset: ASSET, + 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) + } + + /// Test that the Bridge pool VP rejects a wNAM NUT transfer. + #[test] + fn test_bridge_pool_vp_rejects_wnam_nut() { + assert_bridge_pool( + SignedAmount::Negative(GAS_FEE.into()), + SignedAmount::Positive(GAS_FEE.into()), + SignedAmount::Negative(TOKENS.into()), + SignedAmount::Positive(TOKENS.into()), + |transfer, log| { + transfer.transfer.kind = TransferToEthereumKind::Nut; + transfer.transfer.asset = wnam(); + log.write( + &get_pending_key(transfer), + transfer.try_to_vec().unwrap(), + ) + .unwrap(); + BTreeSet::from([get_pending_key(transfer)]) + }, + Expect::False, + ); + } + + /// Test that the Bridge pool VP accepts a wNAM ERC20 transfer. + #[test] + fn test_bridge_pool_vp_accepts_wnam_erc20() { + assert_bridge_pool( + SignedAmount::Negative(GAS_FEE.into()), + SignedAmount::Positive(GAS_FEE.into()), + SignedAmount::Negative(TOKENS.into()), + SignedAmount::Positive(TOKENS.into()), + |transfer, log| { + transfer.transfer.kind = TransferToEthereumKind::Erc20; + transfer.transfer.asset = wnam(); + log.write( + &get_pending_key(transfer), + transfer.try_to_vec().unwrap(), + ) + .unwrap(); + BTreeSet::from([get_pending_key(transfer)]) + }, + Expect::True, + ); + } } 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 5fd7aa6cd1..87be6d1510 100644 --- a/shared/src/ledger/native_vp/ethereum_bridge/vp.rs +++ b/shared/src/ledger/native_vp/ethereum_bridge/vp.rs @@ -1,22 +1,23 @@ //! Validity predicate for the Ethereum bridge use std::collections::{BTreeSet, HashSet}; -use borsh::BorshDeserialize; use eyre::{eyre, Result}; -use itertools::Itertools; -use namada_core::ledger::eth_bridge::storage::{ - self, escrow_key, wrapped_erc20s, -}; +use namada_core::ledger::eth_bridge::storage::{self, escrow_key}; use namada_core::ledger::storage::traits::StorageHasher; use namada_core::ledger::{eth_bridge, storage as ledger_storage}; use namada_core::types::address::Address; use namada_core::types::storage::Key; -use namada_core::types::token::{balance_key, Amount, Change}; +use namada_core::types::token::{balance_key, is_balance_key, Amount}; -use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader, VpEnv}; +use crate::ledger::native_vp::{Ctx, NativeVp, StorageReader}; use crate::proto::Tx; use crate::vm::WasmCacheAccess; +/// Generic error that may be returned by the validity predicate +#[derive(thiserror::Error, Debug)] +#[error(transparent)] +pub struct Error(#[from] eyre::Error); + /// Validity predicate for the Ethereum bridge pub struct EthBridge<'ctx, DB, H, CA> where @@ -34,39 +35,29 @@ where H: 'static + StorageHasher, CA: 'static + WasmCacheAccess, { - /// If the bridge's escrow key was changed, we check - /// that the balance increased and that the bridge pool - /// VP has been triggered. The bridge pool VP will carry - /// out the rest of the checks. + /// If the Ethereum bridge's escrow key was written to, we check + /// that the NAM balance increased and that the Bridge pool VP has + /// been triggered. fn check_escrow( &self, verifiers: &BTreeSet
, ) -> Result { let escrow_key = balance_key(&self.ctx.storage.native_token, ð_bridge::ADDRESS); - let escrow_pre: Amount = if let Ok(Some(bytes)) = - self.ctx.read_bytes_pre(&escrow_key) - { - BorshDeserialize::try_from_slice(bytes.as_slice()).map_err( - |_| Error(eyre!("Couldn't deserialize a balance from storage")), - )? - } else { - tracing::debug!( - "Could not retrieve the Ethereum bridge VP's balance from \ - storage" - ); - return Ok(false); - }; + + let escrow_pre: Amount = + if let Ok(Some(value)) = (&self.ctx).read_pre_value(&escrow_key) { + value + } else { + tracing::debug!( + "Could not retrieve the Ethereum bridge VP's balance from \ + storage" + ); + return Ok(false); + }; let escrow_post: Amount = - if let Ok(Some(bytes)) = self.ctx.read_bytes_post(&escrow_key) { - BorshDeserialize::try_from_slice(bytes.as_slice()).map_err( - |_| { - Error(eyre!( - "Couldn't deserialize the balance of the Ethereum \ - bridge VP from storage." - )) - }, - )? + if let Ok(Some(value)) = (&self.ctx).read_post_value(&escrow_key) { + value } else { tracing::debug!( "Could not retrieve the modified Ethereum bridge VP's \ @@ -77,6 +68,8 @@ where // The amount escrowed should increase. if escrow_pre < escrow_post { + // NB: normally, we only escrow NAM under the Ethereum bridge + // addresss in the context of a Bridge pool transfer Ok(verifiers.contains(&storage::bridge_pool::BRIDGE_POOL_ADDRESS)) } else { tracing::info!( @@ -88,19 +81,6 @@ where } } -/// One of the the two types of checks -/// this VP must perform. -#[derive(Debug)] -enum CheckType { - Escrow, - Erc20Transfer, -} - -#[derive(thiserror::Error, Debug)] -#[error(transparent)] -/// Generic error that may be returned by the validity predicate -pub struct Error(#[from] eyre::Error); - impl<'a, DB, H, CA> NativeVp for EthBridge<'a, DB, H, CA> where DB: 'static + ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, @@ -112,12 +92,8 @@ where /// Validate that a wasm transaction is permitted to change keys under this /// account. /// - /// We permit only the following changes via wasm for the time being: - /// - a wrapped ERC20's supply key to decrease iff one of its balance keys - /// decreased by the same amount - /// - a wrapped ERC20's balance key to decrease iff another one of its - /// balance keys increased by the same amount - /// - Escrowing Nam in order to mint wrapped Nam on Ethereum + /// We only permit increasing the escrowed balance of NAM under the Ethereum + /// bridge address, when writing to storage from wasm transactions. /// /// Some other changes to the storage subspace of this account are expected /// to happen natively i.e. bypassing this validity predicate. For example, @@ -135,33 +111,37 @@ where "Ethereum Bridge VP triggered", ); - match determine_check_type( - &self.ctx.storage.native_token, - keys_changed, - )? { - // Multitoken VP checks the balance changes for the ERC20 transfer - Some(CheckType::Erc20Transfer) => Ok(true), - Some(CheckType::Escrow) => self.check_escrow(verifiers), - None => Ok(false), + if !validate_changed_keys(&self.ctx.storage.native_token, keys_changed)? + { + return Ok(false); } + + self.check_escrow(verifiers) } } /// Checks if `keys_changed` represents a valid set of changed keys. -/// Depending on which keys get changed, chooses which type of -/// check to perform in the `validate_tx` function. -/// 1. If the Ethereum bridge escrow key was changed, we need to check -/// that escrow was performed correctly. -/// 2. If two erc20 keys where changed, this is a transfer that needs -/// to be checked. -fn determine_check_type( +/// +/// This implies cheking if two distinct keys were changed: +/// +/// 1. The Ethereum bridge escrow account's NAM balance key. +/// 2. Another account's NAM balance key. +/// +/// Any other keys changed under the Ethereum bridge account +/// are rejected. +fn validate_changed_keys( nam_addr: &Address, keys_changed: &BTreeSet, -) -> Result, Error> { - // we aren't concerned with keys that changed outside of our account +) -> Result { + // acquire all keys that either changed our account, or that touched + // nam balances let keys_changed: HashSet<_> = keys_changed .iter() - .filter(|key| storage::is_eth_bridge_key(nam_addr, key)) + .filter(|&key| { + let changes_eth_storage = storage::has_eth_addr_segment(key); + let changes_nam_balance = is_balance_key(nam_addr, key).is_some(); + changes_nam_balance || changes_eth_storage + }) .collect(); if keys_changed.is_empty() { return Err(Error(eyre!( @@ -173,157 +153,10 @@ fn determine_check_type( relevant_keys.len = keys_changed.len(), "Found keys changed under our account" ); - if keys_changed.len() == 1 && keys_changed.contains(&escrow_key(nam_addr)) { - return Ok(Some(CheckType::Escrow)); - } else if keys_changed.len() != 2 { - tracing::debug!( - relevant_keys.len = keys_changed.len(), - "Rejecting transaction as only two keys should have changed" - ); - return Ok(None); - } - - let mut keys = HashSet::<_>::default(); - for key in keys_changed.into_iter() { - let key = match wrapped_erc20s::Key::try_from((nam_addr, key)) { - Ok(key) => { - // Disallow changes to any supply keys via wasm transactions, - // since these should only ever be changed via FinalizeBlock - // after a successful transfer to or from Ethereum - if matches!(key.suffix, wrapped_erc20s::KeyType::Supply) { - tracing::debug!( - ?key, - "Rejecting transaction as key is a supply key" - ); - return Ok(None); - } - key - } - Err(error) => { - tracing::debug!( - %key, - ?error, - "Rejecting transaction as key is not a wrapped ERC20 key" - ); - return Ok(None); - } - }; - keys.insert(key); - } - - // We can .unwrap() here as we know for sure that this set has len=2 - let (key_a, key_b) = keys.into_iter().collect_tuple().unwrap(); - if key_a.asset != key_b.asset { - tracing::debug!( - ?key_a, - ?key_b, - "Rejecting transaction as keys are for different assets" - ); - return Ok(None); - } - Ok(Some(CheckType::Erc20Transfer)) -} - -/// Checks that the balances at both `sender` and `receiver` have changed by -/// some amount, and that the changes balance each other out. If the balance -/// changes are invalid, the reason is logged and a `None` is returned. -/// Otherwise, return the `Amount` of the transfer i.e. by how much the sender's -/// balance decreased, or equivalently by how much the receiver's balance -/// increased -pub(super) fn check_balance_changes( - reader: impl StorageReader, - sender: &Key, - receiver: &Key, -) -> Result> { - let sender_balance_pre = reader - .read_pre_value::(sender)? - .unwrap_or_default() - .change(); - let sender_balance_post = match reader.read_post_value::(sender)? { - Some(value) => value, - None => { - return Err(eyre!( - "Rejecting transaction as could not read_post balance key {}", - sender, - )); - } - } - .change(); - let receiver_balance_pre = reader - .read_pre_value::(receiver)? - .unwrap_or_default() - .change(); - let receiver_balance_post = match reader - .read_post_value::(receiver)? - { - Some(value) => value, - None => { - return Err(eyre!( - "Rejecting transaction as could not read_post balance key {}", - receiver, - )); - } - } - .change(); - - let sender_balance_delta = - calculate_delta(sender_balance_pre, sender_balance_post)?; - let receiver_balance_delta = - calculate_delta(receiver_balance_pre, receiver_balance_post)?; - if receiver_balance_delta != -sender_balance_delta { - tracing::debug!( - ?sender_balance_pre, - ?receiver_balance_pre, - ?sender_balance_post, - ?receiver_balance_post, - ?sender_balance_delta, - ?receiver_balance_delta, - "Rejecting transaction as balance changes do not match" - ); - return Ok(None); - } - if sender_balance_delta.is_zero() || sender_balance_delta > Change::zero() { - assert!( - receiver_balance_delta.is_zero() - || receiver_balance_delta < Change::zero() - ); - tracing::debug!( - "Rejecting transaction as no balance change or invalid change" - ); - return Ok(None); - } - if sender_balance_post < Change::zero() { - tracing::debug!( - ?sender_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - if receiver_balance_post < Change::zero() { - tracing::debug!( - ?receiver_balance_post, - "Rejecting transaction as balance is negative" - ); - return Ok(None); - } - - Ok(Some(Amount::from_change(receiver_balance_delta))) -} - -/// Return the delta between `balance_pre` and `balance_post`, erroring if there -/// is an underflow -fn calculate_delta( - balance_pre: Change, - balance_post: Change, -) -> Result { - match balance_post.checked_sub(&balance_pre) { - Some(result) => Ok(result), - None => Err(eyre!( - "Underflow while calculating delta: {} - {}", - balance_post, - balance_pre - )), - } + Ok(keys_changed.contains(&escrow_key(nam_addr)) + && keys_changed + .iter() + .all(|key| is_balance_key(nam_addr, key).is_some())) } #[cfg(test)] @@ -334,6 +167,7 @@ mod tests { use borsh::BorshSerialize; use namada_core::ledger::eth_bridge; use namada_core::ledger::eth_bridge::storage::bridge_pool::BRIDGE_POOL_ADDRESS; + use namada_core::ledger::eth_bridge::storage::wrapped_erc20s; use namada_core::ledger::storage_api::StorageWrite; use namada_ethereum_bridge::parameters::{ Contracts, EthereumBridgeConfig, UpgradeableContract, @@ -347,6 +181,7 @@ mod tests { use crate::ledger::storage::write_log::WriteLog; use crate::ledger::storage::{Storage, WlStorage}; use crate::proto::Tx; + use crate::types::address::testing::established_address_1; use crate::types::address::{nam, wnam}; use crate::types::ethereum_events; use crate::types::ethereum_events::EthAddress; @@ -358,8 +193,6 @@ mod tests { const ARBITRARY_OWNER_A_ADDRESS: &str = "atest1d9khqw36x9zyxwfhgfpygv2pgc65gse4gy6rjs34gfzr2v69gy6y23zpggurjv2yx5m52sesu6r4y4"; - const ARBITRARY_OWNER_B_ADDRESS: &str = - "atest1v4ehgw36xuunwd6989prwdfkxqmnvsfjxs6nvv6xxucrs3f3xcmns3fcxdzrvvz9xverzvzr56le8f"; const ARBITRARY_OWNER_A_INITIAL_BALANCE: u64 = 100; const ESCROW_AMOUNT: u64 = 100; const BRIDGE_POOL_ESCROW_INITIAL_BALANCE: u64 = 0; @@ -393,6 +226,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 { @@ -433,11 +267,23 @@ mod tests { ) } + #[test] + fn test_accepts_expected_keys_changed() { + let keys_changed = BTreeSet::from([ + balance_key(&nam(), &established_address_1()), + balance_key(&nam(), ð_bridge::ADDRESS), + ]); + + let result = validate_changed_keys(&nam(), &keys_changed); + + assert_matches!(result, Ok(true)); + } + #[test] fn test_error_if_triggered_without_keys_changed() { let keys_changed = BTreeSet::new(); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); assert!(result.is_err()); } @@ -447,9 +293,9 @@ mod tests { { let keys_changed = BTreeSet::from_iter(vec![arbitrary_key(); 3]); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } { let keys_changed = BTreeSet::from_iter(vec![ @@ -458,9 +304,9 @@ mod tests { arbitrary_key(), ]); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } } @@ -470,9 +316,9 @@ mod tests { let keys_changed = BTreeSet::from_iter(vec![arbitrary_key(), arbitrary_key()]); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } { @@ -483,9 +329,9 @@ mod tests { )), ]); - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } { @@ -500,54 +346,9 @@ mod tests { ), ]); - let result = determine_check_type(&nam(), &keys_changed); - - assert_matches!(result, Ok(None)); - } - } - - #[test] - fn test_rejects_if_multitoken_keys_for_different_assets() { - { - let keys_changed = BTreeSet::from_iter(vec![ - balance_key( - &wrapped_erc20s::token( - ðereum_events::testing::DAI_ERC20_ETH_ADDRESS, - ), - &Address::decode(ARBITRARY_OWNER_A_ADDRESS) - .expect("Couldn't set up test"), - ), - balance_key( - &wrapped_erc20s::token( - ðereum_events::testing::USDC_ERC20_ETH_ADDRESS, - ), - &Address::decode(ARBITRARY_OWNER_B_ADDRESS) - .expect("Couldn't set up test"), - ), - ]); - - let result = determine_check_type(&nam(), &keys_changed); - - assert_matches!(result, Ok(None)); - } - } - - #[test] - fn test_rejects_if_supply_key_changed() { - let asset = ðereum_events::testing::DAI_ERC20_ETH_ADDRESS; - { - let keys_changed = BTreeSet::from_iter(vec![ - minted_balance_key(&wrapped_erc20s::token(asset)), - balance_key( - &wrapped_erc20s::token(asset), - &Address::decode(ARBITRARY_OWNER_B_ADDRESS) - .expect("Couldn't set up test"), - ), - ]); - - let result = determine_check_type(&nam(), &keys_changed); + let result = validate_changed_keys(&nam(), &keys_changed); - assert_matches!(result, Ok(None)); + assert_matches!(result, Ok(false)); } } diff --git a/shared/src/ledger/protocol/mod.rs b/shared/src/ledger/protocol/mod.rs index 7a0244932a..f3002ca723 100644 --- a/shared/src/ledger/protocol/mod.rs +++ b/shared/src/ledger/protocol/mod.rs @@ -11,6 +11,7 @@ use crate::ledger::gas::{self, BlockGasMeter, VpGasMeter}; use crate::ledger::governance::GovernanceVp; use crate::ledger::ibc::vp::Ibc; use crate::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; +use crate::ledger::native_vp::ethereum_bridge::nut::NonUsableTokens; use crate::ledger::native_vp::ethereum_bridge::vp::EthBridge; use crate::ledger::native_vp::multitoken::MultitokenVp; use crate::ledger::native_vp::parameters::{self, ParametersVp}; @@ -72,6 +73,8 @@ pub enum Error { ReplayProtectionNativeVpError( crate::ledger::native_vp::replay_protection::Error, ), + #[error("Non usable tokens native VP error: {0}")] + NutNativeVpError(native_vp::ethereum_bridge::nut::Error), #[error("Access to an internal address {0} is forbidden")] AccessForbidden(InternalAddress), } @@ -585,6 +588,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/mod.rs b/shared/src/ledger/queries/mod.rs index ce689e6325..bcd9fc8c27 100644 --- a/shared/src/ledger/queries/mod.rs +++ b/shared/src/ledger/queries/mod.rs @@ -12,6 +12,9 @@ pub use types::{ }; use vp::{Vp, VP}; +pub use self::shell::eth_bridge::{ + Erc20FlowControl, GenBridgePoolProofReq, GenBridgePoolProofRsp, +}; use super::storage::traits::StorageHasher; use super::storage::{DBIter, DB}; use super::storage_api; diff --git a/shared/src/ledger/queries/shell.rs b/shared/src/ledger/queries/shell.rs index 94412f1a15..b1e243f74b 100644 --- a/shared/src/ledger/queries/shell.rs +++ b/shared/src/ledger/queries/shell.rs @@ -1,4 +1,4 @@ -mod eth_bridge; +pub(super) mod eth_bridge; use borsh::{BorshDeserialize, BorshSerialize}; use masp_primitives::asset_type::AssetType; diff --git a/shared/src/ledger/queries/shell/eth_bridge.rs b/shared/src/ledger/queries/shell/eth_bridge.rs index d7d0ed249e..2588ef33bd 100644 --- a/shared/src/ledger/queries/shell/eth_bridge.rs +++ b/shared/src/ledger/queries/shell/eth_bridge.rs @@ -1,23 +1,24 @@ //! Ethereum bridge related shell queries. +use std::borrow::Cow; use std::collections::HashMap; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use namada_core::ledger::eth_bridge::storage::bridge_pool::get_key_from_hash; -use namada_core::ledger::eth_bridge::storage::wrapped_erc20s; use namada_core::ledger::storage::merkle_tree::StoreRef; use namada_core::ledger::storage::{DBIter, StorageHasher, StoreType, DB}; use namada_core::ledger::storage_api::{ self, CustomError, ResultExt, StorageRead, }; use namada_core::types::address::Address; +use namada_core::types::eth_bridge_pool::PendingTransferAppendix; use namada_core::types::ethereum_events::{ EthAddress, EthereumEvent, TransferToEthereum, }; use namada_core::types::ethereum_structs::RelayProof; use namada_core::types::storage::{BlockHeight, DbKeySeg, Key}; -use namada_core::types::token::{minted_balance_key, Amount}; +use namada_core::types::token::Amount; use namada_core::types::vote_extensions::validator_set_update::{ ValidatorSetArgs, VotingPowersMap, }; @@ -35,6 +36,7 @@ use namada_ethereum_bridge::storage::{ }; use namada_proof_of_stake::pos_queries::PosQueries; +use crate::eth_bridge::ethers::abi::AbiDecode; use crate::ledger::queries::{EncodedResponseQuery, RequestCtx, RequestQuery}; use crate::types::eth_abi::{Encode, EncodeCell}; use crate::types::eth_bridge_pool::PendingTransfer; @@ -42,7 +44,53 @@ use crate::types::keccak::KeccakHash; use crate::types::storage::Epoch; use crate::types::storage::MembershipProof::BridgePool; -pub type RelayProofBytes = Vec; +/// Contains information about the flow control of some ERC20 +/// wrapped asset. +#[derive( + Debug, Copy, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize, +)] +pub struct Erc20FlowControl { + /// Whether the wrapped asset is whitelisted. + whitelisted: bool, + /// Total minted supply of some wrapped asset. + supply: Amount, + /// The token cap of some wrapped asset. + cap: Amount, +} + +/// Request data to pass to `generate_bridge_pool_proof`. +#[derive(Debug, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct GenBridgePoolProofReq<'transfers, 'relayer> { + /// The hashes of the transfers to be relayed. + pub transfers: Cow<'transfers, [KeccakHash]>, + /// The address of the relayer to compensate. + pub relayer: Cow<'relayer, Address>, + /// Whether to return the appendix of a [`PendingTransfer`]. + pub with_appendix: bool, +} + +/// Response data returned by `generate_bridge_pool_proof`. +#[derive(Debug, Clone, Eq, PartialEq, BorshSerialize, BorshDeserialize)] +pub struct GenBridgePoolProofRsp { + /// Ethereum ABI encoded [`RelayProof`]. + pub abi_encoded_proof: Vec, + /// Appendix data of all requested pending transfers. + pub appendices: Option>>, +} + +impl GenBridgePoolProofRsp { + /// Retrieve all [`PendingTransfer`] instances returned from the RPC server. + pub fn pending_transfers(self) -> impl Iterator { + RelayProof::decode(&self.abi_encoded_proof) + .into_iter() + .flat_map(|proof| proof.transfers) + .zip(self.appendices.into_iter().flatten()) + .map(|(event, appendix)| { + let event: TransferToEthereum = event.into(); + PendingTransfer::from_parts(&event, appendix) + }) + } +} router! {ETH_BRIDGE, // Get the current contents of the Ethereum bridge pool @@ -57,12 +105,12 @@ router! {ETH_BRIDGE, // Generate a merkle proof for the inclusion of requested // transfers in the Ethereum bridge pool ( "pool" / "proof" ) - -> RelayProofBytes = (with_options generate_bridge_pool_proof), + -> GenBridgePoolProofRsp = (with_options generate_bridge_pool_proof), // Iterates over all ethereum events and returns the amount of // voting power backing each `TransferToEthereum` event. ( "pool" / "transfer_to_eth_progress" ) - -> HashMap + -> HashMap = transfer_to_ethereum_progress, // Request a proof of a validator set signed off for @@ -104,31 +152,35 @@ router! {ETH_BRIDGE, ( "voting_powers" / "epoch" / [epoch: Epoch] ) -> VotingPowersMap = voting_powers_at_epoch, - // Read the total supply of some wrapped ERC20 token in Namada. - ( "erc20" / "supply" / [asset: EthAddress] ) - -> Option = read_erc20_supply, + // Read the total supply and respective cap of some wrapped + // ERC20 token in Namada. + ( "erc20" / "flow_control" / [asset: EthAddress] ) + -> Erc20FlowControl = get_erc20_flow_control, } -/// Read the total supply of some wrapped ERC20 token in Namada. -fn read_erc20_supply( +/// Read the total supply and respective cap of some wrapped +/// ERC20 token in Namada. +fn get_erc20_flow_control( ctx: RequestCtx<'_, D, H>, asset: EthAddress, -) -> storage_api::Result> +) -> storage_api::Result where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - let Some(native_erc20) = ctx.wl_storage.read(&native_erc20_key())? else { - return Err(storage_api::Error::SimpleMessage( - "The Ethereum bridge storage is not initialized", - )); - }; - let token = if asset == native_erc20 { - ctx.wl_storage.storage.native_token.clone() - } else { - wrapped_erc20s::token(&asset) - }; - ctx.wl_storage.read(&minted_balance_key(&token)) + let ethbridge_queries = ctx.wl_storage.ethbridge_queries(); + + let whitelisted = ethbridge_queries.is_token_whitelisted(&asset); + let supply = ethbridge_queries + .get_token_supply(&asset) + .unwrap_or_default(); + let cap = ethbridge_queries.get_token_cap(&asset).unwrap_or_default(); + + Ok(Erc20FlowControl { + whitelisted, + supply, + cap, + }) } /// Helper function to read a smart contract from storage. @@ -273,8 +325,11 @@ where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, { - if let Ok((transfer_hashes, relayer)) = - <(Vec, Address)>::try_from_slice(request.data.as_slice()) + if let Ok(GenBridgePoolProofReq { + transfers: transfer_hashes, + relayer, + with_appendix, + }) = BorshDeserialize::try_from_slice(request.data.as_slice()) { // get the latest signed merkle root of the Ethereum bridge pool let (signed_root, height) = ctx @@ -287,6 +342,21 @@ where )) .into_storage_result()?; + // make sure a relay attempt won't happen before the new signed + // root has had time to be generated + let latest_bp_nonce = + ctx.wl_storage.ethbridge_queries().get_bridge_pool_nonce(); + if latest_bp_nonce != signed_root.data.1 { + return Err(storage_api::Error::Custom(CustomError( + format!( + "Mismatch between the nonce in the Bridge pool root proof \ + ({}) and the latest Bridge pool nonce in storage ({})", + signed_root.data.1, latest_bp_nonce, + ) + .into(), + ))); + } + // get the merkle tree corresponding to the above root. let tree = ctx .wl_storage @@ -318,14 +388,19 @@ where .into(), ))); } - let transfers = values - .iter() - .map(|bytes| { - PendingTransfer::try_from_slice(bytes) - .expect("Deserializing storage shouldn't fail") - .into() - }) - .collect(); + let (transfers, appendices) = values.iter().fold( + (vec![], vec![]), + |(mut transfers, mut appendices), bytes| { + let pending = PendingTransfer::try_from_slice(bytes) + .expect("Deserializing storage shouldn't fail"); + let eth_transfer = (&pending).into(); + if with_appendix { + appendices.push(pending.into_appendix()); + } + transfers.push(eth_transfer); + (transfers, appendices) + }, + ); // get the membership proof match tree.get_sub_tree_existence_proof( &keys, @@ -336,7 +411,7 @@ where .wl_storage .ethbridge_queries() .get_validator_set_args(None); - let data = RelayProof { + let relay_proof = RelayProof { validator_set_args: validator_args.into(), signatures: sort_sigs( &voting_powers, @@ -349,9 +424,13 @@ where batch_nonce: signed_root.data.1.into(), relayer_address: relayer.to_string(), }; - let data = ethers::abi::AbiEncode::encode(data) - .try_to_vec() - .expect("Serializing a relay proof should not fail."); + let rsp = GenBridgePoolProofRsp { + abi_encoded_proof: ethers::abi::AbiEncode::encode( + relay_proof, + ), + appendices: with_appendix.then_some(appendices), + }; + let data = rsp.try_to_vec().into_storage_result()?; Ok(EncodedResponseQuery { data, ..Default::default() @@ -372,7 +451,7 @@ where /// backing each `TransferToEthereum` event. fn transfer_to_ethereum_progress( ctx: RequestCtx<'_, D, H>, -) -> storage_api::Result> +) -> storage_api::Result> where D: 'static + DB + for<'iter> DBIter<'iter> + Sync, H: 'static + StorageHasher + Sync, @@ -427,6 +506,12 @@ where ) .average_voting_power(ctx.wl_storage); for transfer in transfers { + let key = get_key_from_hash(&transfer.keccak256()); + let transfer = ctx + .wl_storage + .read::(&key) + .into_storage_result()? + .expect("The transfer must be present in storage"); pending_events.insert(transfer, voting_power); } } @@ -566,6 +651,7 @@ mod test_ethbridge_router { use namada_core::ledger::eth_bridge::storage::bridge_pool::{ get_pending_key, get_signed_root_key, BridgePoolTree, }; + use namada_core::ledger::eth_bridge::storage::whitelist; use namada_core::ledger::storage::mockdb::MockDBWriteBatch; use namada_core::ledger::storage_api::StorageWrite; use namada_core::types::address::testing::established_address_1; @@ -577,7 +663,6 @@ mod test_ethbridge_router { use namada_core::types::voting_power::{ EthBridgeVotingPower, FractionalVotingPower, }; - use namada_ethereum_bridge::parameters::read_native_erc20_address; use namada_ethereum_bridge::protocol::transactions::validator_set_update::aggregate_votes; use namada_ethereum_bridge::storage::proof::BridgePoolRootProof; use namada_proof_of_stake::pos_queries::PosQueries; @@ -586,9 +671,10 @@ mod test_ethbridge_router { use super::*; use crate::ledger::queries::testing::TestClient; use crate::ledger::queries::RPC; + use crate::types::address::nam; use crate::types::eth_abi::Encode; use crate::types::eth_bridge_pool::{ - GasFee, PendingTransfer, TransferToEthereum, + GasFee, PendingTransfer, TransferToEthereum, TransferToEthereumKind, }; use crate::types::ethereum_events::EthAddress; @@ -788,12 +874,14 @@ mod test_ethbridge_router { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -829,12 +917,14 @@ 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(), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -889,12 +979,14 @@ 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(), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -954,9 +1046,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - (vec![transfer.keccak256()], bertha_address()) - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -979,7 +1075,7 @@ mod test_ethbridge_router { let data = RelayProof { validator_set_args: validator_args.into(), signatures: sort_sigs(&voting_powers, &signed_root.signatures), - transfers: vec![transfer.into()], + transfers: vec![(&transfer).into()], pool_root: signed_root.data.0.0, proof: proof.proof.into_iter().map(|hash| hash.0).collect(), proof_flags: proof.flags, @@ -987,23 +1083,24 @@ mod test_ethbridge_router { relayer_address: bertha_address().to_string(), }; let proof = ethers::abi::AbiEncode::encode(data); - assert_eq!(proof, resp.data); + assert_eq!(proof, resp.data.abi_encoded_proof); } - /// Test if the merkle tree including a transfer - /// has had its root signed, then we cannot generate - /// a proof. + /// Test if the merkle tree including a transfer has not had its + /// root signed, then we cannot generate a proof. #[tokio::test] async fn test_cannot_get_proof() { let mut client = TestClient::new(RPC); let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: EthAddress([0; 20]), recipient: EthAddress([0; 20]), sender: bertha_address(), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1069,9 +1166,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - (vec![transfer2.keccak256()], bertha_address()) - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer2.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1088,12 +1189,14 @@ 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(), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1159,12 +1262,14 @@ 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(), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1181,15 +1286,8 @@ mod test_ethbridge_router { ) .expect("Test failed"); - let event_transfer = - namada_core::types::ethereum_events::TransferToEthereum { - asset: transfer.transfer.asset, - receiver: transfer.transfer.recipient, - amount: transfer.transfer.amount, - gas_payer: transfer.gas_fee.payer.clone(), - gas_amount: transfer.gas_fee.amount, - sender: transfer.transfer.sender.clone(), - }; + let event_transfer: namada_core::types::ethereum_events::TransferToEthereum + = (&transfer).into(); let eth_event = EthereumEvent::TransfersToEthereum { nonce: Default::default(), transfers: vec![event_transfer.clone()], @@ -1250,10 +1348,8 @@ mod test_ethbridge_router { .transfer_to_ethereum_progress(&client) .await .unwrap(); - let expected: HashMap< - namada_core::types::ethereum_events::TransferToEthereum, - FractionalVotingPower, - > = [(event_transfer, voting_power)].into_iter().collect(); + let expected: HashMap = + [(transfer, voting_power)].into_iter().collect(); assert_eq!(expected, resp); } @@ -1269,12 +1365,14 @@ 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(), amount: 0.into(), }, gas_fee: GasFee { + token: nam(), amount: 0.into(), payer: bertha_address(), }, @@ -1329,9 +1427,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - vec![(transfer.keccak256(), bertha_address())] - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1352,9 +1454,13 @@ mod test_ethbridge_router { .generate_bridge_pool_proof( &client, Some( - vec![transfer.keccak256()] - .try_to_vec() - .expect("Test failed"), + GenBridgePoolProofReq { + transfers: vec![transfer.keccak256()].into(), + relayer: Cow::Owned(bertha_address()), + with_appendix: false, + } + .try_to_vec() + .expect("Test failed"), ), None, false, @@ -1364,45 +1470,9 @@ mod test_ethbridge_router { assert!(resp.is_err()); } - /// Test reading the wrapped NAM supply + /// Test reading the supply and cap of an ERC20 token. #[tokio::test] - async fn test_read_wnam_supply() { - let mut client = TestClient::new(RPC); - assert_eq!(client.wl_storage.storage.last_epoch.0, 0); - - // initialize storage - test_utils::init_default_storage(&mut client.wl_storage); - - let native_erc20 = - read_native_erc20_address(&client.wl_storage).expect("Test failed"); - - // write tokens to storage - let amount = Amount::native_whole(12345); - let token = &client.wl_storage.storage.native_token; - client - .wl_storage - .write(&minted_balance_key(token), amount) - .expect("Test failed"); - - // commit the changes - client - .wl_storage - .storage - .commit_block(MockDBWriteBatch) - .expect("Test failed"); - - // check that reading wrapped NAM fails - let result = RPC - .shell() - .eth_bridge() - .read_erc20_supply(&client, &native_erc20) - .await; - assert_matches!(result, Ok(Some(a)) if a == amount); - } - - /// Test reading the supply of an ERC20 token. - #[tokio::test] - async fn test_read_erc20_supply() { + async fn test_get_erc20_flow_control() { const ERC20_TOKEN: EthAddress = EthAddress([0; 20]); let mut client = TestClient::new(RPC); @@ -1411,29 +1481,49 @@ mod test_ethbridge_router { // initialize storage test_utils::init_default_storage(&mut client.wl_storage); - // check supply - should be None + // check supply - should be 0 let result = RPC .shell() .eth_bridge() - .read_erc20_supply(&client, &ERC20_TOKEN) + .get_erc20_flow_control(&client, &ERC20_TOKEN) .await; - assert_matches!(result, Ok(None)); + assert_matches!( + result, + Ok(f) if f.supply.is_zero() && f.cap.is_zero() + ); // write tokens to storage - let amount = Amount::native_whole(12345); - let token = wrapped_erc20s::token(&ERC20_TOKEN); + let supply_amount = Amount::native_whole(123); + let cap_amount = Amount::native_whole(12345); + let key = whitelist::Key { + asset: ERC20_TOKEN, + suffix: whitelist::KeyType::WrappedSupply, + } + .into(); client .wl_storage - .write(&minted_balance_key(&token), amount) + .write(&key, supply_amount) + .expect("Test failed"); + let key = whitelist::Key { + asset: ERC20_TOKEN, + suffix: whitelist::KeyType::Cap, + } + .into(); + client + .wl_storage + .write(&key, cap_amount) .expect("Test failed"); // check that the supply was updated let result = RPC .shell() .eth_bridge() - .read_erc20_supply(&client, &ERC20_TOKEN) + .get_erc20_flow_control(&client, &ERC20_TOKEN) .await; - assert_matches!(result, Ok(Some(a)) if a == amount); + assert_matches!( + result, + Ok(f) if f.supply == supply_amount && f.cap == cap_amount + ); } } diff --git a/shared/src/ledger/rpc.rs b/shared/src/ledger/rpc.rs index d0fad687be..a4f24e666f 100644 --- a/shared/src/ledger/rpc.rs +++ b/shared/src/ledger/rpc.rs @@ -983,6 +983,7 @@ pub async fn validate_amount( force: bool, ) -> Option { let input_amount = match amount { + InputAmount::Unvalidated(amt) if amt.is_zero() => return Some(amt), InputAmount::Unvalidated(amt) => amt.canonical(), InputAmount::Validated(amt) => return Some(amt), }; diff --git a/tests/src/native_vp/eth_bridge_pool.rs b/tests/src/native_vp/eth_bridge_pool.rs index 544889b2d1..22584e1464 100644 --- a/tests/src/native_vp/eth_bridge_pool.rs +++ b/tests/src/native_vp/eth_bridge_pool.rs @@ -5,14 +5,15 @@ mod test_bridge_pool_vp { use borsh::{BorshDeserialize, BorshSerialize}; use namada::core::ledger::eth_bridge::storage::bridge_pool::BRIDGE_POOL_ADDRESS; use namada::ledger::eth_bridge::{ - wrapped_erc20s, Contracts, EthereumBridgeConfig, UpgradeableContract, + wrapped_erc20s, Contracts, Erc20WhitelistEntry, EthereumBridgeConfig, + UpgradeableContract, }; use namada::ledger::native_vp::ethereum_bridge::bridge_pool_vp::BridgePoolVp; use namada::proto::Tx; 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}; @@ -29,6 +30,7 @@ mod test_bridge_pool_vp { const BERTHA_TOKENS: u64 = 10_000; const GAS_FEE: u64 = 100; const TOKENS: u64 = 10; + const TOKEN_CAP: u64 = TOKENS; /// A signing keypair for good old Bertha. fn bertha_keypair() -> common::SecretKey { @@ -63,6 +65,10 @@ mod test_bridge_pool_vp { ..Default::default() }; let config = EthereumBridgeConfig { + erc20_whitelist: vec![Erc20WhitelistEntry { + token_address: wnam(), + token_cap: Amount::from_u64(TOKEN_CAP).native_denominated(), + }], eth_start_height: Default::default(), min_confirmations: Default::default(), contracts: Contracts { @@ -88,19 +94,37 @@ mod test_bridge_pool_vp { // Bertha has ERC20 tokens too. let token = wrapped_erc20s::token(&ASSET); env.credit_tokens(&bertha_address(), &token, BERTHA_TOKENS.into()); + // Bertha has... NUTs? :D + let nuts = wrapped_erc20s::nut(&ASSET); + env.credit_tokens(&bertha_address(), &nuts, BERTHA_TOKENS.into()); + // give Bertha some wNAM. technically this is impossible to mint, + // but we're testing invalid protocol paths... + let wnam_tok_addr = wrapped_erc20s::token(&wnam()); + env.credit_tokens( + &bertha_address(), + &wnam_tok_addr, + BERTHA_TOKENS.into(), + ); env } - fn validate_tx(tx: Tx) { + fn run_vp(tx: Tx) -> bool { let env = setup_env(tx); tx_host_env::set(env); let mut tx_env = tx_host_env::take(); tx_env.execute_tx().expect("Test failed."); let vp_env = TestNativeVpEnv::from_tx_env(tx_env, BRIDGE_POOL_ADDRESS); - let result = vp_env + vp_env .validate_tx(|ctx| BridgePoolVp { ctx }) - .expect("Test failed"); - assert!(result); + .expect("Test failed") + } + + fn validate_tx(tx: Tx) { + assert!(run_vp(tx)); + } + + fn invalidate_tx(tx: Tx) { + assert!(!run_vp(tx)); } fn create_tx(transfer: PendingTransfer, keypair: &common::SecretKey) -> Tx { @@ -119,12 +143,14 @@ 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(), amount: Amount::from(TOKENS), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: bertha_address(), }, @@ -136,12 +162,14 @@ 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(), amount: Amount::from(TOKENS), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: bertha_address(), }, @@ -149,20 +177,117 @@ mod test_bridge_pool_vp { validate_tx(create_tx(transfer, &bertha_keypair())); } + #[test] + fn invalidate_wnam_over_cap_tx() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKEN_CAP + 1), + }, + gas_fee: GasFee { + token: nam(), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + invalidate_tx(create_tx(transfer, &bertha_keypair())); + } + #[test] fn validate_mint_wnam_different_sender_tx() { let transfer = PendingTransfer { transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, asset: wnam(), recipient: EthAddress([0; 20]), sender: bertha_address(), amount: Amount::from(TOKENS), }, gas_fee: GasFee { + token: nam(), amount: Amount::from(GAS_FEE), payer: albert_address(), }, }; validate_tx(create_tx(transfer, &bertha_keypair())); } + + #[test] + fn invalidate_fees_paid_in_nuts() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::nut(&ASSET), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + invalidate_tx(create_tx(transfer, &bertha_keypair())); + } + + #[test] + fn invalidate_fees_paid_in_wnam() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::token(&wnam()), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + invalidate_tx(create_tx(transfer, &bertha_keypair())); + } + + #[test] + fn validate_erc20_tx_with_same_gas_token() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: ASSET, + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::token(&ASSET), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + validate_tx(create_tx(transfer, &bertha_keypair())); + } + + #[test] + fn validate_wnam_tx_with_diff_gas_token() { + let transfer = PendingTransfer { + transfer: TransferToEthereum { + kind: TransferToEthereumKind::Erc20, + asset: wnam(), + recipient: EthAddress([0; 20]), + sender: bertha_address(), + amount: Amount::from(TOKENS), + }, + gas_fee: GasFee { + token: wrapped_erc20s::token(&ASSET), + amount: Amount::from(GAS_FEE), + payer: bertha_address(), + }, + }; + validate_tx(create_tx(transfer, &bertha_keypair())); + } } diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index be65562b6d..2e083c3f46 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.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethers", diff --git a/wasm/wasm_source/src/tx_bridge_pool.rs b/wasm/wasm_source/src/tx_bridge_pool.rs index bf73e83f7d..765029d647 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::*; @@ -12,13 +12,16 @@ fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { .map_err(|e| Error::wrap("Error deserializing PendingTransfer", e))?; log_string("Received transfer to add to pool."); // pay the gas fees - let GasFee { amount, ref payer } = transfer.gas_fee; - let nam_addr = ctx.get_native_token().unwrap(); + let GasFee { + token: ref fee_token_addr, + amount, + ref payer, + } = transfer.gas_fee; token::transfer( ctx, payer, &bridge_pool::BRIDGE_POOL_ADDRESS, - &nam_addr, + fee_token_addr, amount.native_denominated(), &None, &None, @@ -33,6 +36,7 @@ fn apply_tx(ctx: &mut Ctx, signed: Tx) -> TxResult { } = transfer.transfer; // if minting wNam, escrow the correct amount if asset == native_erc20_address(ctx)? { + let nam_addr = ctx.get_native_token()?; token::transfer( ctx, sender, @@ -45,7 +49,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 77b2082a4e..108e62cfad 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1677,8 +1677,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-bridge-events", "ethbridge-structs", @@ -1688,8 +1688,8 @@ dependencies = [ [[package]] name = "ethbridge-bridge-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1699,8 +1699,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-contract" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethbridge-governance-events", "ethbridge-structs", @@ -1710,8 +1710,8 @@ dependencies = [ [[package]] name = "ethbridge-governance-events" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethbridge-structs", @@ -1721,8 +1721,8 @@ dependencies = [ [[package]] name = "ethbridge-structs" -version = "0.18.0" -source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.18.0#d49a0d110bb726c526896ff440d542585ced12f2" +version = "0.23.0" +source = "git+https://github.com/heliaxdev/ethbridge-rs?tag=v0.23.0#1bb96e06cbc3889aa46a01e3768bf25f0c78168a" dependencies = [ "ethabi", "ethers",