diff --git a/.github/workflows/ibc-test.yaml b/.github/workflows/ibc-test.yaml index 74819c5ea..d8526e6be 100644 --- a/.github/workflows/ibc-test.yaml +++ b/.github/workflows/ibc-test.yaml @@ -29,7 +29,7 @@ jobs: env: SRC_DIR: ${{ github.workspace }}/ibc-test-src AXON_COMMIT: d03d2bb7cb3dcdc03319c3a74beeee6715e7f448 - IBC_CONTRACT_COMMIT: f407f44289ef8abea31860b963f758dae1c16071 + IBC_CONTRACT_COMMIT: 35290d79c9fa45583b00f228dd3ed7e8468ccc67 strategy: fail-fast: false matrix: diff --git a/README.md b/README.md index 505960c5d..a902d1001 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Detailed deployment steps can be found in [ibc-ckb-contracts](https://github.com |escrow|WIP|WIP| ### Business Module Registration -When deploying the Solidity contract on Axon, an initial ICS20 transfer module is automatically registered in `OwnableIBCHandler` on port `port-0` during the contract migration process. This registration is open to all users. For detailed instructions on how to register your own business module, visit [ibc-solidity-contract](https://github.com/synapseweb3/ibc-solidity-contract) repository. +When deploying the Solidity contract on Axon, an initial ICS20 transfer module is automatically registered in `OwnableIBCHandler` on port `transfer` during the contract migration process. This registration is open to all users. For detailed instructions on how to register your own business module, visit [ibc-solidity-contract](https://github.com/synapseweb3/ibc-solidity-contract) repository. Unlike Axon, business modules cannot be registered directly with a contract on CKB. To address this, we have introduced [forcerelay-ckb-sdk](https://github.com/synapseweb3/forcerelay-ckb-sdk), designed to facilitate the distribution and calling of custom modules. @@ -89,7 +89,7 @@ Establishing IBC channels on both sides of Axon and CKB is required to run Force ``` $ forcerelay create channel \ --a-chain axon-0 --b-chain ckb4ibc-0 \ - --a-port port-0 --b-port \ + --a-port transfer --b-port \ --new-client-connection $ forcerelay start --config -``` \ No newline at end of file +``` diff --git a/crates/relayer/src/chain/axon.rs b/crates/relayer/src/chain/axon.rs index 95059ad4d..57aeb4a2b 100644 --- a/crates/relayer/src/chain/axon.rs +++ b/crates/relayer/src/chain/axon.rs @@ -76,7 +76,8 @@ use tendermint_rpc::endpoint::broadcast::tx_sync::Response; use self::{contract::OwnableIBCHandler, monitor::AxonEventMonitor}; type ContractProvider = SignerMiddleware, Wallet>; -type Contract = OwnableIBCHandler; +type IBCContract = OwnableIBCHandler; +type ERC20Contract = ERC20; use super::{ client::ClientSettings, @@ -122,6 +123,18 @@ lazy_static! { Mutex::new(HashMap::new()); } +abigen!( + ERC20, + r"[ + function totalSupply() external view returns (uint256) + function balanceOf(address account) external view returns (uint256) + function transfer(address to, uint256 amount) external returns (bool) + function allowance(address owner, address spender) external view returns (uint256) + function approve(address spender, uint256 amount) external returns (bool) + function transferFrom(address from, address to, uint256 amount) external returns (bool) + ]" +); + pub struct AxonChain { rt: Arc, config: AxonChainConfig, @@ -145,12 +158,16 @@ impl AxonChain { Ok(Arc::new(SignerMiddleware::new(self.client.clone(), wallet))) } - fn contract(&self) -> Result { - Ok(Contract::new( + fn contract(&self) -> Result { + Ok(IBCContract::new( self.config.contract_address, self.contract_provider()?, )) } + + fn erc20_contract(&self, address: H160) -> Result { + Ok(ERC20::new(address, self.contract_provider()?)) + } } impl ChainEndpoint for AxonChain { @@ -308,22 +325,25 @@ impl ChainEndpoint for AxonChain { self.light_client.check_misbehaviour(update, client_state) } - // FIXME implement this after use a real ics token contract - fn query_balance( - &self, - _key_name: Option<&str>, - _denom: Option<&str>, - ) -> Result { - // const DEFAULT_DENOM: &str = "AT"; - - // let key_name = key_name.unwrap_or(&self.config.key_name); - // let denom = denom.unwrap_or(DEFAULT_DENOM); - // let contract = self.ics20_contract()?; - // let wallet = self.get_wallet(key_name); + fn query_balance(&self, key_name: Option<&str>, denom: Option<&str>) -> Result { + let key_name = key_name.unwrap_or(&self.config.key_name); + let denom: &str = + denom.ok_or_else(|| Error::other_error("do not support default denom".into()))?; + let erc20_address = { + let denom = denom.trim_start_matches("0x"); + let bytes = hex::decode(denom).map_err(Error::other)?; + H160::from_slice(&bytes) + }; + let contract = self.erc20_contract(erc20_address)?; + let wallet = self.get_wallet(key_name)?; + let amount = self + .rt + .block_on(contract.balance_of(wallet.address()).call()) + .map_err(|err| Error::query(format!("{err:?}")))?; Ok(Balance { - amount: "100".to_owned(), - denom: "AT".to_owned(), + amount: format!("{amount:#x}"), + denom: denom.to_string(), }) } diff --git a/crates/relayer/src/chain/ckb4ibc.rs b/crates/relayer/src/chain/ckb4ibc.rs index ef36ebfea..5644af308 100644 --- a/crates/relayer/src/chain/ckb4ibc.rs +++ b/crates/relayer/src/chain/ckb4ibc.rs @@ -609,6 +609,9 @@ impl ChainEndpoint for Ckb4IbcChain { } fn ibc_version(&self) -> Result, Error> { + // TODO @jjy + // the ibc version should be matched with the CKB contract, + // IMO we can put it into the config of forcerelay or save the version in a cell Ok(None) } @@ -749,6 +752,7 @@ impl ChainEndpoint for Ckb4IbcChain { Ok(responses) } + // TODO verify target height with Axon light client / store fn verify_header( &mut self, _trusted: Height, @@ -819,6 +823,7 @@ impl ChainEndpoint for Ckb4IbcChain { Ok(vec![ckb_balance]) } + // TODO Need to align with CKB ibc contract fn query_denom_trace(&self, _hash: String) -> Result { warn!("axon query_denom_trace() cannot implement"); Ok(DenomTrace { @@ -849,6 +854,7 @@ impl ChainEndpoint for Ckb4IbcChain { .onchain_light_clients .keys() .map(|client_type| { + // TODO query latest_height from light client cell (for example Axon metadata cell) let client_id = self.config.lc_client_id(*client_type).unwrap(); let chain_id = self.config.lc_chain_id(&client_id.to_string()).unwrap(); IdentifiedAnyClientState { @@ -869,6 +875,7 @@ impl ChainEndpoint for Ckb4IbcChain { _include_proof: IncludeProof, ) -> Result<(AnyClientState, Option), Error> { let chain_id = self.config.lc_chain_id(&request.client_id.to_string())?; + // TODO query latest_height let client_state = CkbClientState { chain_id, latest_height: Height::default(), diff --git a/tools/ibc-test/README.md b/tools/ibc-test/README.md index a9aa4d8be..ccf23c64c 100644 --- a/tools/ibc-test/README.md +++ b/tools/ibc-test/README.md @@ -15,7 +15,7 @@ We use chain-A chain-B, connection-A connection-B or channel-A channel-B to repr `gaia` chain uses a builtin `transfer` port as the default port in IBC tests. -For Axon chain we use `port-0` as default port since it is defined in the [deployment script](https://github.com/synapseweb3/ibc-solidity-contract/blob/master/migrations/1_deploy_contracts.js#L84). +For Axon chain we use `transfer` as default port. For CKB chain we uses `blake2b(b"transfer")` as default port. diff --git a/tools/ibc-test/src/framework/bootstrap/node.rs b/tools/ibc-test/src/framework/bootstrap/node.rs index 7faef1aa8..280724840 100644 --- a/tools/ibc-test/src/framework/bootstrap/node.rs +++ b/tools/ibc-test/src/framework/bootstrap/node.rs @@ -37,47 +37,45 @@ pub fn bootstrap_single_node( chain_driver.rpc_address(), ); - let (process, miner_process) = match chain_driver.chain_type { + let validator = add_wallet(&chain_driver, "validator", use_random_id)?; + let user1 = add_wallet(&chain_driver, "user1", use_random_id)?; + let user2 = add_wallet(&chain_driver, "user2", use_random_id)?; + let relayer = match chain_driver.chain_type { + ChainType::Ckb => { + // FIXME @jjy use random pk as relayer once remove hardcoded deploy transactions + // let relayer = add_wallet(&chain_driver, "relayer", use_random_id)?; + add_ckb_devnet_relayer_wallet(&chain_driver, "relayer", use_random_id)? + } + ChainType::Axon => add_axon_devnet_relayer_wallet(&chain_driver, "relayer", use_random_id)?, + _ => add_wallet(&chain_driver, "relayer", use_random_id)?, + }; + + let (process, miner_process, denom) = match chain_driver.chain_type { ChainType::Ckb => { // FIXME @jjy // currently the deploy tx is hard coded, which means relayer must be a fixed pk - let (node, miner) = prepare_ckb_chain( - &chain_driver.home_path, - chain_driver.rpc_port as u32, - &[], - // &[(&relayer, 5_198_735_037_00000000u64)], - ); - (node, Some(miner)) + let (node, miner) = + prepare_ckb_chain(&chain_driver.home_path, chain_driver.rpc_port as u32, &[]); + + // TODO deploy a sUDT as default denom + let denom = Denom::base("ckb"); + (node, Some(miner), denom) } ChainType::Axon => { // TODO - let node = prepare_axon_chain( + let (node, denom) = prepare_axon_chain( &chain_driver.home_path, chain_driver.rpc_port as u32, - &[], - // &[(&relayer, 5_198_735_037_00000000u64)], + Some((&relayer, 1000000000000u64)), ) .unwrap(); - (node, None) + (node, None, denom) } _ => { unreachable!() } }; - let validator = add_wallet(&chain_driver, "validator", use_random_id)?; - let user1 = add_wallet(&chain_driver, "user1", use_random_id)?; - let user2 = add_wallet(&chain_driver, "user2", use_random_id)?; - let relayer = match chain_driver.chain_type { - ChainType::Ckb => { - // FIXME @jjy use random pk as relayer once remove hardcoded deploy transactions - // let relayer = add_wallet(&chain_driver, "relayer", use_random_id)?; - add_ckb_devnet_relayer_wallet(&chain_driver, "relayer", use_random_id)? - } - ChainType::Axon => add_axon_devnet_relayer_wallet(&chain_driver, "relayer", use_random_id)?, - _ => add_wallet(&chain_driver, "relayer", use_random_id)?, - }; - info!( "user wallet for chain {} - id: {}, address: {}", chain_driver.chain_id, user1.id.0, user1.address.0, @@ -97,8 +95,6 @@ pub fn bootstrap_single_node( user2, }; - let denom = Denom::base("ckb"); - let node = FullNode { chain_driver, denom, diff --git a/tools/ibc-test/src/framework/utils/axon.rs b/tools/ibc-test/src/framework/utils/axon.rs index 5df2fdc9b..77b33c8c0 100644 --- a/tools/ibc-test/src/framework/utils/axon.rs +++ b/tools/ibc-test/src/framework/utils/axon.rs @@ -12,7 +12,7 @@ use relayer::keyring::{Secp256k1AddressType, Secp256k1KeyPair}; use secp256k1::{Secp256k1, SecretKey}; -use toml_edit::{value, Document, Table}; +use toml_edit::{value, Document}; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; @@ -30,8 +30,8 @@ const IBC_CONTRACTS_SRC_PATH: &str = "IBC_CONTRACTS_SRC_PATH"; pub(crate) fn prepare_axon_chain( dir_path: &str, port: u32, - genesis_wallets: &[(&Wallet, u64)], -) -> Result { + pre_mint: Option<(&Wallet, u64)>, +) -> Result<(ChildProcess, Denom)> { println!("\n========== Prepare Axon node on port {port} ==========\n"); // read envs @@ -73,24 +73,10 @@ pub(crate) fn prepare_axon_chain( config_doc["rpc"]["http_listening_address"] = value(format!("0.0.0.0:{}", port)); config_doc["rpc"]["ws_listening_address"] = value(format!("0.0.0.0:{}", port + 1)); config_doc["network"]["listening_address"] = value(format!("/ip4/0.0.0.0/tcp/{}", port + 2)); - *config_doc["network"]["bootstraps"].get_mut(0).unwrap() = value(&format!( + *config_doc["network"]["bootstraps"].get_mut(0).unwrap() = value(format!( "/ip4/127.0.0.1/tcp/{}/p2p/QmNk6bBwkLPuqnsrtxpp819XLZY3ymgjs3p1nKtxBVgqxj", port + 2 )); - - // genesis wallets - for (wallet, balance) in genesis_wallets { - let mut item = Table::new(); - item.entry("address") - .or_insert(value(wallet.address.as_str())); - item.entry("balance") - .or_insert(value(hex::encode(balance.to_be_bytes()))); - config_doc["accounts"] - .as_array_of_tables_mut() - .unwrap() - .push(item); - } - fs::write(&chain_config_path, config_doc.to_string()) .with_context(|| format!("write config to {:?}", &chain_config_path))?; @@ -112,39 +98,59 @@ pub(crate) fn prepare_axon_chain( wait_for_port(port); // Deploy IBC contract - { - println!("deploying ibc contracts",); - let output = Command::new("yarn") - .arg("migrate") - .env("AXON_HTTP_RPC_URL", format!("http://localhost:{}", port)) - .current_dir(&ibc_contracts_src_path) - .output() - .with_context(|| "yarn migrate")?; - // get contract address from output - check_command_output(&output, &working_dir)?; - let output = String::from_utf8(output.stdout.clone())?; - let contract_address = parsing_contract_address_from_output(&output, "OwnableIBCHandler")?; - let mock_transfer_contract_address = - parsing_contract_address_from_output(&output, "MockTransfer")?; - let transfer_contract_address = - parsing_contract_address_from_output(&output, "ICS20TransferERC20")?; + println!("deploying ibc contracts",); + let output = Command::new("yarn") + .arg("migrate") + .env("AXON_HTTP_RPC_URL", format!("http://localhost:{}", port)) + .current_dir(&ibc_contracts_src_path) + .output() + .with_context(|| "yarn migrate")?; + // get contract address from output + check_command_output(&output, &working_dir)?; + let output = String::from_utf8(output.stdout.clone())?; + let contract_address = parsing_contract_address_from_output(&output, "OwnableIBCHandler")?; + let transfer_contract_address = + parsing_contract_address_from_output(&output, "ICS20TransferERC20")?; - println!("ibc handler deployed at {:#x}", contract_address); + println!("ibc handler deployed at {:#x}", contract_address); - // write deployment info - let deployment = DeployedContracts { - contract_address, - mock_transfer_contract_address, - transfer_contract_address, - image_cell_contract_address: ethers::types::H160::default(), - ckb_light_client_contract_address: ethers::types::H160::default(), - }; - let path = working_dir.join(AXON_CONTRACTS_CONFIG_PATH); - std::fs::write(path, toml::to_string(&deployment)?) - .with_context(|| "write deployment info")?; + // deploy test ERC20 and mint token + let token_name = "AT"; + let token_symbol = "AT"; + let mut cmd = Command::new("yarn"); + cmd.current_dir(&ibc_contracts_src_path) + .arg("truffle") + .arg("exec") + .arg("--network") + .arg("axon") + .arg("scripts/deploy_erc20.js") + .env("AXON_HTTP_RPC_URL", format!("http://localhost:{port}")) + .env("TOKEN_NAME", token_name) + .env("TOKEN_SYMBOL", token_symbol); + if let Some((mint_to, mint_amount)) = pre_mint { + cmd.env("MINT_TO", mint_to.address.as_str()) + .env("MINT_AMOUNT", mint_amount.to_string()); } + let output = cmd.output().with_context(|| "yarn truffle")?; + // get contract address from output + check_command_output(&output, &working_dir)?; + let output = String::from_utf8(output.stdout.clone())?; + let token_address = parsing_contract_address_from_output(&output, "SimpleToken")?; + let denom = Denom::base(&format!("{token_address:#x}")); + + println!("test token deployed at {:#x}", token_address); + + // write deployment info + let deployment = DeployedContracts { + contract_address, + transfer_contract_address, + image_cell_contract_address: ethers::types::H160::default(), + ckb_light_client_contract_address: ethers::types::H160::default(), + }; + let path = working_dir.join(AXON_CONTRACTS_CONFIG_PATH); + std::fs::write(path, toml::to_string(&deployment)?).with_context(|| "write deployment info")?; - Ok(chain_process) + Ok((chain_process, denom)) } fn check_command_output(output: &Output, working_dir: &Path) -> Result<()> { diff --git a/tools/ibc-test/src/framework/utils/common.rs b/tools/ibc-test/src/framework/utils/common.rs index 9fec79e45..0906ee791 100644 --- a/tools/ibc-test/src/framework/utils/common.rs +++ b/tools/ibc-test/src/framework/utils/common.rs @@ -55,7 +55,7 @@ pub fn transfer_port_id(chain_type: ChainType) -> PortId { } ChainType::Axon => { // Axon default port ID - PortId::from_str("port-0").unwrap() + PortId::from_str("transfer").unwrap() } _ => { unreachable!() diff --git a/tools/test-framework/src/relayer/axon/transfer.rs b/tools/test-framework/src/relayer/axon/transfer.rs index b58acaada..df8654a3f 100644 --- a/tools/test-framework/src/relayer/axon/transfer.rs +++ b/tools/test-framework/src/relayer/axon/transfer.rs @@ -15,21 +15,48 @@ use ibc_relayer::{ use ibc_relayer_types::{core::ics04_channel::packet::Packet, events::IbcEvent, Height}; abigen!( - MockTransfer, + TransferContract, r"[ - function sendTransfer(string calldata denom,uint64 amount,address receiver,string calldata sourcePort,string calldata sourceChannel,uint64 timeoutHeight) external + function mint(address account, string memory denom, uint256 amount) external + function sendTransfer(string calldata denom, uint64 amount, address receiver, string calldata sourcePort, string calldata sourceChannel, uint64 timeoutHeight) external + function transferFrom(address sender, address receiver, string memory denom, uint256 amount) external + function denomTokenContract(string denom) returns(address) + function getEscrowAddress(string memory sourceChannel) external view returns (address) ]" ); -async fn new_mock_contract( +abigen!( + ERC20, + r"[ + function totalSupply() external view returns (uint256) + function balanceOf(address account) external view returns (uint256) + function transfer(address to, uint256 amount) external returns (bool) + function allowance(address owner, address spender) external view returns (uint256) + function approve(address spender, uint256 amount) external returns (bool) + function transferFrom(address from, address to, uint256 amount) external returns (bool) + ]" +); + +async fn new_contract( client: Provider, key_pair: &Secp256k1KeyPair, - mock_transfer_address: H160, -) -> eyre::Result, Wallet>>> { + address: H160, +) -> eyre::Result, Wallet>>> { let chain_id: u64 = client.get_chainid().await?.as_u64(); let wallet = key_pair.clone().into_ether_wallet().with_chain_id(chain_id); let client = Arc::new(SignerMiddleware::new(client.clone(), wallet)); - Ok(MockTransfer::new(mock_transfer_address, client)) + Ok(TransferContract::new(address, client)) +} + +async fn new_erc20( + client: Provider, + key_pair: &Secp256k1KeyPair, + address: H160, +) -> eyre::Result, Wallet>>> { + let chain_id: u64 = client.get_chainid().await?.as_u64(); + let wallet = key_pair.clone().into_ether_wallet().with_chain_id(chain_id); + let client = Arc::new(SignerMiddleware::new(client.clone(), wallet)); + Ok(ERC20::new(address, client)) } pub fn read_deployed_contracts>(chain_dir: P) -> Result { @@ -56,13 +83,11 @@ pub async fn ibc_token_transfer( let client = Provider::connect(websocket_addr) .await .map_err(|err| eyre!(err))?; - // get ibc contract address - let (mock_transfer_address, ibc_handler_address) = { - let c = read_deployed_contracts(&home_path).expect("failed to fetch deployed contracts"); - (c.mock_transfer_contract_address, c.contract_address) - }; - let contract = - new_mock_contract(client.clone(), &sender.value().key, mock_transfer_address).await?; + // get contract addresses + let deployed = read_deployed_contracts(&home_path).expect("failed to fetch deployed contracts"); + let transfer_address = deployed.transfer_contract_address; + let ibc_handler_address = deployed.contract_address; + let contract = new_contract(client.clone(), &sender.value().key, transfer_address).await?; let receiver = { let slice = hex::decode(recipient.value().as_str().trim_start_matches("0x")) @@ -72,6 +97,17 @@ pub async fn ibc_token_transfer( let denom = token.denom().value().to_string(); let amount = token.amount().0.as_u64(); let timeout_height = timeout.map(|d| d.as_secs() / 8).unwrap_or_default(); + // ERC20 token approving + { + // Parse ERC20 address + let token_address = H160::from_slice(&hex::decode(denom.trim_start_matches("0x")).unwrap()); + let token = new_erc20(client.clone(), &sender.value().key, token_address).await?; + // approve + let tx = token.approve(transfer_address, amount.into()); + let pending_tx = tx.send().await.unwrap(); + pending_tx.await.unwrap().unwrap(); + } + // ICS sendTransfer let tx = contract.send_transfer( denom, amount, @@ -86,6 +122,7 @@ pub async fn ibc_token_transfer( let block_number = receipt.block_number.unwrap().as_u64(); let tx_hash = receipt.transaction_hash.into(); + // check packet is sent let ibc_logs: Vec = receipt .logs .into_iter() diff --git a/tools/test-framework/src/types/axon/mod.rs b/tools/test-framework/src/types/axon/mod.rs index 4948976a0..adfab4d3f 100644 --- a/tools/test-framework/src/types/axon/mod.rs +++ b/tools/test-framework/src/types/axon/mod.rs @@ -4,7 +4,6 @@ use serde_derive::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct DeployedContracts { pub contract_address: H160, - pub mock_transfer_contract_address: H160, pub transfer_contract_address: H160, pub image_cell_contract_address: H160, pub ckb_light_client_contract_address: H160,