feat: Add check if an address is a contract in Cosmos (#4369)

### Description

Currently, integration into Cosmos make an assumption that the address
where a message should be delivered is a contract. This change allows
Relayer to actually check if the recipient is a contract.

### Backward compatibility

Yes

### Testing

* Tested using E2E tests for Cosmos.
* Tested using auxiliary unit test (it is marked as ignored since it
connects to real gRPC of Neutron).

---------

Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com>
pull/4379/head
Danil Nemirovsky 3 months ago committed by GitHub
parent 25c235a8e2
commit 86b1c7646c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 80
      rust/chains/hyperlane-cosmos/src/providers/grpc.rs
  2. 77
      rust/chains/hyperlane-cosmos/src/providers/grpc/tests.rs
  3. 12
      rust/chains/hyperlane-cosmos/src/providers/mod.rs

@ -16,8 +16,8 @@ use cosmrs::{
},
},
cosmwasm::wasm::v1::{
query_client::QueryClient as WasmQueryClient, MsgExecuteContract,
QuerySmartContractStateRequest,
query_client::QueryClient as WasmQueryClient, ContractInfo, MsgExecuteContract,
QueryContractInfoRequest, QuerySmartContractStateRequest,
},
traits::Message,
},
@ -97,13 +97,8 @@ pub trait WasmProvider: Send + Sync {
block_height: Option<u64>,
) -> ChainResult<Vec<u8>>;
/// Perform a wasm query against a specified contract address.
async fn wasm_query_to<T: Serialize + Sync + Send + Clone + Debug>(
&self,
to: String,
payload: T,
block_height: Option<u64>,
) -> ChainResult<Vec<u8>>;
/// Request contract info from the stored contract address.
async fn wasm_contract_info(&self) -> ChainResult<ContractInfo>;
/// Send a wasm tx.
async fn wasm_send<T: Serialize + Sync + Send + Clone + Debug>(
@ -450,6 +445,13 @@ impl WasmGrpcProvider {
sequence: base_account.sequence,
})
}
fn get_contract_address(&self) -> Result<&CosmosAddress, ChainCommunicationError> {
let contract_address = self.contract_address.as_ref().ok_or_else(|| {
ChainCommunicationError::from_other_str("No contract address available")
})?;
Ok(contract_address)
}
}
#[async_trait]
@ -486,27 +488,12 @@ impl WasmProvider for WasmGrpcProvider {
where
T: Serialize + Send + Sync + Clone + Debug,
{
let contract_address = self.contract_address.as_ref().ok_or_else(|| {
ChainCommunicationError::from_other_str("No contract address available")
})?;
self.wasm_query_to(contract_address.address(), payload, block_height)
.await
}
async fn wasm_query_to<T>(
&self,
to: String,
payload: T,
block_height: Option<u64>,
) -> ChainResult<Vec<u8>>
where
T: Serialize + Send + Sync + Clone,
{
let contract_address = self.get_contract_address()?;
let query_data = serde_json::to_string(&payload)?.as_bytes().to_vec();
let response = self
.provider
.call(move |provider| {
let to = to.clone();
let to = contract_address.address().clone();
let query_data = query_data.clone();
let future = async move {
let mut client = WasmQueryClient::new(provider.channel.clone());
@ -534,15 +521,43 @@ impl WasmProvider for WasmGrpcProvider {
Ok(response.data)
}
async fn wasm_contract_info(&self) -> ChainResult<ContractInfo> {
let contract_address = self.get_contract_address()?;
let response = self
.provider
.call(move |provider| {
let to = contract_address.address().clone();
let future = async move {
let mut client = WasmQueryClient::new(provider.channel.clone());
let request = tonic::Request::new(QueryContractInfoRequest { address: to });
let response = client
.contract_info(request)
.await
.map_err(ChainCommunicationError::from_other)?
.into_inner()
.contract_info
.ok_or(ChainCommunicationError::from_other_str(
"empty contract info",
))?;
Ok(response)
};
Box::pin(future)
})
.await?;
Ok(response)
}
#[instrument(skip(self))]
async fn wasm_send<T>(&self, payload: T, gas_limit: Option<U256>) -> ChainResult<TxResponse>
where
T: Serialize + Send + Sync + Clone + Debug,
{
let signer = self.get_signer()?;
let contract_address = self.contract_address.as_ref().ok_or_else(|| {
ChainCommunicationError::from_other_str("No contract address available")
})?;
let contract_address = self.get_contract_address()?;
let msgs = vec![MsgExecuteContract {
sender: signer.address.clone(),
contract: contract_address.address(),
@ -610,9 +625,7 @@ impl WasmProvider for WasmGrpcProvider {
// Estimating gas requires a signer, which we can reasonably expect to have
// since we need one to send a tx with the estimated gas anyways.
let signer = self.get_signer()?;
let contract_address = self.contract_address.as_ref().ok_or_else(|| {
ChainCommunicationError::from_other_str("No contract address available")
})?;
let contract_address = self.get_contract_address()?;
let msg = MsgExecuteContract {
sender: signer.address.clone(),
contract: contract_address.address(),
@ -636,3 +649,6 @@ impl BlockNumberGetter for WasmGrpcProvider {
self.latest_block_height().await
}
}
#[cfg(test)]
mod tests;

@ -0,0 +1,77 @@
use std::str::FromStr;
use url::Url;
use hyperlane_core::config::OperationBatchConfig;
use hyperlane_core::{ContractLocator, HyperlaneDomain, KnownHyperlaneDomain};
use crate::address::CosmosAddress;
use crate::grpc::{WasmGrpcProvider, WasmProvider};
use crate::{ConnectionConf, CosmosAmount, RawCosmosAmount};
#[ignore]
#[tokio::test]
async fn test_wasm_contract_info_success() {
// given
let provider = provider("neutron1sjzzd4gwkggy6hrrs8kxxatexzcuz3jecsxm3wqgregkulzj8r7qlnuef4");
// when
let result = provider.wasm_contract_info().await;
// then
assert!(result.is_ok());
let contract_info = result.unwrap();
assert_eq!(
contract_info.creator,
"neutron1dwnrgwsf5c9vqjxsax04pdm0mx007yrre4yyvm",
);
assert_eq!(
contract_info.admin,
"neutron1fqf5mprg3f5hytvzp3t7spmsum6rjrw80mq8zgkc0h6rxga0dtzqws3uu7",
);
}
#[ignore]
#[tokio::test]
async fn test_wasm_contract_info_no_contract() {
// given
let provider = provider("neutron1dwnrgwsf5c9vqjxsax04pdm0mx007yrre4yyvm");
// when
let result = provider.wasm_contract_info().await;
// then
assert!(result.is_err());
}
fn provider(address: &str) -> WasmGrpcProvider {
let domain = HyperlaneDomain::Known(KnownHyperlaneDomain::Neutron);
let address = CosmosAddress::from_str(address).unwrap();
let locator = Some(ContractLocator::new(&domain, address.digest()));
WasmGrpcProvider::new(
domain.clone(),
ConnectionConf::new(
vec![Url::parse("http://grpc-kralum.neutron-1.neutron.org:80").unwrap()],
"https://rpc-kralum.neutron-1.neutron.org".to_owned(),
"neutron-1".to_owned(),
"neutron".to_owned(),
"untrn".to_owned(),
RawCosmosAmount::new("untrn".to_owned(), "0".to_owned()),
32,
OperationBatchConfig {
batch_contract_address: None,
max_batch_size: 1,
},
),
CosmosAmount {
denom: "untrn".to_owned(),
amount: Default::default(),
},
locator,
None,
)
.unwrap()
}

@ -1,10 +1,12 @@
use async_trait::async_trait;
use tendermint_rpc::{client::CompatMode, HttpClient};
use hyperlane_core::{
BlockInfo, ChainInfo, ChainResult, ContractLocator, HyperlaneChain, HyperlaneDomain,
HyperlaneProvider, TxnInfo, H256, U256,
};
use tendermint_rpc::{client::CompatMode, HttpClient};
use crate::grpc::WasmProvider;
use crate::{ConnectionConf, CosmosAmount, HyperlaneCosmosError, Signer};
use self::grpc::WasmGrpcProvider;
@ -88,9 +90,11 @@ impl HyperlaneProvider for CosmosProvider {
todo!() // FIXME
}
async fn is_contract(&self, _address: &H256) -> ChainResult<bool> {
// FIXME
Ok(true)
async fn is_contract(&self, address: &H256) -> ChainResult<bool> {
match self.grpc_client.wasm_contract_info().await {
Ok(c) => Ok(true),
Err(e) => Ok(false),
}
}
async fn get_balance(&self, address: String) -> ChainResult<U256> {

Loading…
Cancel
Save