diff --git a/evm/src/omni-bridge/contracts/BridgeTypes.sol b/evm/src/omni-bridge/contracts/BridgeTypes.sol index 4f7e048f..7d4d144f 100644 --- a/evm/src/omni-bridge/contracts/BridgeTypes.sol +++ b/evm/src/omni-bridge/contracts/BridgeTypes.sol @@ -44,7 +44,8 @@ library BridgeTypes { string token, string name, string symbol, - uint8 decimals + uint8 decimals, + uint8 originDecimals ); event LogMetadata( diff --git a/evm/src/omni-bridge/contracts/OmniBridge.sol b/evm/src/omni-bridge/contracts/OmniBridge.sol index 5c869767..ceb768e7 100644 --- a/evm/src/omni-bridge/contracts/OmniBridge.sol +++ b/evm/src/omni-bridge/contracts/OmniBridge.sol @@ -61,11 +61,26 @@ contract OmniBridge is _grantRole(PAUSABLE_ADMIN_ROLE, _msgSender()); } - function addCustomToken(string calldata nearTokenId, address tokenAddress, address customMinter) external onlyRole(DEFAULT_ADMIN_ROLE) { + function addCustomToken(string calldata nearTokenId, address tokenAddress, address customMinter, uint8 originDecimals) external onlyRole(DEFAULT_ADMIN_ROLE) { isBridgeToken[tokenAddress] = true; ethToNearToken[tokenAddress] = nearTokenId; nearToEthToken[nearTokenId] = tokenAddress; customMinters[tokenAddress] = customMinter; + + string memory name = IERC20Metadata(tokenAddress).name(); + string memory symbol = IERC20Metadata(tokenAddress).symbol(); + uint8 decimals = IERC20Metadata(tokenAddress).decimals(); + + deployTokenExtension(nearTokenId, tokenAddress, decimals, originDecimals); + + emit BridgeTypes.DeployToken( + tokenAddress, + nearTokenId, + name, + symbol, + decimals, + originDecimals + ); } function removeCustomToken(address tokenAddress) external onlyRole(DEFAULT_ADMIN_ROLE) { @@ -90,6 +105,7 @@ contract OmniBridge is } require(!isBridgeToken[nearToEthToken[metadata.token]], "ERR_TOKEN_EXIST"); + uint8 decimals = _normalizeDecimals(metadata.decimals); address bridgeTokenProxy = address( new ERC1967Proxy( @@ -98,18 +114,19 @@ contract OmniBridge is BridgeToken.initialize.selector, metadata.name, metadata.symbol, - metadata.decimals + decimals ) ) ); - deployTokenExtension(metadata.token, bridgeTokenProxy); + deployTokenExtension(metadata.token, bridgeTokenProxy, decimals, metadata.decimals); emit BridgeTypes.DeployToken( bridgeTokenProxy, metadata.token, metadata.name, metadata.symbol, + decimals, metadata.decimals ); @@ -120,7 +137,7 @@ contract OmniBridge is return bridgeTokenProxy; } - function deployTokenExtension(string memory token, address tokenAddress) internal virtual {} + function deployTokenExtension(string memory token, address tokenAddress, uint8 decimals, uint8 originDecimals) internal virtual {} function setMetadata( string calldata token, @@ -292,6 +309,16 @@ contract OmniBridge is proxy.upgradeToAndCall(implementation, bytes("")); } + function _normalizeDecimals( + uint8 decimals + ) internal pure returns (uint8) { + uint8 maxAllowedDecimals = 18; + if (decimals > maxAllowedDecimals) { + return maxAllowedDecimals; + } + return decimals; + } + function _authorizeUpgrade( address newImplementation ) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} diff --git a/evm/src/omni-bridge/contracts/OmniBridgeWormhole.sol b/evm/src/omni-bridge/contracts/OmniBridgeWormhole.sol index 27b3134e..4ef82474 100644 --- a/evm/src/omni-bridge/contracts/OmniBridgeWormhole.sol +++ b/evm/src/omni-bridge/contracts/OmniBridgeWormhole.sol @@ -40,12 +40,14 @@ contract OmniBridgeWormhole is OmniBridge { _consistencyLevel = consistencyLevel; } - function deployTokenExtension(string memory token, address tokenAddress) internal override { + function deployTokenExtension(string memory token, address tokenAddress, uint8 decimals, uint8 originDecimals) internal override { bytes memory payload = bytes.concat( bytes1(uint8(MessageType.DeployToken)), Borsh.encodeString(token), bytes1(omniBridgeChainId), - Borsh.encodeAddress(tokenAddress) + Borsh.encodeAddress(tokenAddress), + bytes1(decimals), + bytes1(originDecimals) ); _wormhole.publishMessage{value: msg.value}( wormholeNonce, diff --git a/evm/src/omni-bridge/contracts/test/TestWormhole.sol b/evm/src/omni-bridge/contracts/test/TestWormhole.sol index dc3a2e59..211e086e 100644 --- a/evm/src/omni-bridge/contracts/test/TestWormhole.sol +++ b/evm/src/omni-bridge/contracts/test/TestWormhole.sol @@ -9,6 +9,7 @@ contract TestWormhole { bytes memory payload, uint8 consistencyLevel ) external payable returns (uint64) { + require(msg.value == this.messageFee(), "invalid fee"); emit MessagePublished(nonce, payload, consistencyLevel); return 0; } diff --git a/evm/tests/BridgeToken.ts b/evm/tests/BridgeToken.ts index c3dacd96..830392b7 100644 --- a/evm/tests/BridgeToken.ts +++ b/evm/tests/BridgeToken.ts @@ -11,6 +11,7 @@ const PauseMode = { PausedFinTransfer: 1 << 1, } const PauseAll = PauseMode.PausedInitTransfer | PauseMode.PausedFinTransfer +const PanicCodeArithmeticOperationOverflowed = "0x11" describe("BridgeToken", () => { const wrappedNearId = "wrap.testnet" @@ -69,7 +70,7 @@ describe("BridgeToken", () => { const token = OmniBridgeInstance.attach(tokenProxyAddress) as BridgeToken expect(await token.name()).to.be.equal("Wrapped NEAR fungible token") expect(await token.symbol()).to.be.equal("wNEAR") - expect((await token.decimals()).toString()).to.be.equal("24") + expect((await token.decimals()).toString()).to.be.equal("18") }) it("can't create token if token already exists", async () => { @@ -290,7 +291,7 @@ describe("BridgeToken", () => { "testrecipient.near", message, ), - ).to.be.revertedWithCustomError(OmniBridge, "InvalidValue") + ).to.be.revertedWithPanic(PanicCodeArithmeticOperationOverflowed) }) it("can't init transfer when value is too high", async () => { @@ -305,7 +306,7 @@ describe("BridgeToken", () => { const message = "" await expect( - OmniBridge.initTransfer( + OmniBridge.connect(user1).initTransfer( tokenProxyAddress, payload.amount, fee, @@ -362,7 +363,7 @@ describe("BridgeToken", () => { expect(await BridgeTokenV2Proxied.returnTestString()).to.equal("test") expect(await BridgeTokenV2Proxied.name()).to.equal("Wrapped NEAR fungible token") expect(await BridgeTokenV2Proxied.symbol()).to.equal("wNEAR") - expect((await BridgeTokenV2Proxied.decimals()).toString()).to.equal("24") + expect((await BridgeTokenV2Proxied.decimals()).toString()).to.equal("18") }) it("user can't upgrade token contract", async () => { diff --git a/evm/tests/BridgeTokenWormhole.ts b/evm/tests/BridgeTokenWormhole.ts index 2300f042..002c969e 100644 --- a/evm/tests/BridgeTokenWormhole.ts +++ b/evm/tests/BridgeTokenWormhole.ts @@ -6,6 +6,8 @@ import { ethers, upgrades } from "hardhat" import type { BridgeToken, OmniBridgeWormhole, TestWormhole } from "../typechain-types" import { depositSignature, metadataSignature, testWallet } from "./helpers/signatures" +const WormholeFee = 10000 + class FinTransferWormholeMessage { static schema = { struct: { @@ -124,7 +126,7 @@ describe("BridgeTokenWormhole", () => { ): Promise<{ tokenProxyAddress: string; token: BridgeToken }> { const { signature, payload } = metadataSignature(tokenId) - await OmniBridgeWormhole.deployToken(signature, payload) + await OmniBridgeWormhole.deployToken(signature, payload, { value: WormholeFee }) const tokenProxyAddress = await OmniBridgeWormhole.nearToEthToken(tokenId) const token = OmniBridgeInstance.attach(tokenProxyAddress) as BridgeToken return { tokenProxyAddress, token } @@ -133,7 +135,7 @@ describe("BridgeTokenWormhole", () => { it("deploy token", async () => { const { signature, payload } = metadataSignature(wrappedNearId) - await expect(await OmniBridgeWormhole.deployToken(signature, payload)) + await expect(await OmniBridgeWormhole.deployToken(signature, payload, { value: WormholeFee })) .to.emit(TestWormhole, "MessagePublished") .withArgs(0, anyValue, consistencyLevel) }) @@ -154,7 +156,7 @@ describe("BridgeTokenWormhole", () => { feeRecipient: payload.feeRecipient, }) - await expect(OmniBridgeWormhole.finTransfer(signature, payload)) + await expect(OmniBridgeWormhole.finTransfer(signature, payload, { value: WormholeFee })) .to.emit(TestWormhole, "MessagePublished") .withArgs(1, messagePayload, consistencyLevel) @@ -167,7 +169,7 @@ describe("BridgeTokenWormhole", () => { const { token } = await createToken(wrappedNearId) const tokenProxyAddress = await token.getAddress() const { signature, payload } = depositSignature(tokenProxyAddress, await user1.getAddress()) - await OmniBridgeWormhole.finTransfer(signature, payload) + await OmniBridgeWormhole.finTransfer(signature, payload, { value: WormholeFee }) const recipient = "testrecipient.near" const fee = 0 @@ -198,7 +200,7 @@ describe("BridgeTokenWormhole", () => { recipient, message, { - value: 10000, + value: WormholeFee, }, ), ) @@ -212,7 +214,7 @@ describe("BridgeTokenWormhole", () => { const { token } = await createToken(wrappedNearId) const tokenProxyAddress = await token.getAddress() const { signature, payload } = depositSignature(tokenProxyAddress, await user1.getAddress()) - await OmniBridgeWormhole.finTransfer(signature, payload) + await OmniBridgeWormhole.finTransfer(signature, payload, { value: WormholeFee }) await expect( OmniBridgeWormhole.connect(user1).initTransfer( @@ -223,6 +225,6 @@ describe("BridgeTokenWormhole", () => { "testrecipient.near", "", ), - ).to.be.revertedWithCustomError(OmniBridgeWormhole, "InvalidValue") + ).to.be.revertedWith("invalid fee") }) }) diff --git a/near/omni-bridge/src/lib.rs b/near/omni-bridge/src/lib.rs index ed759cbf..b53a81a8 100644 --- a/near/omni-bridge/src/lib.rs +++ b/near/omni-bridge/src/lib.rs @@ -27,7 +27,7 @@ use omni_types::{ BasicMetadata, ChainKind, Fee, InitTransferMsg, MetadataPayload, Nonce, OmniAddress, PayloadType, SignRequest, TransferId, TransferMessage, TransferMessagePayload, UpdateFee, }; -use storage::{TransferMessageStorage, TransferMessageStorageValue, NEP141_DEPOSIT}; +use storage::{Decimals, TransferMessageStorage, TransferMessageStorageValue, NEP141_DEPOSIT}; mod errors; mod storage; @@ -70,6 +70,7 @@ enum StorageKey { TokenDeployerAccounts, DeployedTokens, DestinationNonces, + TokenDecimals, } #[derive(AccessControlRole, Deserialize, Serialize, Copy, Clone)] @@ -164,6 +165,7 @@ pub struct Contract { pub finalised_transfers: LookupSet, pub token_id_to_address: LookupMap<(ChainKind, AccountId), OmniAddress>, pub token_address_to_id: LookupMap, + pub token_decimals: LookupMap, pub deployed_tokens: LookupSet, pub token_deployer_accounts: LookupMap, pub mpc_signer: AccountId, @@ -248,6 +250,7 @@ impl Contract { finalised_transfers: LookupSet::new(StorageKey::FinalisedTransfers), token_id_to_address: LookupMap::new(StorageKey::TokenIdToAddress), token_address_to_id: LookupMap::new(StorageKey::TokenAddressToId), + token_decimals: LookupMap::new(StorageKey::TokenDecimals), deployed_tokens: LookupSet::new(StorageKey::DeployedTokens), token_deployer_accounts: LookupMap::new(StorageKey::TokenDeployerAccounts), mpc_signer, @@ -398,12 +401,21 @@ impl Contract { ) .unwrap_or_else(|| env::panic_str("ERR_FAILED_TO_GET_TOKEN_ADDRESS")); + let decimals = self + .token_decimals + .get(&token_address) + .sdk_expect("ERR_TOKEN_DECIMALS_NOT_FOUND"); + let amount_to_transfer = Self::normalize_amount( + transfer_message.amount.0 - transfer_message.fee.fee.0, + decimals, + ); + let transfer_payload = TransferMessagePayload { prefix: PayloadType::TransferMessage, destination_nonce: transfer_message.destination_nonce, transfer_id, token_address, - amount: U128(transfer_message.amount.0 - transfer_message.fee.fee.0), + amount: U128(amount_to_transfer), recipient: transfer_message.recipient, fee_recipient, }; @@ -495,14 +507,22 @@ impl Contract { "Unknown factory" ); + let decimals = self + .token_decimals + .get(&init_transfer.token) + .sdk_expect("ERR_TOKEN_DECIMALS_NOT_FOUND"); + let destination_nonce = self.get_next_destination_nonce(init_transfer.recipient.get_chain()); let transfer_message = TransferMessage { origin_nonce: init_transfer.origin_nonce, token: init_transfer.token, - amount: init_transfer.amount, + amount: Self::denormalize_amount(init_transfer.amount.0, decimals).into(), recipient: init_transfer.recipient, - fee: init_transfer.fee, + fee: Fee { + fee: Self::denormalize_amount(init_transfer.fee.fee.0, decimals).into(), + native_fee: init_transfer.fee.native_fee, + }, sender: init_transfer.sender, msg: init_transfer.msg, destination_nonce, @@ -564,7 +584,20 @@ impl Contract { ); let message = self.remove_transfer_message(fin_transfer.transfer_id); - let fee = message.amount.0 - fin_transfer.amount.0; + let token_address = self + .get_token_address( + message.get_destination_chain(), + self.get_token_id(&message.token), + ) + .unwrap_or_else(|| env::panic_str("ERR_FAILED_TO_GET_TOKEN_ADDRESS")); + + let denormalized_amount = Self::denormalize_amount( + fin_transfer.amount.0, + self.token_decimals + .get(&token_address) + .sdk_expect("ERR_TOKEN_DECIMALS_NOT_FOUND"), + ); + let fee = message.amount.0 - denormalized_amount; if message.fee.native_fee.0 != 0 { if message.get_origin_chain() == ChainKind::Near { @@ -707,6 +740,18 @@ impl Contract { .is_none(), "ERR_TOKEN_EXIST" ); + require!( + self.token_decimals + .insert( + token_address, + &Decimals { + decimals: metadata.decimals, + origin_decimals: metadata.decimals + } + ) + .is_none(), + "ERR_TOKEN_EXIST" + ); require!(self.deployed_tokens.insert(&token_id), "ERR_TOKEN_EXIST"); let required_deposit = env::storage_byte_cost() .saturating_mul((env::storage_usage().saturating_sub(storage_usage)).into()) @@ -782,6 +827,15 @@ impl Contract { ); self.token_address_to_id .insert(&deploy_token.token_address, &deploy_token.token); + + self.token_decimals.insert( + &deploy_token.token_address, + &Decimals { + decimals: deploy_token.decimals, + origin_decimals: deploy_token.origin_decimals, + }, + ); + let required_deposit = env::storage_byte_cost() .saturating_mul((env::storage_usage().saturating_sub(storage_usage)).into()); @@ -875,6 +929,13 @@ impl Contract { &(token_address.get_chain(), token_id.clone()), &token_address, ); + self.token_decimals.insert( + &token_address, + &Decimals { + decimals: 0, + origin_decimals: 0, + }, + ); ext_token::ext(token_id) .with_static_gas(STORAGE_DEPOSIT_GAS) @@ -1211,4 +1272,14 @@ impl Contract { Promise::new(account_id).transfer(amount); } } + + fn denormalize_amount(amount: u128, decimals: Decimals) -> u128 { + let diff_decimals: u32 = (decimals.origin_decimals - decimals.decimals).into(); + amount * (10_u128.pow(diff_decimals)) + } + + fn normalize_amount(amount: u128, decimals: Decimals) -> u128 { + let diff_decimals: u32 = (decimals.origin_decimals - decimals.decimals).into(); + amount / (10_u128.pow(diff_decimals)) + } } diff --git a/near/omni-bridge/src/storage.rs b/near/omni-bridge/src/storage.rs index aa1db87a..72ca58b8 100644 --- a/near/omni-bridge/src/storage.rs +++ b/near/omni-bridge/src/storage.rs @@ -40,6 +40,12 @@ impl TransferMessageStorage { } } +#[derive(BorshDeserialize, BorshSerialize, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Decimals { + pub decimals: u8, + pub origin_decimals: u8, +} + #[near_bindgen] impl Contract { #[payable] @@ -183,7 +189,7 @@ impl Contract { .len() as u64; env::storage_byte_cost() - .saturating_mul((2 * (Self::get_basic_storage() + key_len + value_len)).into()) + .saturating_mul((3 * (Self::get_basic_storage() + key_len + value_len)).into()) } pub fn required_balance_for_deploy_token(&self) -> NearToken { diff --git a/near/omni-bridge/src/tests/lib_test.rs b/near/omni-bridge/src/tests/lib_test.rs index cf1adc44..3e3e8a01 100644 --- a/near/omni-bridge/src/tests/lib_test.rs +++ b/near/omni-bridge/src/tests/lib_test.rs @@ -1,6 +1,7 @@ use near_contract_standards::storage_management::StorageBalance; use omni_types::locker_args::StorageDepositAction; +use crate::storage::Decimals; use crate::Contract; use near_sdk::test_utils::VMContextBuilder; use near_sdk::RuntimeFeesConfig; @@ -486,6 +487,13 @@ fn test_fin_transfer_callback_near_success() { &native_token_address, &DEFAULT_FT_CONTRACT_ACCOUNT.parse().unwrap(), ); + contract.token_decimals.insert( + &OmniAddress::Near(AccountId::try_from(DEFAULT_FT_CONTRACT_ACCOUNT.to_string()).unwrap()), + &Decimals { + decimals: 24, + origin_decimals: 24, + }, + ); let storage_actions = vec![ StorageDepositAction { @@ -564,6 +572,14 @@ fn test_fin_transfer_callback_non_near_success() { let eth_recipient = OmniAddress::Eth(EvmAddress::from_str(DEFAULT_ETH_USER_ADDRESS).unwrap()); let prover_result = get_prover_result(Some(eth_recipient.clone())); + contract.token_decimals.insert( + &OmniAddress::Near(AccountId::try_from(DEFAULT_FT_CONTRACT_ACCOUNT.to_string()).unwrap()), + &Decimals { + decimals: 24, + origin_decimals: 24, + }, + ); + setup_test_env( predecessor.clone(), NearToken::from_near(1), @@ -654,3 +670,75 @@ fn test_is_transfer_finalised() { contract.finalised_transfers.insert(&transfer_id); assert!(contract.is_transfer_finalised(transfer_id)); } + +#[test] +fn test_normalize_amount() { + assert_eq!( + Contract::normalize_amount( + u128::MAX, + Decimals { + decimals: 18, + origin_decimals: 18 + } + ), + u128::MAX + ); + + assert_eq!( + Contract::normalize_amount( + u128::MAX, + Decimals { + decimals: 18, + origin_decimals: 24 + } + ), + u128::MAX / 1_000_000 + ); + + assert_eq!( + Contract::normalize_amount( + u128::MAX, + Decimals { + decimals: 9, + origin_decimals: 24 + } + ), + u128::MAX / 1_000_000_000_000_000 + ); +} + +#[test] +fn test_denormalize_amount() { + assert_eq!( + Contract::denormalize_amount( + u128::MAX, + Decimals { + decimals: 18, + origin_decimals: 18 + } + ), + u128::MAX + ); + + assert_eq!( + Contract::denormalize_amount( + u64::MAX.into(), + Decimals { + decimals: 18, + origin_decimals: 24 + } + ), + u64::MAX as u128 * 1_000_000_u128 + ); + + assert_eq!( + Contract::denormalize_amount( + u64::MAX.into(), + Decimals { + decimals: 9, + origin_decimals: 24 + } + ), + u64::MAX as u128 * 1_000_000_000_000_000_u128 + ); +} diff --git a/near/omni-prover/wormhole-omni-prover-proxy/src/parsed_vaa.rs b/near/omni-prover/wormhole-omni-prover-proxy/src/parsed_vaa.rs index 235c6a18..01f524a3 100644 --- a/near/omni-prover/wormhole-omni-prover-proxy/src/parsed_vaa.rs +++ b/near/omni-prover/wormhole-omni-prover-proxy/src/parsed_vaa.rs @@ -118,6 +118,8 @@ struct DeployTokenWh { payload_type: ProofKind, token: String, token_address: OmniAddress, + decimals: u8, + origin_decimals: u8, } #[derive(Debug, BorshDeserialize)] @@ -215,6 +217,8 @@ impl TryInto for ParsedVAA { Ok(DeployTokenMessage { token: parsed_payload.token.parse().map_err(stringify)?, token_address: parsed_payload.token_address.clone(), + decimals: parsed_payload.decimals, + origin_decimals: parsed_payload.origin_decimals, emitter_address: OmniAddress::new_from_slice( parsed_payload.token_address.get_chain(), &self.emitter_address, diff --git a/near/omni-tests/src/fin_transfer.rs b/near/omni-tests/src/fin_transfer.rs index a94fd847..f3bb8bcd 100644 --- a/near/omni-tests/src/fin_transfer.rs +++ b/near/omni-tests/src/fin_transfer.rs @@ -1,14 +1,14 @@ #[cfg(test)] mod tests { use crate::helpers::tests::{ - account_n, eth_eoa_address, eth_factory_address, relayer_account_id, LOCKER_PATH, - MOCK_PROVER_PATH, MOCK_TOKEN_PATH, NEP141_DEPOSIT, + account_n, eth_eoa_address, eth_factory_address, eth_token_address, relayer_account_id, + LOCKER_PATH, MOCK_PROVER_PATH, MOCK_TOKEN_PATH, NEP141_DEPOSIT, }; use near_sdk::{borsh, json_types::U128, serde_json::json, AccountId}; use near_workspaces::types::NearToken; use omni_types::{ - locker_args::{FinTransferArgs, StorageDepositAction}, - prover_result::{InitTransferMessage, ProverResult}, + locker_args::{BindTokenArgs, FinTransferArgs, StorageDepositAction}, + prover_result::{DeployTokenMessage, InitTransferMessage, ProverResult}, Fee, OmniAddress, }; use rstest::rstest; @@ -73,7 +73,9 @@ mod tests { "Expected an error but got success" ), Err(result_error) => { - let error = expected_error.expect("Got an error when none was expected"); + let error = expected_error.expect(&format!( + "Got an error {result_error} when none was expected" + )); assert!( result_error.to_string().contains(error), "Wrong error. Got: {}, Expected: {}", @@ -166,6 +168,31 @@ mod tests { .await? .into_result()?; + // Bind token + let required_balance_for_bind_token: NearToken = locker_contract + .view("required_balance_for_bind_token") + .await? + .json()?; + + relayer_account + .call(locker_contract.id(), "bind_token") + .args_borsh(BindTokenArgs { + chain_kind: omni_types::ChainKind::Eth, + prover_args: borsh::to_vec(&ProverResult::DeployToken(DeployTokenMessage { + token: token_contract.id().clone(), + token_address: eth_token_address(), + decimals: 24, + origin_decimals: 24, + emitter_address: eth_factory_address(), + })) + .unwrap(), + }) + .deposit(required_balance_for_bind_token) + .max_gas() + .transact() + .await? + .into_result()?; + let required_deposit_for_fin_transfer = NEP141_DEPOSIT .saturating_mul(storage_deposit_accounts.len() as u128) .saturating_add(required_balance_for_fin_transfer); @@ -187,7 +214,7 @@ mod tests { storage_deposit_actions, prover_args: borsh::to_vec(&ProverResult::InitTransfer(InitTransferMessage { origin_nonce: 1, - token: OmniAddress::Near(token_contract.id().clone()), + token: eth_token_address(), recipient: OmniAddress::Near(account_n(1)), amount: U128(amount), fee: Fee { diff --git a/near/omni-tests/src/helpers.rs b/near/omni-tests/src/helpers.rs index 6b33d05a..72d667f0 100644 --- a/near/omni-tests/src/helpers.rs +++ b/near/omni-tests/src/helpers.rs @@ -30,6 +30,22 @@ pub mod tests { .unwrap() } + pub fn arb_factory_address() -> OmniAddress { + "arb:0x252e87862A3A720287E7fd527cE6e8d0738427A2" + .parse() + .unwrap() + } + + pub fn base_factory_address() -> OmniAddress { + "base:0x252e87862A3A720287E7fd527cE6e8d0738427A2" + .parse() + .unwrap() + } + + pub fn sol_factory_address() -> OmniAddress { + "sol:11111111111111111111111111111111".parse().unwrap() + } + pub fn eth_eoa_address() -> OmniAddress { "eth:0xc5ed912ca6db7b41de4ef3632fa0a5641e42bf09" .parse() @@ -88,6 +104,7 @@ pub mod tests { pub fn get_test_deploy_token_args( token_address: &OmniAddress, + factory_contract_address: &OmniAddress, token_metadata: &BasicMetadata, ) -> DeployTokenArgs { let log_metadata_message = LogMetadataMessage { @@ -95,7 +112,7 @@ pub mod tests { name: token_metadata.name.clone(), symbol: token_metadata.symbol.clone(), decimals: token_metadata.decimals, - emitter_address: token_address.clone(), + emitter_address: factory_contract_address.clone(), }; let prover_result = ProverResult::LogMetadata(log_metadata_message); @@ -111,11 +128,15 @@ pub mod tests { token: &AccountId, token_address: &OmniAddress, emitter_address: &OmniAddress, + decimals: u8, + origin_decimals: u8, ) -> BindTokenArgs { let deploy_token_message = DeployTokenMessage { token: token.clone(), token_address: token_address.clone(), emitter_address: emitter_address.clone(), + decimals, + origin_decimals, }; let prover_result = ProverResult::DeployToken(deploy_token_message); diff --git a/near/omni-tests/src/init_transfer.rs b/near/omni-tests/src/init_transfer.rs index 560c9ca9..4e0b13bb 100644 --- a/near/omni-tests/src/init_transfer.rs +++ b/near/omni-tests/src/init_transfer.rs @@ -149,6 +149,8 @@ mod tests { &token_contract.id(), ð_token_address(), ð_factory_address, + 24, + 24, )) .deposit(required_deposit_for_bind_token) .max_gas() diff --git a/near/omni-tests/src/omni_token.rs b/near/omni-tests/src/omni_token.rs index 6ac564eb..2a24c778 100644 --- a/near/omni-tests/src/omni_token.rs +++ b/near/omni-tests/src/omni_token.rs @@ -1,9 +1,10 @@ #[cfg(test)] mod tests { use crate::helpers::tests::{ - account_n, arb_token_address, base_token_address, eth_eoa_address, eth_token_address, - get_test_deploy_token_args, sol_token_address, LOCKER_PATH, MOCK_PROVER_PATH, - NEP141_DEPOSIT, TOKEN_DEPLOYER_PATH, + account_n, arb_factory_address, arb_token_address, base_factory_address, + base_token_address, eth_eoa_address, eth_factory_address, eth_token_address, + get_test_deploy_token_args, sol_factory_address, sol_token_address, LOCKER_PATH, + MOCK_PROVER_PATH, NEP141_DEPOSIT, TOKEN_DEPLOYER_PATH, }; use anyhow; use near_sdk::borsh; @@ -23,6 +24,7 @@ mod tests { locker: near_workspaces::Contract, token_contract: near_workspaces::Contract, init_token_address: OmniAddress, + factory_contract_address: OmniAddress, token_metadata: BasicMetadata, } @@ -86,10 +88,18 @@ mod tests { .await? .into_result()?; + let factory_contract_address = match init_token_address.get_chain() { + ChainKind::Eth => eth_factory_address(), + ChainKind::Sol => sol_factory_address(), + ChainKind::Arb => arb_factory_address(), + ChainKind::Base => base_factory_address(), + _ => panic!("Unsupported chain"), + }; + locker .call("add_factory") .args_json(json!({ - "address": init_token_address, + "address": factory_contract_address, })) .max_gas() .transact() @@ -97,14 +107,21 @@ mod tests { .into_result()?; // Deploy token - let token_contract = - Self::deploy_token(&worker, &locker, &init_token_address, &token_metadata).await?; + let token_contract = Self::deploy_token( + &worker, + &locker, + &init_token_address, + &factory_contract_address, + &token_metadata, + ) + .await?; Ok(Self { worker, locker, token_contract, init_token_address, + factory_contract_address, token_metadata, }) } @@ -118,6 +135,7 @@ mod tests { worker: &near_workspaces::Worker, locker: &near_workspaces::Contract, init_token_address: &OmniAddress, + factoty_contract_address: &OmniAddress, token_metadata: &BasicMetadata, ) -> anyhow::Result { let token_deploy_initiator = worker @@ -150,6 +168,7 @@ mod tests { .call(locker.id(), "deploy_token") .args_borsh(get_test_deploy_token_args( init_token_address, + &factoty_contract_address, token_metadata, )) .deposit(required_storage) @@ -259,6 +278,7 @@ mod tests { &env.token_contract, &recipient, env.init_token_address, + env.factory_contract_address, amount, ) .await?; @@ -314,6 +334,7 @@ mod tests { &env.token_contract, &sender, env.init_token_address, + env.factory_contract_address, amount, ) .await?; @@ -369,6 +390,7 @@ mod tests { locker_contract: &near_workspaces::Contract, token_contract: &near_workspaces::Contract, recipient: &near_workspaces::Account, + token_address: OmniAddress, emitter_address: OmniAddress, amount: U128, ) -> anyhow::Result<()> { @@ -392,7 +414,7 @@ mod tests { storage_deposit_actions, prover_args: borsh::to_vec(&ProverResult::InitTransfer(InitTransferMessage { origin_nonce: 1, - token: OmniAddress::Near(token_contract.id().clone()), + token: token_address, recipient: OmniAddress::Near(recipient.id().clone()), amount, fee: Fee { diff --git a/near/omni-types/src/evm/events.rs b/near/omni-types/src/evm/events.rs index 4cdc3542..9f5b250b 100644 --- a/near/omni-types/src/evm/events.rs +++ b/near/omni-types/src/evm/events.rs @@ -37,7 +37,8 @@ sol! { string token, string name, string symbol, - uint8 decimals + uint8 decimals, + uint8 originDecimals ); event LogMetadata( @@ -128,15 +129,17 @@ impl TryFromLog> for DeployTokenMessage { } Ok(DeployTokenMessage { - emitter_address: OmniAddress::new_from_evm_address( - chain_kind, - H160(event.address.into()), - )?, token: event.data.token.parse().map_err(stringify)?, token_address: OmniAddress::new_from_evm_address( chain_kind, H160(event.data.tokenAddress.into()), )?, + decimals: event.data.decimals, + origin_decimals: event.data.originDecimals, + emitter_address: OmniAddress::new_from_evm_address( + chain_kind, + H160(event.address.into()), + )?, }) } } diff --git a/near/omni-types/src/prover_result.rs b/near/omni-types/src/prover_result.rs index 71868d4e..9a40b1aa 100644 --- a/near/omni-types/src/prover_result.rs +++ b/near/omni-types/src/prover_result.rs @@ -29,6 +29,8 @@ pub struct FinTransferMessage { pub struct DeployTokenMessage { pub token: AccountId, pub token_address: OmniAddress, + pub decimals: u8, + pub origin_decimals: u8, pub emitter_address: OmniAddress, } diff --git a/solana/bridge_token_factory/programs/bridge_token_factory/src/constants.rs b/solana/bridge_token_factory/programs/bridge_token_factory/src/constants.rs index bb12b994..4c6cebbe 100644 --- a/solana/bridge_token_factory/programs/bridge_token_factory/src/constants.rs +++ b/solana/bridge_token_factory/programs/bridge_token_factory/src/constants.rs @@ -26,3 +26,6 @@ pub const USED_NONCES_ACCOUNT_SIZE: u32 = 8 + (USED_NONCES_PER_ACCOUNT + 7) / 8; #[constant] pub const SOLANA_OMNI_BRIDGE_CHAIN_ID: u8 = 2; + +#[constant] +pub const MAX_ALLOWED_DECIMALS: u8 = 9; \ No newline at end of file diff --git a/solana/bridge_token_factory/programs/bridge_token_factory/src/instructions/user/deploy_token.rs b/solana/bridge_token_factory/programs/bridge_token_factory/src/instructions/user/deploy_token.rs index b88d2034..9fbe8198 100644 --- a/solana/bridge_token_factory/programs/bridge_token_factory/src/instructions/user/deploy_token.rs +++ b/solana/bridge_token_factory/programs/bridge_token_factory/src/instructions/user/deploy_token.rs @@ -1,4 +1,4 @@ -use crate::constants::{AUTHORITY_SEED, WRAPPED_MINT_SEED}; +use crate::constants::{AUTHORITY_SEED, MAX_ALLOWED_DECIMALS, WRAPPED_MINT_SEED}; use crate::instructions::wormhole_cpi::*; use crate::state::message::SignedPayload; use crate::state::message::{ @@ -25,7 +25,7 @@ pub struct DeployToken<'info> { payer = wormhole.payer, seeds = [WRAPPED_MINT_SEED, data.payload.token.as_bytes().as_ref()], bump, - mint::decimals = data.payload.decimals, + mint::decimals = std::cmp::min(MAX_ALLOWED_DECIMALS, data.payload.decimals), mint::authority = authority, )] pub mint: Box>, @@ -49,9 +49,11 @@ pub struct DeployToken<'info> { } impl<'info> DeployToken<'info> { - pub fn initialize_token_metadata(&self, metadata: DeployTokenPayload) -> Result<()> { + pub fn initialize_token_metadata(&self, mut metadata: DeployTokenPayload) -> Result<()> { let bump = &[self.wormhole.config.bumps.authority]; let signer_seeds = &[&[AUTHORITY_SEED, bump][..]]; + let origin_decimals = metadata.decimals; + metadata.decimals = std::cmp::min(MAX_ALLOWED_DECIMALS, metadata.decimals); let cpi_accounts = CreateMetadataAccountsV3 { payer: self.wormhole.payer.to_account_info(), @@ -86,6 +88,8 @@ impl<'info> DeployToken<'info> { let payload = DeployTokenResponse { token: metadata.token, solana_mint: self.mint.key(), + decimals: metadata.decimals, + origin_decimals, } .serialize_for_near(())?; diff --git a/solana/bridge_token_factory/programs/bridge_token_factory/src/state/message/deploy_token.rs b/solana/bridge_token_factory/programs/bridge_token_factory/src/state/message/deploy_token.rs index 96b89b6b..110d230c 100644 --- a/solana/bridge_token_factory/programs/bridge_token_factory/src/state/message/deploy_token.rs +++ b/solana/bridge_token_factory/programs/bridge_token_factory/src/state/message/deploy_token.rs @@ -30,6 +30,8 @@ impl Payload for DeployTokenPayload { pub struct DeployTokenResponse { pub token: String, pub solana_mint: Pubkey, + pub decimals: u8, + pub origin_decimals: u8, } impl Payload for DeployTokenResponse {