diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index e490a04b0..f6ad3c93f 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -11,7 +11,6 @@ jobs: steps: - name: Clone the repository uses: actions/checkout@v2 - - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - run: make release - run: ls -lH mainnet-release.wasm - name: Upload the mainnet-release.wasm artifact @@ -34,7 +33,6 @@ jobs: steps: - name: Clone the repository uses: actions/checkout@v2 - - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - run: make debug - run: ls -lH mainnet-debug.wasm - name: Upload the mainnet-debug.wasm artifact diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml index 354c0ffd6..ce50f5641 100644 --- a/.github/workflows/lints.yml +++ b/.github/workflows/lints.yml @@ -1,6 +1,14 @@ # See: https://github.com/actions-rs/example/blob/master/.github/workflows/nightly_lints.yml --- -on: [push, pull_request] +on: + push: + branches: + - master + - develop + pull_request: + branches: + - "*" + name: Lints jobs: fmt: @@ -9,7 +17,6 @@ jobs: steps: - name: Clone the repository uses: actions/checkout@v2 - - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Run cargo fmt run: cargo fmt --all -- --check clippy: @@ -18,7 +25,6 @@ jobs: steps: - name: Clone the repository uses: actions/checkout@v2 - - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - run: make etc/eth-contracts/res/EvmErc20.bin - name: Run Contract cargo clippy run: cargo clippy --no-default-features --features=contract -- -D warnings diff --git a/.github/workflows/scheduled_lints.yml b/.github/workflows/scheduled_lints.yml index eea60553a..d03a50b6a 100644 --- a/.github/workflows/scheduled_lints.yml +++ b/.github/workflows/scheduled_lints.yml @@ -10,7 +10,8 @@ jobs: steps: - name: Clone the repository uses: actions/checkout@v2 - - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - run: make etc/eth-contracts/res/EvmErc20.bin + - name: Update toolchain + run: rustup update nightly - name: Run cargo clippy - run: cargo clippy --no-default-features --features=mainnet -- -D warnings + run: cargo +nightly clippy --no-default-features --features=mainnet -- -D warnings diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 24c0deef9..89a3edf96 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,5 +1,13 @@ --- -on: [push, pull_request] +on: + push: + branches: + - master + - develop + pull_request: + branches: + - "*" + name: Tests jobs: test: @@ -11,27 +19,33 @@ jobs: steps: - name: Clone the repository uses: actions/checkout@v2 - - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - name: Cache lookup for target dir - run: cache-util -restore -path target -key aurora-engine-target@${{ matrix.net }}net@${{ hashFiles('**/Cargo.lock') }} - - run: make ${{ matrix.net }}net-test-build - - name: Run ${{ matrix.net }}net cargo test - run: cargo test --locked --verbose --features ${{ matrix.net }}net-test - - name: Caching target dir - run: cache-util -save -move -path target -key aurora-engine-target@${{ matrix.net }}net@${{ hashFiles('**/Cargo.lock') }} + - name: Restore cache + run: | + cache-util restore cargo_git cargo_registry sccache yarn_cache + cache-util restore aurora-engine-target@${{ matrix.net }}net@${{ hashFiles('**/Cargo.lock') }}:target + - run: make test-${{ matrix.net }}net +# - run: rm -rf target/solidity_build/ + - name: Save cache + run: | + cache-util save cargo_git cargo_registry sccache yarn_cache + cache-util msave aurora-engine-target@${{ matrix.net }}net@${{ hashFiles('**/Cargo.lock') }}:target bully-build: name: Bully build runs-on: self-hosted steps: - name: Clone the repository uses: actions/checkout@v2 - - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH - - name: Cache lookup for target dir - run: cache-util -restore -path target -key aurora-engine-target@bully@${{ hashFiles('**/Cargo.lock') }} + - name: Restore cache + run: | + cache-util restore cargo_git cargo_registry sccache yarn_cache + cache-util restore aurora-engine-target@bully@${{ hashFiles('**/Cargo.lock') }}:target - run: make mainnet-debug evm-bully=yes - run: ls -lH mainnet-debug.wasm - - name: Cache target dir - run: cache-util -save -move -path target -key aurora-engine-target@bully@${{ hashFiles('**/Cargo.lock') }} +# - run: rm -rf target/solidity_build/ + - name: Save cache + run: | + cache-util save cargo_git cargo_registry sccache yarn_cache + cache-util msave aurora-engine-target@bully@${{ hashFiles('**/Cargo.lock') }}:target env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 diff --git a/.gitignore b/.gitignore index eb342215e..f0b0d9eb7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ artifacts/ cache/ node_modules/ -res/ +etc/eth-contracts/res/ # Other etc/state-migration-test/target/ diff --git a/CHANGES.md b/CHANGES.md index fbb7e3962..ab92acaa0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.6.0] - 2021-08-13 + ## [1.5.0] - 2021-07-30 ## [1.4.3] - 2021-07-08 @@ -23,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] - 2021-05-12 +[1.6.0]: https://github.com/aurora-is-near/aurora-engine/compare/1.5.0...1.6.0 [1.5.0]: https://github.com/aurora-is-near/aurora-engine/compare/1.4.3...1.5.0 [1.4.3]: https://github.com/aurora-is-near/aurora-engine/compare/1.4.2...1.4.3 [1.4.2]: https://github.com/aurora-is-near/aurora-engine/compare/1.4.1...1.4.2 diff --git a/Cargo.lock b/Cargo.lock index c9869aee9..d6d2c4c15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,7 +115,7 @@ dependencies = [ [[package]] name = "aurora-engine" -version = "1.5.0" +version = "1.6.0" dependencies = [ "aurora-bn", "base64 0.13.0", @@ -239,7 +239,8 @@ dependencies = [ [[package]] name = "blake2" version = "0.9.1" -source = "git+https://github.com/near/near-blake2.git#736ff607cc8160af87ffa697c14ebef85050138f" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a5720225ef5daecf08657f23791354e1685a8c91a4c60c7f3d3b2892f978f4" dependencies = [ "crypto-mac 0.8.0", "digest 0.9.0", @@ -249,8 +250,7 @@ dependencies = [ [[package]] name = "blake2" version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a5720225ef5daecf08657f23791354e1685a8c91a4c60c7f3d3b2892f978f4" +source = "git+https://github.com/near/near-blake2.git#736ff607cc8160af87ffa697c14ebef85050138f" dependencies = [ "crypto-mac 0.8.0", "digest 0.9.0", @@ -1461,9 +1461,9 @@ checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" [[package]] name = "git2" -version = "0.13.19" +version = "0.13.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17929de7239dea9f68aa14f94b2ab4974e7b24c1314275ffcc12a7758172fa18" +checksum = "d9831e983241f8c5591ed53f17d874833e2fa82cac2625f3888c50cbfe136cba" dependencies = [ "bitflags", "libc", @@ -1734,9 +1734,9 @@ checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "libgit2-sys" -version = "0.12.20+1.1.0" +version = "0.12.21+1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2f09917e00b9ad194ae72072bb5ada2cca16d8171a43e91ddba2afbb02664b" +checksum = "86271bacd72b2b9e854c3dcfb82efd538f15f870e4c11af66900effb462f6825" dependencies = [ "cc", "libc", @@ -2205,7 +2205,7 @@ dependencies = [ [[package]] name = "near-sdk" version = "3.1.0" -source = "git+https://github.com/aurora-is-near/near-sdk-rs?rev=1b9843fe5b652928582e33879fc92ba87a639450#1b9843fe5b652928582e33879fc92ba87a639450" +source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=1b9843fe5b652928582e33879fc92ba87a639450#1b9843fe5b652928582e33879fc92ba87a639450" dependencies = [ "base64 0.13.0", "borsh", @@ -2221,7 +2221,7 @@ dependencies = [ [[package]] name = "near-sdk-core" version = "3.1.0" -source = "git+https://github.com/aurora-is-near/near-sdk-rs?rev=1b9843fe5b652928582e33879fc92ba87a639450#1b9843fe5b652928582e33879fc92ba87a639450" +source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=1b9843fe5b652928582e33879fc92ba87a639450#1b9843fe5b652928582e33879fc92ba87a639450" dependencies = [ "Inflector", "proc-macro2", @@ -2232,7 +2232,7 @@ dependencies = [ [[package]] name = "near-sdk-macros" version = "3.1.0" -source = "git+https://github.com/aurora-is-near/near-sdk-rs?rev=1b9843fe5b652928582e33879fc92ba87a639450#1b9843fe5b652928582e33879fc92ba87a639450" +source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=1b9843fe5b652928582e33879fc92ba87a639450#1b9843fe5b652928582e33879fc92ba87a639450" dependencies = [ "near-sdk-core", "proc-macro2", @@ -2243,7 +2243,7 @@ dependencies = [ [[package]] name = "near-sdk-sim" version = "3.2.0" -source = "git+https://github.com/aurora-is-near/near-sdk-rs?rev=1b9843fe5b652928582e33879fc92ba87a639450#1b9843fe5b652928582e33879fc92ba87a639450" +source = "git+https://github.com/aurora-is-near/near-sdk-rs.git?rev=1b9843fe5b652928582e33879fc92ba87a639450#1b9843fe5b652928582e33879fc92ba87a639450" dependencies = [ "chrono", "funty", @@ -3130,9 +3130,8 @@ dependencies = [ [[package]] name = "rjson" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5510dbde48c4c37bf69123b1f636b6dd5f8dffe1f4e358af03c46a4947dca219" +version = "0.3.2" +source = "git+https://github.com/aurora-is-near/rjson?rev=cc3da949#cc3da9495e7e520900d66d2b517539f74fff93cf" [[package]] name = "rlp" diff --git a/Cargo.toml b/Cargo.toml index f768d195c..68c314674 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aurora-engine" -version = "1.5.0" +version = "1.6.0" authors = ["NEAR "] edition = "2018" description = "" @@ -45,6 +45,7 @@ lto = true opt-level = 3 [dependencies] +base64 = { version = "0.13.0", default-features = false, features = ["alloc"] } blake2 = { git = "https://github.com/near/near-blake2.git", version = "0.9.1", default-features = false } borsh = { version = "0.8.2", default-features = false } bn = { package = "aurora-bn", git = "https://github.com/aurora-is-near/aurora-bn.git", default-features = false } @@ -62,7 +63,7 @@ logos = { version = "0.12", default-features = false, features = ["export_derive ethabi = { git = "https://github.com/darwinia-network/ethabi", branch = "xavier-no-std", default-features = false } hex = { version = "0.4", default-features = false, features = ["alloc"] } byte-slice-cast = { version = "1.0", default-features = false } -rjson = { version = "0.3.1", default-features = false } +rjson = { git = "https://github.com/aurora-is-near/rjson", rev = "cc3da949", default-features = false, features = ["integer"] } [dev-dependencies] bstr = "0.2" @@ -80,7 +81,6 @@ rand = "0.7.3" criterion = "0.3.4" git2 = "0.13" lazy-static-include = "3.1.1" -base64 = "0.13.0" [features] default = ["sha2", "std"] diff --git a/Makefile b/Makefile index 8e9b34172..aa81f0715 100644 --- a/Makefile +++ b/Makefile @@ -68,13 +68,22 @@ target/wasm32-unknown-unknown/debug/aurora_engine.wasm: Cargo.toml Cargo.lock $( test: test-mainnet mainnet-test-build: FEATURES=mainnet,integration-test,meta-call -mainnet-test-build: mainnet-release.wasm +mainnet-test-build: mainnet-test.wasm betanet-test-build: FEATURES=betanet,integration-test,meta-call -betanet-test-build: betanet-release.wasm +betanet-test-build: betanet-test.wasm testnet-test-build: FEATURES=testnet,integration-test,meta-call -testnet-test-build: testnet-release.wasm +testnet-test-build: testnet-test.wasm + +mainnet-test.wasm: target/wasm32-unknown-unknown/release/aurora_engine.wasm + cp $< $@ + +testnet-test.wasm: target/wasm32-unknown-unknown/release/aurora_engine.wasm + cp $< $@ + +betanet-test.wasm: target/wasm32-unknown-unknown/release/aurora_engine.wasm + cp $< $@ test-mainnet: mainnet-test-build $(CARGO) test --features mainnet-test @@ -104,6 +113,7 @@ format: clean: @rm -Rf *.wasm + @rm -Rf etc/eth-contracts/res cargo clean .PHONY: release mainnet testnet betanet compile-release test-build deploy check check-format check-clippy test test-sol format clean debug mainnet-debug testnet-debug betanet-debug compile-debug mainnet-test-build testnet-test-build betanet-test-build target/wasm32-unknown-unknown/release/aurora_engine.wasm target/wasm32-unknown-unknown/debug/aurora_engine.wasm diff --git a/VERSION b/VERSION index bc80560fa..dc1e644a1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.0 +1.6.0 diff --git a/etc/eth-contracts/yarn.lock b/etc/eth-contracts/yarn.lock index a8ab03d78..7053fda07 100644 --- a/etc/eth-contracts/yarn.lock +++ b/etc/eth-contracts/yarn.lock @@ -9657,9 +9657,9 @@ tar-stream@^1.5.2: xtend "^4.0.0" tar@^4.0.2: - version "4.4.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" - integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + version "4.4.15" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.15.tgz#3caced4f39ebd46ddda4d6203d48493a919697f8" + integrity sha512-ItbufpujXkry7bHH9NpQyTXPbJ72iTlXgkBAYsAjDXk3Ds8t/3NfO5P4xZGy7u+sYuQUbimgzswX4uQIEeNVOA== dependencies: chownr "^1.1.1" fs-minipass "^1.2.5" diff --git a/etc/state-migration-test/Cargo.lock b/etc/state-migration-test/Cargo.lock index d440d8c53..bfb0aedf8 100644 --- a/etc/state-migration-test/Cargo.lock +++ b/etc/state-migration-test/Cargo.lock @@ -31,9 +31,10 @@ dependencies = [ [[package]] name = "aurora-engine" -version = "1.0.0" +version = "1.6.0" dependencies = [ "aurora-bn", + "base64", "blake2", "borsh", "byte-slice-cast", @@ -67,6 +68,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "beef" version = "0.5.0" @@ -707,9 +714,8 @@ dependencies = [ [[package]] name = "rjson" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5510dbde48c4c37bf69123b1f636b6dd5f8dffe1f4e358af03c46a4947dca219" +version = "0.3.2" +source = "git+https://github.com/aurora-is-near/rjson?rev=cc3da949#cc3da9495e7e520900d66d2b517539f74fff93cf" [[package]] name = "rlp" diff --git a/rust-toolchain b/rust-toolchain index 9ade1f340..d00399aad 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2021-03-25" +channel = "nightly-2021-08-01" components = [] targets = ["wasm32-unknown-unknown"] diff --git a/src/connector.rs b/src/connector.rs index bed566ebf..2a80229af 100644 --- a/src/connector.rs +++ b/src/connector.rs @@ -26,7 +26,7 @@ pub(crate) const PAUSE_WITHDRAW: PausedMask = 1 << 1; #[derive(BorshSerialize, BorshDeserialize)] pub struct EthConnectorContract { contract: EthConnector, - ft: FungibleToken, + pub ft: FungibleToken, paused_mask: PausedMask, } @@ -82,6 +82,7 @@ impl EthConnectorContract { let contract_data = Self::set_contract_data(SetContractDataCallArgs { prover_account: args.prover_account, eth_custodian_address: args.eth_custodian_address, + metadata: args.metadata, }); let current_account_id = sdk::current_account_id(); @@ -118,6 +119,11 @@ impl EthConnectorContract { &contract_data, ); + sdk::save_contract( + &Self::get_contract_key(&EthConnectorStorageId::FungibleTokenMetadata), + &args.metadata, + ); + contract_data } @@ -578,7 +584,7 @@ impl EthConnectorContract { } /// Save eth-connector contract data - fn save_ft_contract(&mut self) { + pub(crate) fn save_ft_contract(&mut self) { sdk::save_contract( &Self::get_contract_key(&EthConnectorStorageId::FungibleToken), &self.ft, @@ -616,6 +622,14 @@ impl EthConnectorContract { pub fn set_paused_flags(&mut self, args: PauseEthConnectorCallArgs) { self.set_paused(args.paused_mask); } + + /// Return metdata + pub fn get_metadata() -> Option { + sdk::read_storage(&Self::get_contract_key( + &EthConnectorStorageId::FungibleTokenMetadata, + )) + .and_then(|data| FungibleTokenMetadata::try_from_slice(&data).ok()) + } } impl AdminControlled for EthConnectorContract { diff --git a/src/engine.rs b/src/engine.rs index fa2f2e7b3..58cc7bb83 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,4 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use core::mem; use evm::backend::{Apply, ApplyBackend, Backend, Basic, Log}; use evm::executor::{StackExecutor, StackSubstateMetadata}; use evm::ExitFatal; @@ -10,7 +11,7 @@ use crate::contract::current_address; use crate::map::{BijectionMap, LookupMap}; use crate::parameters::{ FunctionCallArgs, NEP141FtOnTransferArgs, NewCallArgs, PromiseCreateArgs, SubmitResult, - ViewCallArgs, + TransactionStatus, ViewCallArgs, }; use crate::precompiles::Precompiles; @@ -20,6 +21,13 @@ use crate::state::AuroraStackState; use crate::storage::{address_to_key, bytes_to_key, storage_to_key, KeyPrefix, KeyPrefixU8}; use crate::types::{u256_to_arr, AccountId, Wei, ERC20_MINT_SELECTOR}; +/// Used as the first byte in the concatenation of data used to compute the blockhash. +/// Could be useful in the future as a version byte, or to distinguish different types of blocks. +const BLOCK_HASH_PREFIX: u8 = 0; +const BLOCK_HASH_PREFIX_SIZE: usize = 1; +const BLOCK_HEIGHT_SIZE: usize = 8; +const CHAIN_ID_SIZE: usize = 32; + #[cfg(not(feature = "contract"))] pub fn current_address() -> Address { crate::types::near_account_to_evm_address("engine".as_bytes()) @@ -48,9 +56,27 @@ macro_rules! assert_or_finish { }; } +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct EngineError { + pub kind: EngineErrorKind, + pub gas_used: u64, +} + +impl AsRef for EngineError { + fn as_ref(&self) -> &str { + self.kind.to_str() + } +} + +impl AsRef<[u8]> for EngineError { + fn as_ref(&self) -> &[u8] { + self.kind.to_str().as_bytes() + } +} + /// Errors with the EVM engine. #[derive(Debug, Clone, Eq, PartialEq)] -pub enum EngineError { +pub enum EngineErrorKind { /// Normal EVM errors. EvmError(ExitError), /// Fatal EVM errors. @@ -59,9 +85,16 @@ pub enum EngineError { IncorrectNonce, } -impl EngineError { +impl EngineErrorKind { + pub fn with_gas_used(self, gas_used: u64) -> EngineError { + EngineError { + kind: self, + gas_used, + } + } + pub fn to_str(&self) -> &str { - use EngineError::*; + use EngineErrorKind::*; match self { EvmError(ExitError::StackUnderflow) => "ERR_STACK_UNDERFLOW", EvmError(ExitError::StackOverflow) => "ERR_STACK_OVERFLOW", @@ -85,27 +118,27 @@ impl EngineError { } } -impl AsRef for EngineError { +impl AsRef for EngineErrorKind { fn as_ref(&self) -> &str { self.to_str() } } -impl AsRef<[u8]> for EngineError { +impl AsRef<[u8]> for EngineErrorKind { fn as_ref(&self) -> &[u8] { self.to_str().as_bytes() } } -impl From for EngineError { +impl From for EngineErrorKind { fn from(e: ExitError) -> Self { - EngineError::EvmError(e) + EngineErrorKind::EvmError(e) } } -impl From for EngineError { +impl From for EngineErrorKind { fn from(e: ExitFatal) -> Self { - EngineError::EvmFatal(e) + EngineErrorKind::EvmFatal(e) } } @@ -114,20 +147,55 @@ pub type EngineResult = Result; trait ExitIntoResult { /// Checks if the EVM exit is ok or an error. - fn into_result(self) -> EngineResult<()>; + fn into_result(self, data: Vec) -> Result; } impl ExitIntoResult for ExitReason { - fn into_result(self) -> EngineResult<()> { + fn into_result(self, data: Vec) -> Result { use ExitReason::*; match self { - Succeed(_) | Revert(_) => Ok(()), + Succeed(_) => Ok(TransactionStatus::Succeed(data)), + Revert(_) => Ok(TransactionStatus::Revert(data)), + Error(ExitError::OutOfOffset) => Ok(TransactionStatus::OutOfOffset), + Error(ExitError::OutOfFund) => Ok(TransactionStatus::OutOfFund), + Error(ExitError::OutOfGas) => Ok(TransactionStatus::OutOfGas), Error(e) => Err(e.into()), Fatal(e) => Err(e.into()), } } } +pub struct BalanceOverflow; +impl AsRef<[u8]> for BalanceOverflow { + fn as_ref(&self) -> &[u8] { + b"ERR_BALANCE_OVERFLOW" + } +} + +/// Errors resulting from trying to pay for gas +pub enum GasPaymentError { + /// Overflow adding ETH to an account balance (should never happen) + BalanceOverflow(BalanceOverflow), + /// Overflow in gas * gas_price calculation + EthAmountOverflow, + /// Not enough balance for account to cover the gas cost + OutOfFund, +} +impl AsRef<[u8]> for GasPaymentError { + fn as_ref(&self) -> &[u8] { + match self { + Self::BalanceOverflow(overflow) => overflow.as_ref(), + Self::EthAmountOverflow => b"ERR_GAS_ETH_AMOUNT_OVERFLOW", + Self::OutOfFund => b"ERR_OUT_OF_FUND", + } + } +} +impl From for GasPaymentError { + fn from(overflow: BalanceOverflow) -> Self { + Self::BalanceOverflow(overflow) + } +} + pub const ERR_INVALID_NEP141_ACCOUNT_ID: &str = "ERR_INVALID_NEP141_ACCOUNT_ID"; #[derive(Debug)] @@ -191,7 +259,7 @@ impl AsRef<[u8]> for EngineStateError { /// Should not contain anything large or enumerable. #[derive(BorshSerialize, BorshDeserialize, Default)] pub struct EngineState { - /// Chain id, according to the EIP-115 / ethereum-lists spec. + /// Chain id, according to the EIP-155 / ethereum-lists spec. pub chain_id: [u8; 32], /// Account which can upgrade this contract. /// Use empty to disable updatability. @@ -245,6 +313,93 @@ impl Engine { ); } + /// There is one Aurora block per NEAR block height (note: when heights in NEAR are skipped + /// they are interpreted as empty blocks on Aurora). The blockhash is derived from the height + /// according to + /// ```text + /// block_hash = sha256(concat( + /// BLOCK_HASH_PREFIX, + /// block_height as u64, + /// chain_id, + /// engine_account_id, + /// )) + /// ``` + pub fn compute_block_hash(chain_id: [u8; 32], block_height: u64, account_id: &[u8]) -> H256 { + debug_assert_eq!(BLOCK_HASH_PREFIX_SIZE, mem::size_of_val(&BLOCK_HASH_PREFIX)); + debug_assert_eq!(BLOCK_HEIGHT_SIZE, mem::size_of_val(&block_height)); + debug_assert_eq!(CHAIN_ID_SIZE, mem::size_of_val(&chain_id)); + let mut data = Vec::with_capacity( + BLOCK_HASH_PREFIX_SIZE + BLOCK_HEIGHT_SIZE + CHAIN_ID_SIZE + account_id.len(), + ); + data.push(BLOCK_HASH_PREFIX); + data.extend_from_slice(&chain_id); + data.extend_from_slice(account_id); + data.extend_from_slice(&block_height.to_be_bytes()); + + #[cfg(not(feature = "contract"))] + { + use sha2::Digest; + + let output = sha2::Sha256::digest(&data); + H256(output.into()) + } + + #[cfg(feature = "contract")] + sdk::sha256(&data) + } + + pub fn charge_gas_limit( + sender: &Address, + gas_limit: U256, + gas_price: U256, + ) -> Result { + // Early exit as performance optimization + if gas_price.is_zero() { + return Ok(Wei::zero()); + } + + let payment_for_gas = Wei::new( + gas_limit + .checked_mul(gas_price) + .ok_or(GasPaymentError::EthAmountOverflow)?, + ); + let account_balance = Self::get_balance(sender); + let remaining_balance = account_balance + .checked_sub(payment_for_gas) + .ok_or(GasPaymentError::OutOfFund)?; + + Self::set_balance(sender, &remaining_balance); + + Ok(payment_for_gas) + } + + pub fn refund_unused_gas( + sender: &Address, + relayer: &Address, + prepaid_amount: Wei, + used_gas: u64, + gas_price: U256, + ) -> Result<(), GasPaymentError> { + // Early exit as performance optimization + if gas_price.is_zero() { + return Ok(()); + } + + let used_amount = Wei::new( + gas_price + .checked_mul(used_gas.into()) + .ok_or(GasPaymentError::EthAmountOverflow)?, + ); + // We cannot have used more than the gas_limit + debug_assert!(used_amount <= prepaid_amount); + let refund_amount = prepaid_amount - used_amount; + + Self::add_balance(sender, refund_amount)?; + Self::add_balance(relayer, used_amount)?; + + Ok(()) + } + /// Fails if state is not found. pub fn get_state() -> Result { match sdk::read_storage(&bytes_to_key(KeyPrefix::Config, STATE_KEY)) { @@ -284,11 +439,11 @@ impl Engine { /// Checks the nonce to ensure that the address matches the transaction /// nonce. #[inline] - pub fn check_nonce(address: &Address, transaction_nonce: &U256) -> EngineResult<()> { + pub fn check_nonce(address: &Address, transaction_nonce: &U256) -> Result<(), EngineErrorKind> { let account_nonce = Self::get_nonce(address); if transaction_nonce != &account_nonce { - return Err(EngineError::IncorrectNonce); + return Err(EngineErrorKind::IncorrectNonce); } Ok(()) @@ -300,6 +455,13 @@ impl Engine { .unwrap_or_else(U256::zero) } + pub fn add_balance(address: &Address, amount: Wei) -> Result<(), BalanceOverflow> { + let current_balance = Self::get_balance(address); + let new_balance = current_balance.checked_add(amount).ok_or(BalanceOverflow)?; + Self::set_balance(address, &new_balance); + Ok(()) + } + pub fn set_balance(address: &Address, balance: &Wei) { sdk::write_storage( &address_to_key(KeyPrefix::Balance, address), @@ -397,24 +559,27 @@ impl Engine { ) -> EngineResult { let mut executor = self.make_executor(gas_limit); let address = executor.create_address(CreateScheme::Legacy { caller: origin }); - let (status, result) = ( + let (exit_reason, result) = ( executor.transact_create(origin, value.raw(), input, gas_limit, access_list), address, ); - let is_succeed = status.is_succeed(); - if let Err(e) = status.into_result() { - Engine::increment_nonce(&origin); - return Err(e); - } + let used_gas = executor.used_gas(); + let status = match exit_reason.into_result(result.0.to_vec()) { + Ok(status) => status, + Err(e) => { + Engine::increment_nonce(&origin); + return Err(e.with_gas_used(used_gas)); + } + }; + let (values, logs, promises) = executor.into_state().deconstruct(); self.apply(values, Vec::::new(), true); Self::schedule_promises(promises); Ok(SubmitResult { - status: is_succeed, + status, gas_used: used_gas, - result: result.0.to_vec(), logs: logs.into_iter().map(Into::into).collect(), }) } @@ -436,15 +601,17 @@ impl Engine { access_list: Vec<(Address, Vec)>, // See EIP-2930 ) -> EngineResult { let mut executor = self.make_executor(gas_limit); - let (status, result) = + let (exit_reason, result) = executor.transact_call(origin, contract, value.raw(), input, gas_limit, access_list); - let is_succeed = status.is_succeed(); - if let Err(e) = status.into_result() { - Engine::increment_nonce(&origin); - return Err(e); - } let used_gas = executor.used_gas(); + let status = match exit_reason.into_result(result) { + Ok(status) => status, + Err(e) => { + Engine::increment_nonce(&origin); + return Err(e.with_gas_used(used_gas)); + } + }; let (values, logs, promises) = executor.into_state().deconstruct(); @@ -454,9 +621,8 @@ impl Engine { Self::schedule_promises(promises); Ok(SubmitResult { - status: is_succeed, + status, gas_used: used_gas, - result, logs: logs.into_iter().map(Into::into).collect(), }) } @@ -467,7 +633,7 @@ impl Engine { Self::set_nonce(address, &new_nonce); } - pub fn view_with_args(&self, args: ViewCallArgs) -> EngineResult> { + pub fn view_with_args(&self, args: ViewCallArgs) -> Result { let origin = Address::from_slice(&args.sender); let contract = Address::from_slice(&args.address); let value = U256::from_big_endian(&args.amount); @@ -481,12 +647,11 @@ impl Engine { value: Wei, input: Vec, gas_limit: u64, - ) -> EngineResult> { + ) -> Result { let mut executor = self.make_executor(gas_limit); let (status, result) = executor.transact_call(origin, contract, value.raw(), input, gas_limit, Vec::new()); - status.into_result()?; - Ok(result) + status.into_result(result) } fn make_executor(&self, gas_limit: u64) -> StackExecutor { @@ -705,7 +870,15 @@ impl evm::backend::Backend for Engine { fn block_hash(&self, number: U256) -> H256 { let idx = U256::from(sdk::block_index()); if idx.saturating_sub(U256::from(256)) <= number && number < idx { - H256::from([255u8; 32]) + // since `idx` comes from `u64` it is always safe to downcast `number` from `U256` + #[cfg(feature = "contract")] + { + let account_id = sdk::current_account_id(); + Self::compute_block_hash(self.state.chain_id, number.low_u64(), &account_id) + } + + #[cfg(not(feature = "contract"))] + Self::compute_block_hash(self.state.chain_id, number.low_u64(), b"aurora") } else { H256::zero() } diff --git a/src/fungible_token.rs b/src/fungible_token.rs index 00ff7b5c0..de57c10cb 100644 --- a/src/fungible_token.rs +++ b/src/fungible_token.rs @@ -1,5 +1,7 @@ +use crate::json::JsonValue; #[cfg(feature = "log")] use crate::prelude::format; +use crate::prelude::BTreeMap; use crate::types::*; use borsh::{BorshDeserialize, BorshSerialize}; use { @@ -27,6 +29,67 @@ pub struct FungibleToken { pub account_storage_usage: StorageUsage, } +#[derive(BorshDeserialize, BorshSerialize, Clone)] +pub struct FungibleTokenMetadata { + pub spec: String, + pub name: String, + pub symbol: String, + pub icon: Option, + pub reference: Option, + pub reference_hash: Option<[u8; 32]>, + pub decimals: u8, +} + +impl Default for FungibleTokenMetadata { + fn default() -> Self { + Self { + spec: "ft-1.0.0".to_string(), + name: "Ether".to_string(), + symbol: "ETH".to_string(), + icon: Some("data:image/png;base64,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".to_string()), + reference: None, + reference_hash: None, + decimals: 18, + } + } +} + +impl From for JsonValue { + fn from(metadata: FungibleTokenMetadata) -> Self { + let mut kvs = BTreeMap::new(); + kvs.insert("spec".to_string(), JsonValue::String(metadata.spec)); + kvs.insert("name".to_string(), JsonValue::String(metadata.name)); + kvs.insert("symbol".to_string(), JsonValue::String(metadata.symbol)); + kvs.insert( + "icon".to_string(), + metadata + .icon + .map(JsonValue::String) + .unwrap_or(JsonValue::Null), + ); + kvs.insert( + "reference".to_string(), + metadata + .reference + .map(JsonValue::String) + .unwrap_or(JsonValue::Null), + ); + kvs.insert( + "reference_hash".to_string(), + metadata + .reference_hash + .map(|hash| JsonValue::String(base64::encode(hash))) + .unwrap_or(JsonValue::Null), + ); + kvs.insert( + "decimals".to_string(), + JsonValue::U64(metadata.decimals as u64), + ); + + JsonValue::Object(kvs) + } +} + impl FungibleToken { pub fn new() -> Self { Self::default() diff --git a/src/json.rs b/src/json.rs index dede148a2..242699d73 100644 --- a/src/json.rs +++ b/src/json.rs @@ -5,9 +5,12 @@ use rjson::{Array, Null, Object, Value}; #[cfg(test)] use std::collections::BTreeMap; +#[derive(PartialEq)] pub enum JsonValue { Null, - Number(f64), + F64(f64), + I64(i64), + U64(u64), Bool(bool), String(String), Array(Vec), @@ -30,9 +33,8 @@ pub enum JsonError { #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] pub enum JsonOutOfRangeError { - U8, - U64, - U128, + OutOfRangeU8, + OutOfRangeU128, } #[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] @@ -59,15 +61,7 @@ impl JsonValue { pub fn u64(&self, key: &str) -> Result { match self { JsonValue::Object(o) => match o.get(key).ok_or(JsonError::MissingValue)? { - JsonValue::Number(n) => { - if n.is_sign_negative() || n.is_infinite() || n > &(u64::MAX as f64) { - Err(JsonError::OutOfRange(JsonOutOfRangeError::U64)) - } else if n.is_nan() { - Err(JsonError::InvalidU64) - } else { - Ok(*n as u64) - } - } + JsonValue::U64(n) => Ok(*n), _ => Err(JsonError::InvalidU64), }, _ => Err(JsonError::NotJsonType), @@ -96,9 +90,9 @@ impl JsonValue { #[allow(dead_code)] pub fn parse_u8(v: &JsonValue) -> Result { match v { - JsonValue::Number(n) => { - if n.is_sign_negative() || n > &(u8::MAX as f64) { - Err(JsonError::OutOfRange(JsonOutOfRangeError::U8)) + JsonValue::U64(n) => { + if *n > u8::MAX as u64 { + Err(JsonError::OutOfRange(JsonOutOfRangeError::OutOfRangeU8)) } else { Ok(*n as u8) } @@ -128,9 +122,8 @@ impl AsRef<[u8]> for JsonError { impl AsRef<[u8]> for JsonOutOfRangeError { fn as_ref(&self) -> &[u8] { match self { - Self::U8 => b"ERR_OUT_OF_RANGE_U8", - Self::U64 => b"ERR_OUT_OF_RANGE_U64", - Self::U128 => b"ERR_OUT_OF_RANGE_U128", + Self::OutOfRangeU8 => b"ERR_OUT_OF_RANGE_U8", + Self::OutOfRangeU128 => b"ERR_OUT_OF_RANGE_U128", } } } @@ -180,7 +173,19 @@ impl Value for JsonValue {} impl From for JsonValue { fn from(v: f64) -> Self { - JsonValue::Number(v) + JsonValue::F64(v) + } +} + +impl From for JsonValue { + fn from(v: i64) -> Self { + JsonValue::I64(v) + } +} + +impl From for JsonValue { + fn from(v: u64) -> Self { + JsonValue::U64(v) } } @@ -217,12 +222,14 @@ impl TryFrom<&JsonValue> for u128 { if let Ok(x) = n.parse::() { Ok(x) } else if n.parse::().is_ok() { - Err(JsonError::OutOfRange(JsonOutOfRangeError::U128)) + Err(JsonError::OutOfRange(JsonOutOfRangeError::OutOfRangeU128)) } else { Err(JsonError::InvalidU128) } } - JsonValue::Number(_) => Err(JsonError::ExpectedStringGotNumber), + JsonValue::F64(_) => Err(JsonError::ExpectedStringGotNumber), + JsonValue::I64(_) => Err(JsonError::ExpectedStringGotNumber), + JsonValue::U64(_) => Err(JsonError::ExpectedStringGotNumber), _ => Err(JsonError::InvalidU128), } } @@ -233,7 +240,9 @@ impl core::fmt::Debug for JsonValue { match *self { JsonValue::Null => f.write_str("null"), JsonValue::String(ref v) => f.write_fmt(format_args!("\"{}\"", v)), - JsonValue::Number(ref v) => f.write_fmt(format_args!("{}", v)), + JsonValue::F64(ref v) => f.write_fmt(format_args!("{}", v)), + JsonValue::I64(ref v) => f.write_fmt(format_args!("{}", v)), + JsonValue::U64(ref v) => f.write_fmt(format_args!("{}", v)), JsonValue::Bool(ref v) => f.write_fmt(format_args!("{}", v)), JsonValue::Array(ref v) => f.write_fmt(format_args!("{:?}", v)), JsonValue::Object(ref v) => f.write_fmt(format_args!("{:#?}", v)), @@ -289,6 +298,12 @@ mod tests { assert_eq!(err, JsonError::NotJsonType); } + #[test] + #[should_panic(expected = "overflow")] + fn test_json_type_u64_with_u128_value() { + let _ = parse_json(format!(r#"{{"foo": {} }}"#, u128::MAX).as_bytes()); + } + #[test] fn test_json_type_u64() { let json = parse_json(r#"{"foo": 123}"#.as_bytes()).unwrap(); @@ -300,17 +315,12 @@ mod tests { assert_eq!(val, u64::MAX); let json = parse_json(r#"{"foo": 12.99}"#.as_bytes()).unwrap(); - // TODO [#176]: should fail since it is not a `u64` - let val = json.u64("foo").ok().unwrap(); - assert_eq!(val, 12); - - let json = parse_json(format!(r#"{{"foo": {} }}"#, u128::MAX).as_bytes()).unwrap(); let err = json.u64("foo").unwrap_err(); - assert_eq!(err, JsonError::OutOfRange(JsonOutOfRangeError::U64)); + assert_eq!(err, JsonError::InvalidU64); let json = parse_json(r#"{"foo": -123}"#.as_bytes()).unwrap(); let err = json.u64("foo").unwrap_err(); - assert_eq!(err, JsonError::OutOfRange(JsonOutOfRangeError::U64)); + assert_eq!(err, JsonError::InvalidU64); let json = parse_json(r#"{"foo": "abcd"}"#.as_bytes()).unwrap(); let err = json.u64("foo").unwrap_err(); @@ -349,7 +359,10 @@ mod tests { let json = parse_json(r#"{"foo": "-123"}"#.as_bytes()).unwrap(); let err = json.u128("foo").unwrap_err(); - assert_eq!(err, JsonError::OutOfRange(JsonOutOfRangeError::U128)); + assert_eq!( + err, + JsonError::OutOfRange(JsonOutOfRangeError::OutOfRangeU128) + ); let json = parse_json(r#"{"foo": 123}"#.as_bytes()).unwrap(); let err = json.u128("foo").unwrap_err(); @@ -437,17 +450,20 @@ mod tests { #[test] fn test_json_type_u8() { - let json = JsonValue::from(123f64); + let json = JsonValue::from(123_u64); let val = JsonValue::parse_u8(&json).ok().unwrap(); assert_eq!(val, 123); - let json = JsonValue::from(-1f64); + let json = JsonValue::from(-1_i64); let err = JsonValue::parse_u8(&json).unwrap_err(); - assert_eq!(err, JsonError::OutOfRange(JsonOutOfRangeError::U8)); + assert_eq!(err, JsonError::InvalidU8); - let json = JsonValue::from(256f64); + let json = JsonValue::from(256_u64); let err = JsonValue::parse_u8(&json).unwrap_err(); - assert_eq!(err, JsonError::OutOfRange(JsonOutOfRangeError::U8)); + assert_eq!( + err, + JsonError::OutOfRange(JsonOutOfRangeError::OutOfRangeU8) + ); let json = JsonValue::from("abcd".to_string()); let err = JsonValue::parse_u8(&json).unwrap_err(); diff --git a/src/lib.rs b/src/lib.rs index 3f2843730..f6cdba375 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,13 +77,15 @@ mod contract { use borsh::{BorshDeserialize, BorshSerialize}; use crate::connector::EthConnectorContract; - use crate::engine::{Engine, EngineState}; + use crate::engine::{Engine, EngineState, GasPaymentError}; + use crate::fungible_token::FungibleTokenMetadata; #[cfg(feature = "evm_bully")] use crate::parameters::{BeginBlockArgs, BeginChainArgs}; use crate::parameters::{ DeployErc20TokenArgs, ExpectUtf8, FunctionCallArgs, GetErc20FromNep141CallArgs, GetStorageAtArgs, InitCallArgs, IsUsedProofCallArgs, NEP141FtOnTransferArgs, NewCallArgs, - PauseEthConnectorCallArgs, SetContractDataCallArgs, TransferCallCallArgs, ViewCallArgs, + PauseEthConnectorCallArgs, SetContractDataCallArgs, SubmitResult, TransactionStatus, + TransferCallCallArgs, ViewCallArgs, }; use crate::json::parse_json; @@ -180,8 +182,7 @@ mod contract { /// code. #[no_mangle] pub extern "C" fn state_migration() { - // This function is purposely left empty because we do not have any state migration - // to do. + // TODO: currently we don't have migrations } /// @@ -241,12 +242,35 @@ mod contract { match signed_transaction.intrinsic_gas(crate::engine::CONFIG) { None => sdk::panic_utf8(GAS_OVERFLOW.as_bytes()), Some(intrinsic_gas) => { - if signed_transaction.gas_limit() < &intrinsic_gas.into() { + if signed_transaction.gas_limit() < intrinsic_gas.into() { sdk::panic_utf8(b"ERR_INTRINSIC_GAS") } } } + // Pay for gas + let gas_price = signed_transaction.gas_price(); + let prepaid_amount = + match Engine::charge_gas_limit(&sender, signed_transaction.gas_limit(), gas_price) { + Ok(amount) => amount, + // If the account does not have enough funds to cover the gas cost then we still + // must increment the nonce to prevent the transaction from being replayed in the + // future when the state may have changed such that it could pass. + Err(GasPaymentError::OutOfFund) => { + Engine::increment_nonce(&sender); + let result = SubmitResult { + status: TransactionStatus::OutOfFund, + gas_used: 0, + logs: crate::prelude::Vec::new(), + }; + sdk::return_output(&result.try_to_vec().unwrap()); + return; + } + // If an overflow happens then the transaction is statically invalid + // (i.e. validity does not depend on state), so we do not need to increment the nonce. + Err(err) => sdk::panic_utf8(err.as_ref()), + }; + // Figure out what kind of a transaction this is, and execute it: let mut engine = Engine::new_with_state(state, sender); let (value, gas_limit, data, maybe_receiver, access_list) = @@ -272,6 +296,17 @@ mod contract { Engine::deploy_code(&mut engine, sender, value, data, gas_limit, access_list) // TODO: charge for storage }; + + // Give refund + let relayer = predecessor_address(); + let gas_used = match &result { + Ok(submit_result) => submit_result.gas_used, + Err(engine_err) => engine_err.gas_used, + }; + Engine::refund_unused_gas(&sender, &relayer, prepaid_amount, gas_used, gas_price) + .sdk_unwrap(); + + // return result to user result .map(|res| res.try_to_vec().sdk_expect("ERR_SERIALIZE")) .sdk_process(); @@ -356,21 +391,24 @@ mod contract { ethabi::Token::Address(current_address()), ]); - Engine::deploy_code_with_input( + let address = match Engine::deploy_code_with_input( &mut engine, (&[erc20_contract, deploy_args.as_slice()].concat()).to_vec(), - ) - .map(|res| { - let address = H160(res.result.as_slice().try_into().unwrap()); - crate::log!( - crate::prelude::format!("Deployed ERC-20 in Aurora at: {:#?}", address).as_str() - ); - engine - .register_token(address.as_bytes(), &args.nep141.as_bytes()) - .sdk_unwrap(); - res.result.try_to_vec().sdk_expect("ERR_SERIALIZE") - }) - .sdk_process(); + ) { + Ok(result) => match result.status { + TransactionStatus::Succeed(ret) => H160(ret.as_slice().try_into().unwrap()), + other => sdk::panic_utf8(other.as_ref()), + }, + Err(e) => sdk::panic_utf8(e.as_ref()), + }; + + crate::log!( + crate::prelude::format!("Deployed ERC-20 in Aurora at: {:#?}", address).as_str() + ); + engine + .register_token(address.as_bytes(), args.nep141.as_bytes()) + .sdk_unwrap(); + sdk::return_output(&address.as_bytes().try_to_vec().sdk_expect("ERR_SERIALIZE")); // TODO: charge for storage } @@ -583,7 +621,7 @@ mod contract { .sdk_expect("ERR_ARG_PARSE"); sdk::return_output( - Engine::get_erc20_from_nep141(&args.nep141.as_bytes()) + Engine::get_erc20_from_nep141(args.nep141.as_bytes()) .sdk_unwrap() .as_slice(), ); @@ -599,6 +637,51 @@ mod contract { ); } + #[no_mangle] + pub extern "C" fn ft_metadata() { + let metadata: FungibleTokenMetadata = + EthConnectorContract::get_metadata().unwrap_or_default(); + let json_data = crate::json::JsonValue::from(metadata); + sdk::return_output(json_data.to_string().as_bytes()) + } + + /// Due to the design change to stop minting and burning bridged ETH tokens + /// (see https://github.com/aurora-is-near/aurora-engine/pull/133 ), + /// there is currently an incorrect number of tokens in the Aurora NEP-141 token + /// account. This issue only impacts testnet because at the time the design was changed + /// the eth-connector had not been deployed to mainnet. The purpose of this function is + /// to mint the correct number of tokens in order to make up for the ones that were burned + /// before the design change in #133. + #[cfg(feature = "testnet")] + #[no_mangle] + pub extern "C" fn balance_evm_and_nep_141() { + use crate::precompiles::native::{ExitToEthereum, ExitToNear}; + use crate::prelude::String; + + let mut connector = EthConnectorContract::get_instance(); + let aurora_account = unsafe { String::from_utf8_unchecked(sdk::current_account_id()) }; + let aurora_nep_141_balance = connector.ft.ft_balance_of(&aurora_account); + let total_evm_balance = connector.ft.ft_total_eth_supply_on_aurora(); + let exit_to_near_balance = Engine::get_balance(&ExitToNear::ADDRESS).raw().low_u128(); + let exit_to_eth_balance = Engine::get_balance(&ExitToEthereum::ADDRESS) + .raw() + .low_u128(); + + // ETH sent to the exit precompiles is no longer accessible (and would have already been + // transferred from the Aurora account in the NEP-141 contract). + let available_evm_balance = total_evm_balance - exit_to_eth_balance - exit_to_near_balance; + + // After #133 it should be true that `aurora_nep_141_balance == available_evm_balance`. + // If it is not then we mint tokens to make this the case. + if aurora_nep_141_balance < available_evm_balance { + let missing_balance = available_evm_balance - aurora_nep_141_balance; + connector + .ft + .internal_deposit_eth_to_near(&aurora_account, missing_balance); + connector.save_ft_contract(); + } + } + #[cfg(feature = "integration-test")] #[no_mangle] pub extern "C" fn verify_log_entry() { @@ -607,6 +690,27 @@ mod contract { sdk::return_output(&data[..]); } + /// Function used to create accounts for tests + #[cfg(feature = "integration-test")] + #[no_mangle] + pub extern "C" fn mint_account() { + use evm::backend::ApplyBackend; + + let args: ([u8; 20], u64, u64) = sdk::read_input_borsh().sdk_expect("ERR_ARGS"); + let address = Address(args.0); + let nonce = U256::from(args.1); + let balance = U256::from(args.2); + let mut engine = Engine::new(address).sdk_unwrap(); + let state_change = evm::backend::Apply::Modify { + address, + basic: evm::backend::Basic { balance, nonce }, + code: None, + storage: core::iter::empty(), + reset_storage: false, + }; + engine.apply(core::iter::once(state_change), core::iter::empty(), false); + } + /// /// Utility methods. /// diff --git a/src/parameters.rs b/src/parameters.rs index 58cd1c730..214d3d541 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -1,5 +1,6 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use crate::fungible_token::FungibleTokenMetadata; use crate::prelude::{String, Vec}; use crate::types::{AccountId, Balance, RawAddress, RawH256, RawU256}; use crate::{ @@ -61,13 +62,53 @@ impl From for ResultLog { } } +/// The status of a transaction. +#[derive(Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +pub enum TransactionStatus { + Succeed(Vec), + Revert(Vec), + OutOfGas, + OutOfFund, + OutOfOffset, + CallTooDeep, +} + +impl TransactionStatus { + pub fn is_ok(&self) -> bool { + matches!(*self, TransactionStatus::Succeed(_)) + } + + pub fn is_revert(&self) -> bool { + matches!(*self, TransactionStatus::Revert(_)) + } + + pub fn is_fail(&self) -> bool { + *self == TransactionStatus::OutOfGas + || *self == TransactionStatus::OutOfFund + || *self == TransactionStatus::OutOfOffset + || *self == TransactionStatus::CallTooDeep + } +} + +impl AsRef<[u8]> for TransactionStatus { + fn as_ref(&self) -> &[u8] { + match self { + Self::Succeed(_) => b"SUCCESS", + Self::Revert(_) => b"ERR_REVERT", + Self::OutOfFund => b"ERR_OUT_OF_FUNDS", + Self::OutOfGas => b"ERR_OUT_OF_GAS", + Self::OutOfOffset => b"ERR_OUT_OF_OFFSET", + Self::CallTooDeep => b"ERR_CALL_TOO_DEEP", + } + } +} + /// Borsh-encoded parameters for the `call`, `call_with_args`, `deploy_code`, /// and `deploy_with_input` methods. #[derive(Debug, BorshSerialize, BorshDeserialize)] pub struct SubmitResult { - pub status: bool, + pub status: TransactionStatus, pub gas_used: u64, - pub result: Vec, pub logs: Vec, } @@ -226,7 +267,7 @@ pub struct StorageBalance { impl StorageBalance { pub fn to_json_bytes(&self) -> Vec { crate::prelude::format!( - "{{\"total\": \"{}\", \"available\": \"{}\",}}", + "{{\"total\": \"{}\", \"available\": \"{}\"}}", self.total.to_string(), self.available.to_string() ) @@ -276,6 +317,7 @@ pub struct FinishDepositEthCallArgs { pub struct InitCallArgs { pub prover_account: AccountId, pub eth_custodian_address: AccountId, + pub metadata: FungibleTokenMetadata, } /// Eth-connector Set contract data call args diff --git a/src/precompiles/hash.rs b/src/precompiles/hash.rs index 87e99256a..ce9562858 100644 --- a/src/precompiles/hash.rs +++ b/src/precompiles/hash.rs @@ -87,7 +87,7 @@ pub struct RIPEMD160; impl RIPEMD160 { pub(super) const ADDRESS: Address = super::make_address(0, 3); - #[cfg(not(feature = "testnet"))] + #[cfg(not(feature = "contract"))] fn internal_impl(input: &[u8]) -> [u8; 20] { use ripemd160::Digest; let hash = ripemd160::Ripemd160::digest(input); @@ -119,9 +119,9 @@ impl Precompile for RIPEMD160 { if cost > target_gas { Err(ExitError::OutOfGas) } else { - #[cfg(not(feature = "testnet"))] + #[cfg(not(feature = "contract"))] let hash = Self::internal_impl(input); - #[cfg(feature = "testnet")] + #[cfg(feature = "contract")] let hash = crate::sdk::ripemd160(input); // The result needs to be padded with leading zeros because it is only 20 bytes, but // the evm works with 32-byte words. diff --git a/src/precompiles/mod.rs b/src/precompiles/mod.rs index 88423a9ac..a8b9856a9 100644 --- a/src/precompiles/mod.rs +++ b/src/precompiles/mod.rs @@ -23,7 +23,7 @@ mod hash; mod identity; mod modexp; #[cfg_attr(not(feature = "contract"), allow(dead_code))] -mod native; +pub(crate) mod native; mod secp256k1; #[derive(Debug)] diff --git a/src/precompiles/native.rs b/src/precompiles/native.rs index 14a9df9b1..673e2960f 100644 --- a/src/precompiles/native.rs +++ b/src/precompiles/native.rs @@ -46,7 +46,7 @@ impl ExitToNear { /// /// Address: `0xe9217bc70b7ed1f598ddd3199e80b093fa71124f` /// This address is computed as: `&keccak("exitToNear")[12..]` - pub(super) const ADDRESS: Address = + pub(crate) const ADDRESS: Address = super::make_address(0xe9217bc7, 0x0b7ed1f598ddd3199e80b093fa71124f); } @@ -188,7 +188,7 @@ impl ExitToEthereum { /// /// Address: `0xb0bd02f6a392af548bdf1cfaee5dfa0eefcc8eab` /// This address is computed as: `&keccak("exitToEthereum")[12..]` - pub(super) const ADDRESS: Address = + pub(crate) const ADDRESS: Address = super::make_address(0xb0bd02f6, 0xa392af548bdf1cfaee5dfa0eefcc8eab); } diff --git a/src/precompiles/secp256k1.rs b/src/precompiles/secp256k1.rs index 849d4fd28..337bf13b8 100644 --- a/src/precompiles/secp256k1.rs +++ b/src/precompiles/secp256k1.rs @@ -19,15 +19,15 @@ mod consts { pub(crate) fn ecrecover(hash: H256, signature: &[u8]) -> Result { assert_eq!(signature.len(), 65); - #[cfg(feature = "testnet")] + #[cfg(feature = "contract")] return crate::sdk::ecrecover(hash, signature) .map_err(|e| ExitError::Other(Borrowed(e.as_str()))); - #[cfg(not(feature = "testnet"))] + #[cfg(not(feature = "contract"))] internal_impl(hash, signature) } -#[cfg(not(feature = "testnet"))] +#[cfg(not(feature = "contract"))] fn internal_impl(hash: H256, signature: &[u8]) -> Result { use sha3::Digest; diff --git a/src/sdk.rs b/src/sdk.rs index df8d5cad7..c7b484ce1 100644 --- a/src/sdk.rs +++ b/src/sdk.rs @@ -5,11 +5,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; const READ_STORAGE_REGISTER_ID: u64 = 0; const INPUT_REGISTER_ID: u64 = 0; -#[cfg(feature = "testnet")] const ECRECOVER_MESSAGE_SIZE: u64 = 32; -#[cfg(feature = "testnet")] const ECRECOVER_SIGNATURE_LENGTH: u64 = 64; -#[cfg(feature = "testnet")] const ECRECOVER_MALLEABILITY_FLAG: u64 = 1; /// Register used to record evicted values from the storage. @@ -51,9 +48,7 @@ mod exports { fn random_seed(register_id: u64); pub(crate) fn sha256(value_len: u64, value_ptr: u64, register_id: u64); pub(crate) fn keccak256(value_len: u64, value_ptr: u64, register_id: u64); - #[cfg(feature = "testnet")] pub(crate) fn ripemd160(value_len: u64, value_ptr: u64, register_id: u64); - #[cfg(feature = "testnet")] pub(crate) fn ecrecover( hash_len: u64, hash_ptr: u64, @@ -315,7 +310,9 @@ pub fn remove_storage_with_result(key: &[u8]) -> Option> { #[allow(dead_code)] pub fn block_timestamp() -> u64 { - unsafe { exports::block_timestamp() } + // NEAR timestamp is in nanoseconds + let timestamp_ns = unsafe { exports::block_timestamp() }; + timestamp_ns / 1000 // convert to milliseconds for Ethereum compatibility } pub fn block_index() -> u64 { @@ -391,7 +388,6 @@ pub fn keccak(input: &[u8]) -> H256 { } /// Calls environment ripemd160 on given input. -#[cfg(feature = "testnet")] pub fn ripemd160(input: &[u8]) -> [u8; 20] { unsafe { const REGISTER_ID: u64 = 1; @@ -403,7 +399,6 @@ pub fn ripemd160(input: &[u8]) -> [u8; 20] { } /// Recover address from message hash and signature. -#[cfg(feature = "testnet")] pub fn ecrecover(hash: H256, signature: &[u8]) -> Result { unsafe { let hash_ptr = hash.as_ptr() as u64; diff --git a/src/storage.rs b/src/storage.rs index 52af685e9..6d8132fbf 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -30,6 +30,7 @@ pub enum EthConnectorStorageId { UsedEvent = 0x2, PausedMask = 0x3, StatisticsAuroraAccountsCounter = 0x4, + FungibleTokenMetadata = 0x5, } /// We can't use const generic over Enum, but we can do it over integral type diff --git a/src/test_utils/exit_precompile.rs b/src/test_utils/exit_precompile.rs index 2912fbca4..11ea55bc5 100644 --- a/src/test_utils/exit_precompile.rs +++ b/src/test_utils/exit_precompile.rs @@ -1,6 +1,6 @@ use crate::parameters::SubmitResult; use crate::prelude::{Address, U256}; -use crate::test_utils::{solidity, AuroraRunner, Signer}; +use crate::test_utils::{self, solidity, AuroraRunner, Signer}; use crate::transaction::LegacyEthTransaction; pub(crate) struct TesterConstructor(pub solidity::ContractConstructor); @@ -78,8 +78,9 @@ impl Tester { let result = runner.submit_transaction(&signer.secret_key, tx).unwrap(); - if result.status { - Ok(ethabi::decode(output_type, result.result.as_slice()).unwrap()) + if result.status.is_ok() { + let result = test_utils::unwrap_success(result); + Ok(ethabi::decode(output_type, result.as_slice()).unwrap()) } else { Err(result) } diff --git a/src/test_utils/mod.rs b/src/test_utils/mod.rs index 3ab166990..d70112403 100644 --- a/src/test_utils/mod.rs +++ b/src/test_utils/mod.rs @@ -12,8 +12,8 @@ use primitive_types::U256; use rlp::RlpStream; use secp256k1::{self, Message, PublicKey, SecretKey}; -use crate::fungible_token::FungibleToken; -use crate::parameters::{InitCallArgs, NewCallArgs, SubmitResult}; +use crate::fungible_token::{FungibleToken, FungibleTokenMetadata}; +use crate::parameters::{InitCallArgs, NewCallArgs, SubmitResult, TransactionStatus}; use crate::prelude::Address; use crate::storage; use crate::test_utils::solidity::{ContractConstructor, DeployedContract}; @@ -29,7 +29,7 @@ use crate::types::AccountId; not(any(feature = "testnet", feature = "betanet")) ))] lazy_static_include::lazy_static_include_bytes! { - EVM_WASM_BYTES => "mainnet-release.wasm" + EVM_WASM_BYTES => "mainnet-test.wasm" } #[cfg(all( @@ -37,7 +37,7 @@ lazy_static_include::lazy_static_include_bytes! { not(any(feature = "mainnet", feature = "betanet")) ))] lazy_static_include::lazy_static_include_bytes! { - EVM_WASM_BYTES => "testnet-release.wasm" + EVM_WASM_BYTES => "testnet-test.wasm" } #[cfg(all( @@ -45,7 +45,7 @@ lazy_static_include::lazy_static_include_bytes! { not(any(feature = "mainnet", feature = "testnet")) ))] lazy_static_include::lazy_static_include_bytes! { - EVM_WASM_BYTES => "betanet-release.wasm" + EVM_WASM_BYTES => "betanet-test.wasm" } // TODO(Copied from #84): Make sure that there is only one Signer after both PR are merged. @@ -281,7 +281,7 @@ impl AuroraRunner { assert!(maybe_err.is_none()); let submit_result = SubmitResult::try_from_slice(&output.unwrap().return_data.as_value().unwrap()).unwrap(); - let address = Address::from_slice(&submit_result.result); + let address = Address::from_slice(&unwrap_success(submit_result)); let contract_constructor: ContractConstructor = contract_constructor.into(); DeployedContract { abi: contract_constructor.abi, @@ -381,6 +381,7 @@ pub(crate) fn deploy_evm() -> AuroraRunner { let args = InitCallArgs { prover_account: "prover.near".to_string(), eth_custodian_address: "d045f7e19B2488924B97F9c145b5E51D0D895A65".to_string(), + metadata: FungibleTokenMetadata::default(), }; let (_, maybe_error) = runner.call( "new_eth_connector", @@ -512,3 +513,25 @@ pub(crate) fn address_from_hex(address: &str) -> Address { Address::from_slice(&bytes) } + +pub fn unwrap_success(result: SubmitResult) -> Vec { + match result.status { + TransactionStatus::Succeed(ret) => ret, + other => panic!("Unexpected status: {:?}", other), + } +} + +pub fn unwrap_revert(result: SubmitResult) -> Vec { + match result.status { + TransactionStatus::Revert(ret) => ret, + other => panic!("Unexpected status: {:?}", other), + } +} + +pub fn panic_on_fail(status: TransactionStatus) { + match status { + TransactionStatus::Succeed(_) => (), + TransactionStatus::Revert(message) => panic!("{}", String::from_utf8_lossy(&message)), + other => panic!("{}", String::from_utf8_lossy(other.as_ref())), + } +} diff --git a/src/test_utils/self_destruct.rs b/src/test_utils/self_destruct.rs index d8c268778..0f3745214 100644 --- a/src/test_utils/self_destruct.rs +++ b/src/test_utils/self_destruct.rs @@ -1,6 +1,6 @@ use crate::parameters::FunctionCallArgs; use crate::prelude::Address; -use crate::test_utils::{solidity, AuroraRunner, Signer}; +use crate::test_utils::{self, solidity, AuroraRunner, Signer}; use crate::transaction::LegacyEthTransaction; use borsh::BorshSerialize; use primitive_types::U256; @@ -73,8 +73,9 @@ impl SelfDestructFactory { }; let result = runner.submit_transaction(&signer.secret_key, tx).unwrap(); + let result = test_utils::unwrap_success(result); - Address::from_slice(&result.result[12..]) + Address::from_slice(&result[12..]) } } @@ -111,11 +112,10 @@ impl SelfDestruct { }; let result = runner.submit_transaction(&signer.secret_key, tx).unwrap(); + let result = test_utils::unwrap_success(result); - if result.result.len() == 32 { - Some(u128::from_be_bytes( - result.result[16..32].try_into().unwrap(), - )) + if result.len() == 32 { + Some(u128::from_be_bytes(result[16..32].try_into().unwrap())) } else { None } diff --git a/src/test_utils/solidity.rs b/src/test_utils/solidity.rs index 37f4c7a7b..6a9a2bc69 100644 --- a/src/test_utils/solidity.rs +++ b/src/test_utils/solidity.rs @@ -1,4 +1,5 @@ -use crate::prelude::Address; +use crate::prelude::{Address, U256}; +use crate::transaction::LegacyEthTransaction; use near_sdk::serde_json; use serde::Deserialize; use std::fs; @@ -73,6 +74,42 @@ impl ContractConstructor { address, } } + + pub fn deploy_without_args(&self, nonce: U256) -> LegacyEthTransaction { + let data = self + .abi + .constructor() + .unwrap() + .encode_input(self.code.clone(), &[]) + .unwrap(); + LegacyEthTransaction { + nonce, + gas_price: Default::default(), + gas: u64::MAX.into(), + to: None, + value: Default::default(), + data, + } + } +} + +impl DeployedContract { + pub fn call_method_without_args(&self, method_name: &str, nonce: U256) -> LegacyEthTransaction { + let data = self + .abi + .function(method_name) + .unwrap() + .encode_input(&[]) + .unwrap(); + LegacyEthTransaction { + nonce, + gas_price: Default::default(), + gas: u64::MAX.into(), + to: Some(self.address), + value: Default::default(), + data, + } + } } /// Compiles a solidity contract. `source_path` gives the directory containing all solidity diff --git a/src/test_utils/standard_precompiles.rs b/src/test_utils/standard_precompiles.rs index 362d285d5..56db06814 100644 --- a/src/test_utils/standard_precompiles.rs +++ b/src/test_utils/standard_precompiles.rs @@ -24,21 +24,7 @@ impl PrecompilesConstructor { } pub fn deploy(&self, nonce: U256) -> LegacyEthTransaction { - let data = self - .0 - .abi - .constructor() - .unwrap() - .encode_input(self.0.code.clone(), &[]) - .unwrap(); - LegacyEthTransaction { - nonce, - gas_price: Default::default(), - gas: u64::MAX.into(), - to: None, - value: Default::default(), - data, - } + self.0.deploy_without_args(nonce) } fn solidity_artifacts_path() -> PathBuf { @@ -52,21 +38,7 @@ impl PrecompilesConstructor { impl PrecompilesContract { pub fn call_method(&self, method_name: &str, nonce: U256) -> LegacyEthTransaction { - let data = self - .0 - .abi - .function(method_name) - .unwrap() - .encode_input(&[]) - .unwrap(); - LegacyEthTransaction { - nonce, - gas_price: Default::default(), - gas: u64::MAX.into(), - to: Some(self.0.address), - value: Default::default(), - data, - } + self.0.call_method_without_args(method_name, nonce) } pub fn all_method_names() -> &'static [&'static str] { diff --git a/src/tests/erc20.rs b/src/tests/erc20.rs index 95dd88bc2..13f953a68 100644 --- a/src/tests/erc20.rs +++ b/src/tests/erc20.rs @@ -1,14 +1,15 @@ +use crate::parameters::TransactionStatus; use crate::prelude::{Address, U256}; use crate::test_utils::{ self, erc20::{ERC20Constructor, ERC20}, Signer, }; -use crate::types::Wei; +use crate::types::{self, Wei}; use bstr::ByteSlice; use secp256k1::SecretKey; -const INITIAL_BALANCE: u64 = 1000; +const INITIAL_BALANCE: u64 = 1_000_000; const INITIAL_NONCE: u64 = 0; const TRANSFER_AMOUNT: u64 = 67; @@ -59,19 +60,27 @@ fn erc20_mint_out_of_gas() { assert!(error_message.contains("ERR_INTRINSIC_GAS")); // not enough gas to complete transaction - mint_tx.gas = U256::from(67_000); + const GAS_LIMIT: u64 = 67_000; + const GAS_PRICE: u64 = 10; + mint_tx.gas = U256::from(GAS_LIMIT); + mint_tx.gas_price = U256::from(GAS_PRICE); // also set non-zero gas price to check gas still charged. let outcome = runner.submit_transaction(&source_account.secret_key, mint_tx); - let error = outcome.unwrap_err(); - let error_message = format!("{:?}", error); - assert!(error_message.contains("ERR_OUT_OF_GAS")); + let error = outcome.unwrap(); + assert_eq!(error.status, TransactionStatus::OutOfGas); // Validate post-state test_utils::validate_address_balance_and_nonce( &runner, test_utils::address_from_secret_key(&source_account.secret_key), - Wei::new_u64(INITIAL_BALANCE), + Wei::new_u64(INITIAL_BALANCE - GAS_LIMIT * GAS_PRICE), (INITIAL_NONCE + 3).into(), ); + test_utils::validate_address_balance_and_nonce( + &runner, + types::near_account_to_evm_address(runner.context.predecessor_account_id.as_bytes()), + Wei::new_u64(GAS_LIMIT * GAS_PRICE), + U256::zero(), + ); } #[test] @@ -100,7 +109,7 @@ fn erc20_transfer_success() { contract.transfer(dest_address, TRANSFER_AMOUNT.into(), nonce) }) .unwrap(); - assert!(outcome.status); + assert!(outcome.status.is_ok()); // Validate post-state assert_eq!( @@ -139,8 +148,7 @@ fn erc20_transfer_insufficient_balance() { contract.transfer(dest_address, (2 * INITIAL_BALANCE).into(), nonce) }) .unwrap(); - assert!(!outcome.status); // status == false means execution error - let message = parse_erc20_error_message(&outcome.result); + let message = parse_erc20_error_message(&test_utils::unwrap_revert(outcome)); assert_eq!(&message, "&ERC20: transfer amount exceeds balance"); // Validate post-state @@ -183,9 +191,8 @@ fn deploy_erc_20_out_of_gas() { // not enough gas to complete transaction deploy_transaction.gas = U256::from(3_200_000); let outcome = runner.submit_transaction(&source_account, deploy_transaction); - let error = outcome.unwrap_err(); - let error_message = format!("{:?}", error); - assert!(error_message.contains("ERR_OUT_OF_GAS")); + let error = outcome.unwrap(); + assert_eq!(error.status, TransactionStatus::OutOfGas); // Validate post-state test_utils::validate_address_balance_and_nonce( @@ -202,9 +209,11 @@ fn get_address_erc20_balance( address: Address, contract: &ERC20, ) -> U256 { - let outcome = runner.submit_with_signer(signer, |nonce| contract.balance_of(address, nonce)); - assert!(outcome.is_ok()); - U256::from_big_endian(&outcome.unwrap().result) + let outcome = runner + .submit_with_signer(signer, |nonce| contract.balance_of(address, nonce)) + .unwrap(); + let output = test_utils::unwrap_success(outcome); + U256::from_big_endian(&output) } fn parse_erc20_error_message(result: &[u8]) -> String { diff --git a/src/tests/erc20_connector.rs b/src/tests/erc20_connector.rs index cc727dd08..79d839388 100644 --- a/src/tests/erc20_connector.rs +++ b/src/tests/erc20_connector.rs @@ -139,7 +139,8 @@ impl test_utils::AuroraRunner { let input = build_input("balanceOf(address)", &[Token::Address(target.into())]); let result = self.evm_call(token, input, origin); result.check_ok(); - U256::from_big_endian(result.submit_result().result.as_slice()) + let output = test_utils::unwrap_success(result.submit_result()); + U256::from_big_endian(output.as_slice()) } pub fn mint( diff --git a/src/tests/eth_connector.rs b/src/tests/eth_connector.rs index 1f8994d60..2c65e70d5 100644 --- a/src/tests/eth_connector.rs +++ b/src/tests/eth_connector.rs @@ -2,6 +2,7 @@ use crate::admin_controlled::{PausedMask, ERR_PAUSED}; use crate::connector::{ ERR_NOT_ENOUGH_BALANCE_FOR_FEE, PAUSE_DEPOSIT, PAUSE_WITHDRAW, UNPAUSE_ALL, }; +use crate::fungible_token::FungibleTokenMetadata; use crate::parameters::{ InitCallArgs, NewCallArgs, RegisterRelayerCallArgs, WithdrawCallArgs, WithdrawResult, }; @@ -71,6 +72,7 @@ fn init_contract( &InitCallArgs { prover_account: PROVER_ACCOUNT.into(), eth_custodian_address: custodian_address.into(), + metadata: FungibleTokenMetadata::default(), } .try_to_vec() .unwrap(), diff --git a/src/tests/res/blockhash.sol b/src/tests/res/blockhash.sol new file mode 100644 index 000000000..acf6bc115 --- /dev/null +++ b/src/tests/res/blockhash.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +contract BlockHash { + constructor() payable {} + + function test() public view { + require( + blockhash(0) == hex"ec035c7409243a343a8fd798077fb0a5f879cc32c9cd31fd07baa2292e4d3d7c", + "Bad block hash" + ); + } +} diff --git a/src/tests/sanity.rs b/src/tests/sanity.rs index b0faa2e74..44694a672 100644 --- a/src/tests/sanity.rs +++ b/src/tests/sanity.rs @@ -1,11 +1,17 @@ -use crate::prelude::Address; +use crate::fungible_token::FungibleTokenMetadata; +use crate::parameters::{SubmitResult, TransactionStatus}; +use crate::prelude::{Address, U256}; use crate::test_utils; -use crate::types::{Wei, ERC20_MINT_SELECTOR}; +use crate::tests::state_migration; +use crate::types::{self, Wei, ERC20_MINT_SELECTOR}; +use borsh::BorshSerialize; use secp256k1::SecretKey; +use std::path::{Path, PathBuf}; -const INITIAL_BALANCE: Wei = Wei::new_u64(1000); +const INITIAL_BALANCE: Wei = Wei::new_u64(1_000_000); const INITIAL_NONCE: u64 = 0; const TRANSFER_AMOUNT: Wei = Wei::new_u64(123); +const GAS_PRICE: u64 = 10; /// Tests we can transfer Eth from one account to another and that the balances are correctly /// updated. @@ -62,14 +68,13 @@ fn test_eth_transfer_insufficient_balance() { test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); // attempt transfer - let err = runner + let result = runner .submit_with_signer(&mut source_account, |nonce| { // try to transfer more than we have test_utils::transfer(dest_address, INITIAL_BALANCE + INITIAL_BALANCE, nonce) }) - .unwrap_err(); - let error_message = format!("{:?}", err); - assert!(error_message.contains("ERR_OUT_OF_FUND")); + .unwrap(); + assert_eq!(result.status, TransactionStatus::OutOfFund); // validate post-state test_utils::validate_address_balance_and_nonce( @@ -153,6 +158,100 @@ fn test_eth_transfer_not_enough_gas() { test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); } +#[test] +fn test_transfer_charging_gas_success() { + let (mut runner, mut source_account, dest_address) = initialize_transfer(); + let source_address = test_utils::address_from_secret_key(&source_account.secret_key); + let transaction = |nonce| { + let mut tx = test_utils::transfer(dest_address, TRANSFER_AMOUNT, nonce); + tx.gas = 30_000.into(); + tx.gas_price = GAS_PRICE.into(); + tx + }; + + // validate pre-state + test_utils::validate_address_balance_and_nonce( + &runner, + source_address, + INITIAL_BALANCE, + INITIAL_NONCE.into(), + ); + test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); + + // do transfer + let result = runner + .submit_with_signer(&mut source_account, transaction) + .unwrap(); + let spent_amount = Wei::new_u64(GAS_PRICE * result.gas_used); + let expected_source_balance = INITIAL_BALANCE - TRANSFER_AMOUNT - spent_amount; + let expected_dest_balance = TRANSFER_AMOUNT; + let expected_relayer_balance = spent_amount; + let relayer_address = + types::near_account_to_evm_address(runner.context.predecessor_account_id.as_bytes()); + + // validate post-state + test_utils::validate_address_balance_and_nonce( + &runner, + source_address, + expected_source_balance, + (INITIAL_NONCE + 1).into(), + ); + test_utils::validate_address_balance_and_nonce( + &runner, + dest_address, + expected_dest_balance, + 0.into(), + ); + test_utils::validate_address_balance_and_nonce( + &runner, + relayer_address, + expected_relayer_balance, + 0.into(), + ); +} + +#[test] +fn test_eth_transfer_charging_gas_not_enough_balance() { + let (mut runner, mut source_account, dest_address) = initialize_transfer(); + let source_address = test_utils::address_from_secret_key(&source_account.secret_key); + let transaction = |nonce| { + let mut tx = test_utils::transfer(dest_address, TRANSFER_AMOUNT, nonce); + // With this gas limit and price the account does not + // have enough balance to cover the gas cost + tx.gas = 3_000_000.into(); + tx.gas_price = GAS_PRICE.into(); + tx + }; + + // validate pre-state + test_utils::validate_address_balance_and_nonce( + &runner, + source_address, + INITIAL_BALANCE, + INITIAL_NONCE.into(), + ); + test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); + + // attempt transfer + let result = runner + .submit_with_signer(&mut source_account, transaction) + .unwrap(); + assert_eq!(result.status, TransactionStatus::OutOfFund); + + // validate post-state + let relayer = + types::near_account_to_evm_address(runner.context.predecessor_account_id.as_bytes()); + test_utils::validate_address_balance_and_nonce( + &runner, + source_address, + INITIAL_BALANCE, + // nonce is still incremented since the transaction was otherwise valid + (INITIAL_NONCE + 1).into(), + ); + test_utils::validate_address_balance_and_nonce(&runner, dest_address, Wei::zero(), 0.into()); + test_utils::validate_address_balance_and_nonce(&runner, relayer, Wei::zero(), 0.into()); +} + fn initialize_transfer() -> (test_utils::AuroraRunner, test_utils::Signer, Address) { // set up Aurora runner and accounts let mut runner = test_utils::deploy_evm(); @@ -178,3 +277,227 @@ fn check_selector() { hasher.update(b"mint(address,uint256)"); assert_eq!(hasher.finalize()[..4].to_vec(), ERC20_MINT_SELECTOR); } + +#[test] +fn test_block_hash() { + let runner = test_utils::AuroraRunner::default(); + let chain_id = { + let number = crate::prelude::U256::from(runner.chain_id); + crate::types::u256_to_arr(&number) + }; + let account_id = runner.aurora_account_id; + let block_hash = crate::engine::Engine::compute_block_hash(chain_id, 10, account_id.as_bytes()); + + assert_eq!( + hex::encode(block_hash.0).as_str(), + "c4a46f076b64877cbd8c5dbfd7bfbbea21a5653b79e3b6d06b6dfb5c88f1c384", + ); +} + +#[test] +fn test_block_hash_contract() { + let (mut runner, mut source_account, _) = initialize_transfer(); + let test_constructor = test_utils::solidity::ContractConstructor::compile_from_source( + ["src", "tests", "res"].iter().collect::(), + Path::new("target").join("solidity_build"), + "blockhash.sol", + "BlockHash", + ); + let nonce = source_account.use_nonce(); + let test_contract = runner.deploy_contract( + &source_account.secret_key, + |c| c.deploy_without_args(nonce.into()), + test_constructor, + ); + + let result = runner + .submit_with_signer(&mut source_account, |nonce| { + test_contract.call_method_without_args("test", nonce) + }) + .unwrap(); + + test_utils::panic_on_fail(result.status); +} + +#[test] +fn test_ft_metadata() { + let mut runner = test_utils::deploy_evm(); + + let (maybe_outcome, maybe_error) = runner.call( + "ft_metadata", + runner.context.signer_account_id.clone(), + Vec::new(), + ); + assert!(maybe_error.is_none()); + let outcome = maybe_outcome.unwrap(); + let json_value = crate::json::parse_json(&outcome.return_data.as_value().unwrap()).unwrap(); + + assert_eq!( + json_value, + crate::json::JsonValue::from(FungibleTokenMetadata::default()) + ); +} + +#[cfg(feature = "testnet-test")] +#[test] +fn test_balance_evm_and_nep_141() { + use crate::precompiles::native::{ExitToEthereum, ExitToNear}; + + let (mut runner, _, _) = initialize_transfer(); + let caller = runner.aurora_account_id.clone(); + + // Include some ETH at the exit precompiles addresses for testing purposes + runner.create_address(ExitToNear::ADDRESS, TRANSFER_AMOUNT, U256::zero()); + runner.create_address( + ExitToEthereum::ADDRESS, + TRANSFER_AMOUNT + TRANSFER_AMOUNT, + U256::zero(), + ); + + let aurora_balance = |runner: &mut test_utils::AuroraRunner| -> u128 { + let (maybe_result, maybe_err) = runner.call( + "ft_balance_of", + caller.clone(), + format!(r#"{{"account_id": "{}"}}"#, runner.aurora_account_id).into_bytes(), + ); + assert!(maybe_err.is_none()); + let result = maybe_result.unwrap(); + String::from_utf8(result.return_data.as_value().unwrap()) + .unwrap() + .parse() + .unwrap() + }; + + let evm_balance = |runner: &mut test_utils::AuroraRunner| -> u128 { + let exit_to_near_balance = runner.get_balance(ExitToNear::ADDRESS).raw().low_u128(); + let exit_to_eth_balance = runner.get_balance(ExitToEthereum::ADDRESS).raw().low_u128(); + let total_evm_balance: u128 = { + let (maybe_result, maybe_err) = + runner.call("ft_total_eth_supply_on_aurora", caller.clone(), Vec::new()); + assert!(maybe_err.is_none()); + let result = maybe_result.unwrap(); + String::from_utf8(result.return_data.as_value().unwrap()) + .unwrap() + .parse() + .unwrap() + }; + total_evm_balance - exit_to_near_balance - exit_to_eth_balance + }; + + // There is not enough in the Aurora NEP-141 account, relative to how much is in the EVM; + assert_eq!(evm_balance(&mut runner), INITIAL_BALANCE.raw().low_u128()); + assert_eq!(aurora_balance(&mut runner), 0); + + // Call method to restore balance + let (_, maybe_err) = runner.call("balance_evm_and_nep_141", caller.clone(), Vec::new()); + assert!(maybe_err.is_none()); + + // Balance is restored + assert_eq!(aurora_balance(&mut runner), evm_balance(&mut runner)); + + // Calling multiple times is a no-op + let (_, maybe_err) = runner.call("balance_evm_and_nep_141", caller.clone(), Vec::new()); + assert!(maybe_err.is_none()); + assert_eq!(aurora_balance(&mut runner), evm_balance(&mut runner)); +} + +// Same as `test_eth_transfer_insufficient_balance` above, except runs through +// `near-sdk-sim` instead of `near-vm-runner`. This is important because `near-sdk-sim` +// has more production logic, in particular, state revert on contract panic. +// TODO: should be able to generalize the `call` backend of `AuroraRunner` so that this +// test does not need to be written twice. +#[test] +fn test_eth_transfer_insufficient_balance_sim() { + let (aurora, mut signer, address) = initialize_evm_sim(); + + // Run transaction which will fail (transfer more than current balance) + let nonce = signer.use_nonce(); + let tx = test_utils::transfer( + Address([1; 20]), + INITIAL_BALANCE + INITIAL_BALANCE, + nonce.into(), + ); + let signed_tx = test_utils::sign_transaction( + tx, + Some(test_utils::AuroraRunner::default().chain_id), + &signer.secret_key, + ); + let call_result = aurora.call("submit", rlp::encode(&signed_tx).as_ref()); + let result: SubmitResult = call_result.unwrap_borsh(); + assert_eq!(result.status, TransactionStatus::OutOfFund); + + // validate post-state + assert_eq!( + query_address_sim(&address, "get_nonce", &aurora), + U256::from(INITIAL_NONCE + 1), + ); + assert_eq!( + query_address_sim(&address, "get_balance", &aurora), + INITIAL_BALANCE.raw(), + ); +} + +// Same as `test_eth_transfer_charging_gas_not_enough_balance` but run through `near-sdk-sim`. +#[test] +fn test_eth_transfer_charging_gas_not_enough_balance_sim() { + let (aurora, mut signer, address) = initialize_evm_sim(); + + // Run transaction which will fail (not enough balance to cover gas) + let nonce = signer.use_nonce(); + let mut tx = test_utils::transfer(Address([1; 20]), TRANSFER_AMOUNT, nonce.into()); + tx.gas = 3_000_000.into(); + tx.gas_price = GAS_PRICE.into(); + let signed_tx = test_utils::sign_transaction( + tx, + Some(test_utils::AuroraRunner::default().chain_id), + &signer.secret_key, + ); + let call_result = aurora.call("submit", rlp::encode(&signed_tx).as_ref()); + let result: SubmitResult = call_result.unwrap_borsh(); + assert_eq!(result.status, TransactionStatus::OutOfFund); + + // validate post-state + assert_eq!( + query_address_sim(&address, "get_nonce", &aurora), + U256::from(INITIAL_NONCE + 1), + ); + assert_eq!( + query_address_sim(&address, "get_balance", &aurora), + INITIAL_BALANCE.raw(), + ); +} + +fn initialize_evm_sim() -> (state_migration::AuroraAccount, test_utils::Signer, Address) { + let aurora = state_migration::deploy_evm(); + let signer = test_utils::Signer::random(); + let address = test_utils::address_from_secret_key(&signer.secret_key); + + let args = (address.0, INITIAL_NONCE, INITIAL_BALANCE.raw().low_u64()); + aurora + .call("mint_account", &args.try_to_vec().unwrap()) + .assert_success(); + + // validate pre-state + assert_eq!( + query_address_sim(&address, "get_nonce", &aurora), + U256::from(INITIAL_NONCE), + ); + assert_eq!( + query_address_sim(&address, "get_balance", &aurora), + INITIAL_BALANCE.raw(), + ); + + (aurora, signer, address) +} + +fn query_address_sim( + address: &Address, + method: &str, + aurora: &state_migration::AuroraAccount, +) -> U256 { + let x = aurora.call(method, &address.0); + match &x.outcome().status { + near_sdk_sim::transaction::ExecutionStatus::SuccessValue(b) => U256::from_big_endian(&b), + other => panic!("Unexpected outcome: {:?}", other), + } +} diff --git a/src/tests/standard_precompiles.rs b/src/tests/standard_precompiles.rs index bf2ca483a..c6dd25f9f 100644 --- a/src/tests/standard_precompiles.rs +++ b/src/tests/standard_precompiles.rs @@ -31,8 +31,5 @@ fn standard_precompiles() { .submit_transaction(&source_account, test_all_tx) .unwrap(); - // status == false indicates failure - if !outcome.status { - panic!("{}", String::from_utf8_lossy(&outcome.result)) - } + test_utils::panic_on_fail(outcome.status); } diff --git a/src/tests/state_migration.rs b/src/tests/state_migration.rs index 297501024..fd055cd52 100644 --- a/src/tests/state_migration.rs +++ b/src/tests/state_migration.rs @@ -1,4 +1,4 @@ -use crate::parameters::NewCallArgs; +use crate::parameters::{InitCallArgs, NewCallArgs}; use crate::prelude::U256; use crate::test_utils::AuroraRunner; use crate::types; @@ -26,7 +26,7 @@ fn test_state_migration() { assert_eq!(some_numbers, [3, 1, 4, 1, 5, 9, 2]); } -fn deploy_evm() -> AuroraAccount { +pub fn deploy_evm() -> AuroraAccount { let aurora_runner = AuroraRunner::default(); let main_account = near_sdk_sim::init_simulator(None); let contract_account = main_account.deploy( @@ -34,10 +34,11 @@ fn deploy_evm() -> AuroraAccount { aurora_runner.aurora_account_id.parse().unwrap(), 5 * near_sdk_sim::STORAGE_AMOUNT, ); + let prover_account = "prover.near".to_string(); let new_args = NewCallArgs { chain_id: types::u256_to_arr(&U256::from(aurora_runner.chain_id)), owner_id: main_account.account_id.clone().into(), - bridge_prover_id: "prover.near".to_string(), + bridge_prover_id: prover_account.clone(), upgrade_delay_blocks: 1, }; main_account @@ -49,19 +50,33 @@ fn deploy_evm() -> AuroraAccount { 0, ) .assert_success(); + let init_args = InitCallArgs { + prover_account, + eth_custodian_address: "d045f7e19B2488924B97F9c145b5E51D0D895A65".to_string(), + metadata: Default::default(), + }; + contract_account + .call( + contract_account.account_id.clone(), + "new_eth_connector", + &init_args.try_to_vec().unwrap(), + near_sdk_sim::DEFAULT_GAS, + 0, + ) + .assert_success(); AuroraAccount { user: main_account, contract: contract_account, } } -struct AuroraAccount { +pub struct AuroraAccount { user: UserAccount, contract: UserAccount, } impl AuroraAccount { - fn call(&self, method: &str, args: &[u8]) -> ExecutionResult { + pub fn call(&self, method: &str, args: &[u8]) -> ExecutionResult { self.user.call( self.contract.account_id.clone(), method, diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index eeaa75ba3..18a8e378f 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -43,10 +43,17 @@ impl EthTransaction { } } - pub fn gas_limit(&self) -> &U256 { + pub fn gas_limit(&self) -> U256 { match self { - Self::Legacy(tx) => &tx.transaction.gas, - Self::AccessList(tx) => &tx.transaction_data.gas_limit, + Self::Legacy(tx) => tx.transaction.gas, + Self::AccessList(tx) => tx.transaction_data.gas_limit, + } + } + + pub fn gas_price(&self) -> U256 { + match self { + Self::Legacy(tx) => tx.transaction.gas_price, + Self::AccessList(tx) => tx.transaction_data.gas_price, } } @@ -139,12 +146,10 @@ fn rlp_extract_to(rlp: &Rlp<'_>, index: usize) -> Result, Decode } } -// TODO: need to include access_list gas cost (see https://eips.ethereum.org/EIPS/eip-2930) -// Should go in the config, which requires upstream change. fn intrinsic_gas( is_contract_creation: bool, data: &[u8], - _access_list: &[access_list::AccessTuple], + access_list: &[access_list::AccessTuple], config: &evm::Config, ) -> Option { let base_gas = if is_contract_creation { @@ -163,9 +168,21 @@ fn intrinsic_gas( .gas_transaction_non_zero_data .checked_mul(num_non_zero_bytes as u64)?; + let gas_access_list_address = config + .gas_access_list_address + .checked_mul(access_list.len() as u64)?; + let gas_access_list_storage = config.gas_access_list_storage_key.checked_mul( + access_list + .iter() + .map(|a| a.storage_keys.len() as u64) + .sum(), + )?; + base_gas .checked_add(gas_zero_bytes) .and_then(|gas| gas.checked_add(gas_non_zero_bytes)) + .and_then(|gas| gas.checked_add(gas_access_list_address)) + .and_then(|gas| gas.checked_add(gas_access_list_storage)) } fn vrs_to_arr(v: u8, r: U256, s: U256) -> [u8; 65] { diff --git a/src/types.rs b/src/types.rs index 03168b82a..008e59dbb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,8 +1,6 @@ -use crate::prelude::{self, Address, String, ToString, Vec, H256, U256}; +use crate::prelude::{self, str, Address, String, ToString, Vec, H256, U256}; #[cfg(not(feature = "contract"))] use crate::prelude::{format, vec}; - -use crate::prelude::str; use borsh::{BorshDeserialize, BorshSerialize}; use ethabi::{Event, EventParam, Hash, Log, RawLog}; @@ -12,7 +10,6 @@ use ethabi::{ParamType, Token}; #[cfg(not(feature = "contract"))] use sha3::{Digest, Keccak256}; -use crate::engine::EngineResult; use crate::log_entry::LogEntry; use crate::sdk; @@ -169,7 +166,7 @@ impl Proof { } /// Newtype to distinguish balances (denominated in Wei) from other U256 types. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Default)] +#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Copy, Clone, Default)] pub struct Wei(U256); impl Wei { const ETH_TO_WEI: U256 = U256([1_000_000_000_000_000_000, 0, 0, 0]); @@ -206,6 +203,14 @@ impl Wei { pub fn raw(self) -> U256 { self.0 } + + pub fn checked_sub(self, other: Self) -> Option { + self.0.checked_sub(other.0).map(Self) + } + + pub fn checked_add(self, other: Self) -> Option { + self.0.checked_add(other.0).map(Self) + } } impl prelude::Sub for Wei { type Output = Self; @@ -414,7 +419,7 @@ pub(crate) trait SdkProcess { fn sdk_process(self); } -impl> SdkProcess for EngineResult { +impl, E: AsRef<[u8]>> SdkProcess for Result { fn sdk_process(self) { match self { Ok(r) => sdk::return_output(r.as_ref()),