Skip to content

Commit 14cc551

Browse files
authored
Adding example of calling EVM contract from CosmWasm contract (#79)
* examples in progress * call evm works * Adding EVM example readme. Tweaking the binding to address the recent change. * formatting * new line * formatting again * - Bump the version - update rust doc for CallEvm * Update README * Few README updates
1 parent 886802d commit 14cc551

File tree

12 files changed

+264
-14
lines changed

12 files changed

+264
-14
lines changed

contracts/sei-tester/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ cw2 = "0.13.2"
4747
schemars = "0.8.8"
4848
serde = { version = "1.0.137", default-features = false, features = ["derive"] }
4949
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
50-
base64 = { version = "0.13.0" }
50+
base64 = { version = "0.21.7" }
5151
thiserror = { version = "1.0.31" }
5252
protobuf = { version = "3.2.0", features = ["with-bytes"] }
5353

contracts/sei-tester/README.md

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Purpose
2+
3+
This package primarily serves as testing support for the sei-cosmwasm package.
4+
It includes an example contract that can be used to test package behavior
5+
locally and can be used as a reference for implementation details to include
6+
sei-chain integration in your smart contracts.
7+
8+
## Examples
9+
10+
### Call an EVM contract from a CosmWasm contract
11+
12+
First, we need to deploy an EVM contract on the Sei chain. Let's use a simple counter contract and
13+
foundry tooling to deploy it.
14+
15+
Install the [foundry tooling](https://book.getfoundry.sh/) by following this [Installation guide](https://book.getfoundry.sh/getting-started/installation.html).
16+
17+
18+
Create a new project following the [Creating New Project Guide](https://book.getfoundry.sh/projects/creating-a-new-project).
19+
20+
Once project is created, tweak the contract code to the following, by adding a `getCounter` function:
21+
22+
```solidity
23+
// SPDX-License-Identifier: UNLICENSED
24+
pragma solidity ^0.8.13;
25+
26+
contract Counter {
27+
uint256 public number;
28+
29+
function setNumber(uint256 newNumber) public {
30+
number = newNumber;
31+
}
32+
33+
function increment() public {
34+
number++;
35+
}
36+
37+
function getCount() public view returns (uint256) {
38+
return number;
39+
}
40+
}
41+
42+
```
43+
44+
And the test code to the following:
45+
```solidity
46+
// SPDX-License-Identifier: UNLICENSED
47+
pragma solidity ^0.8.13;
48+
49+
import {Test, console} from "forge-std/Test.sol";
50+
import {Counter} from "../src/Counter.sol";
51+
52+
contract CounterTest is Test {
53+
Counter public counter;
54+
55+
function setUp() public {
56+
counter = new Counter();
57+
counter.setNumber(0);
58+
}
59+
60+
function test_Increment() public {
61+
counter.increment();
62+
assertEq(counter.number(), 1);
63+
}
64+
65+
function testFuzz_SetNumber(uint256 x) public {
66+
counter.setNumber(x);
67+
assertEq(counter.number(), x);
68+
}
69+
70+
function test_GetCount() public {
71+
uint256 initialCount = counter.getCount();
72+
counter.increment();
73+
assertEq(counter.getCount(), initialCount + 1);
74+
}
75+
}
76+
```
77+
Run the tests with the following command:
78+
```shell
79+
forge test
80+
```
81+
If tests pass, deploy the contract to the Sei chain with the following command:
82+
```shell
83+
forge create --rpc-url $SEI_NODE_URI --mnemonic $MNEMONIC src/Counter.sol:Counter
84+
```
85+
Where `$SEI_NODE_URI` is the URI of the Sei node and `$MNEMONIC` is the mnemonic of the account that will deploy the
86+
contract.
87+
If you run local Sei node, the address will be `http://localhost:8545`.
88+
If deployment is successful, you will get the EVM contract address in the output.
89+
```shell
90+
[⠒] Compiling...
91+
No files changed, compilation skipped
92+
Deployer: $0X_DEPLOYER_ADDRESS
93+
Deployed to: $0X_CONTRACT_ADDRESS
94+
Transaction hash: $0X_TX_HASH
95+
96+
```
97+
Let's use the `cast` command to query the contract:
98+
```shell
99+
cast call $0X_CONTRACT_ADDRESS "getCount()(uint256)"
100+
```
101+
The command should return `0` as the initial value of the counter.
102+
103+
Now we can use the `cast` command to call the `increment` function:
104+
```shell
105+
cast send $0X_CONTRACT_ADDRESS "increment()" --mnemonic $MNEMONIC
106+
```
107+
If command is successful, you will get the transaction hash and other info back.
108+
109+
Now let's call the `getCount` function again and this case it should return `1`.
110+
111+
Let's deploy the CosmWasm contract that will call the EVM contract.
112+
113+
Follow the instructions in [this README](../../README.md) to deploy the test contract.
114+
115+
Once the contract is deployed, please note the contract address of the CosmWasm contract or export it as
116+
`$COSMWASM_CONTRACT_ADDRESS`.
117+
118+
We can now query EVM contract from the CosmWasm contract. But before, we need to get the correct inputs.
119+
To generate inputs, we can use the `seid` command line tool.
120+
121+
First we need to get the ABI of the EVM contract. It was generated by the foundry tooling and is usually located in the
122+
`out` folder. E.g. at path `out/Counter.sol/Counter.json`.
123+
124+
For our example the ABI looks like this:
125+
126+
```json
127+
[{"type":"function","name":"getCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"increment","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"number","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"setNumber","inputs":[{"name":"newNumber","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"}]
128+
```
129+
For convenience, we can save the ABI to a file, e.g. `abi.json`.
130+
131+
Now let's generate the inputs to invoke the `increcemnt` function with the `seid` command line tool.
132+
133+
```shell
134+
seid q evm payload abi.json increment
135+
```
136+
Command produces result like `d09de08a`. This is the hex encoded EVM contract function input.
137+
For our example though we need bash64 encoded bytes. So let's decode the hex to bytes and then base64encode them:
138+
```shell
139+
echo -n 'd09de08a' | xxd -r -p | base64
140+
```
141+
This should produce result like `0J3gig==`, that is base64 encoded instruction to invoke EVM function.
142+
143+
Let's call the `increment` function of the EVM contract from the CosmWasm contract.
144+
```shell
145+
seid tx wasm execute $COSMWASM_CONTRACT_ADDRESS '{"call_evm": {"value": "0", "to": "$0X_CONTRACT_ADDRESS", "data": "0J3gig==" }}' --from=admin --broadcast-mode=block --fees=200000usei --gas=3000000
146+
```
147+
148+
So now the counter should be set to `2`. Let's verify with the `cast` command first:
149+
```shell
150+
cast call $0X_CONTRACT_ADDRESS "getCount()(uint256)"
151+
```
152+
And now also with the `seid` command:
153+
```shell
154+
seid q evm payload out/abi.json getCount
155+
```
156+
```shell
157+
echo -n 'a87d942c' | xxd -r -p | base64
158+
```
159+
```shell
160+
seid q wasm contract-state smart $COSMWASM_CONTRACT_ADDRESS '{"static_call": {"from": "$SEI_ACCOUNT_ADDRESS", "to": "$0X_CONTRACT_ADDRESS", "data": "qH2ULA==" }}'
161+
```
162+
The above command should return output like this:
163+
```shell
164+
data:
165+
encoded_data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=
166+
```
167+
Let's decode it:
168+
```shell
169+
echo 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=' | base64 -D | od -t u1 -An -j 31
170+
```
171+
This should return `2` as well.

contracts/sei-tester/src/contract.rs

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use base64::{engine::general_purpose, Engine as _};
12
use cosmwasm_std::to_json_binary;
23
#[cfg(not(feature = "library"))]
34
use cosmwasm_std::{
@@ -18,7 +19,7 @@ use sei_cosmwasm::{
1819
ExchangeRatesResponse, GetLatestPriceResponse, GetOrderByIdResponse, GetOrdersResponse,
1920
Metadata, MsgPlaceOrdersResponse, OracleTwapsResponse, Order, OrderSimulationResponse,
2021
OrderType, PositionDirection, SeiAddressResponse, SeiMsg, SeiQuerier, SeiQueryWrapper,
21-
SettlementEntry, SudoMsg,
22+
SettlementEntry, StaticCallResponse, SudoMsg,
2223
};
2324

2425
const PLACE_ORDER_REPLY_ID: u64 = 1;
@@ -74,6 +75,7 @@ pub fn execute(
7475
test_occ_iterator_range(deps, env, info, start, end)
7576
}
7677
ExecuteMsg::TestOccParallelism { value } => test_occ_parallelism(deps, env, info, value),
78+
ExecuteMsg::CallEvm { value, to, data } => call_evm(value, to, data),
7779
}
7880
}
7981

@@ -135,6 +137,11 @@ fn test_occ_parallelism(
135137
.add_attribute("val", value.to_string()))
136138
}
137139

140+
fn call_evm(value: Uint128, to: String, data: String) -> Result<Response<SeiMsg>, StdError> {
141+
let call_evm = SeiMsg::CallEvm { value, to, data };
142+
Ok(Response::new().add_message(call_evm))
143+
}
144+
138145
pub fn place_orders(
139146
deps: DepsMut<SeiQueryWrapper>,
140147
_env: Env,
@@ -328,7 +335,7 @@ pub fn process_bulk_order_placements(
328335
Ok(val) => val,
329336
Err(error) => panic!("Problem parsing response: {:?}", error),
330337
};
331-
let base64_json_str = base64::encode(serialized_json);
338+
let base64_json_str = general_purpose::STANDARD.encode(serialized_json);
332339
let binary = match Binary::from_base64(base64_json_str.as_ref()) {
333340
Ok(val) => val,
334341
Err(error) => panic!("Problem converting binary for order request: {:?}", error),
@@ -437,6 +444,9 @@ pub fn query(deps: Deps<SeiQueryWrapper>, _env: Env, msg: QueryMsg) -> StdResult
437444
QueryMsg::GetDenomsFromCreator { creator } => {
438445
to_json_binary(&query_denoms_from_creator(deps, creator)?)
439446
}
447+
QueryMsg::StaticCall { from, to, data } => {
448+
to_json_binary(&query_static_call(deps, from, to, data)?)
449+
}
440450
QueryMsg::GetEvmAddressBySeiAddress { sei_address } => {
441451
to_json_binary(&query_evm_address(deps, sei_address)?)
442452
}
@@ -557,6 +567,19 @@ pub fn query_denoms_from_creator(
557567
Ok(res)
558568
}
559569

570+
pub fn query_static_call(
571+
deps: Deps<SeiQueryWrapper>,
572+
from: String,
573+
to: String,
574+
data: String,
575+
) -> StdResult<StaticCallResponse> {
576+
let valid_from_addr = deps.api.addr_validate(&from)?;
577+
let querier = SeiQuerier::new(&deps.querier);
578+
let res: StaticCallResponse = querier.static_call(valid_from_addr.to_string(), to, data)?;
579+
580+
Ok(res)
581+
}
582+
560583
pub fn query_evm_address(
561584
deps: Deps<SeiQueryWrapper>,
562585
sei_address: String,

contracts/sei-tester/src/msg.rs

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use cosmwasm_std::Uint128;
12
use schemars::JsonSchema;
23
use sei_cosmwasm::Order;
34
use serde::{Deserialize, Serialize};
@@ -9,15 +10,29 @@ pub struct InstantiateMsg {}
910
#[serde(rename_all = "snake_case")]
1011
pub enum ExecuteMsg {
1112
PlaceOrders {},
12-
CancelOrders { order_ids: Vec<u64> },
13+
CancelOrders {
14+
order_ids: Vec<u64>,
15+
},
1316
CreateDenom {},
1417
Mint {},
1518
Burn {},
1619
ChangeAdmin {},
1720
SetMetadata {},
18-
TestOccIteratorWrite { values: Vec<(u64, u64)> },
19-
TestOccIteratorRange { start: u64, end: u64 },
20-
TestOccParallelism { value: u64 },
21+
TestOccIteratorWrite {
22+
values: Vec<(u64, u64)>,
23+
},
24+
TestOccIteratorRange {
25+
start: u64,
26+
end: u64,
27+
},
28+
TestOccParallelism {
29+
value: u64,
30+
},
31+
CallEvm {
32+
value: Uint128,
33+
to: String,
34+
data: String,
35+
},
2136
}
2237

2338
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -57,6 +72,11 @@ pub enum QueryMsg {
5772
GetDenomsFromCreator {
5873
creator: String,
5974
},
75+
StaticCall {
76+
from: String,
77+
to: String,
78+
data: String,
79+
},
6080
GetEvmAddressBySeiAddress {
6181
sei_address: String,
6282
},

contracts/sei-tester/tests/sei_tester_integration_tests.rs

+26
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use base64::{engine::general_purpose, Engine as _};
12
use cosmwasm_std::{
23
coin, from_json,
34
testing::{MockApi, MockStorage},
@@ -15,6 +16,7 @@ use sei_cosmwasm::{
1516
EvmAddressResponse, ExchangeRatesResponse, GetOrderByIdResponse, GetOrdersResponse,
1617
OracleExchangeRate, OracleTwapsResponse, Order, OrderSimulationResponse, OrderStatus,
1718
OrderType, PositionDirection, SeiAddressResponse, SeiMsg, SeiQuery, SeiQueryWrapper, SeiRoute,
19+
StaticCallResponse,
1820
};
1921
use sei_integration_tests::{
2022
helper::{get_balance, mock_app},
@@ -934,6 +936,30 @@ fn test_dex_module_query_dex_twap() {
934936
}
935937

936938
/// EVM Module - query EVM address
939+
940+
#[test]
941+
fn test_static_call_query() {
942+
let mut app = mock_app(init_default_balances, vec![]);
943+
let sei_tester_addr = setup_test(&mut app);
944+
945+
let res: StaticCallResponse = app
946+
.wrap()
947+
.query_wasm_smart(
948+
sei_tester_addr.clone(),
949+
&QueryMsg::StaticCall {
950+
from: SEI_ADDRESS.to_string(),
951+
to: EVM_ADDRESS.to_string(),
952+
data: "".to_string(),
953+
},
954+
)
955+
.unwrap();
956+
957+
let expected_res = StaticCallResponse {
958+
encoded_data: general_purpose::STANDARD.encode(b"static call response"),
959+
};
960+
assert_eq!(res, expected_res);
961+
}
962+
937963
#[test]
938964
fn test_evm_address_query() {
939965
let mut app = mock_app(init_default_balances, vec![]);

packages/sei-cosmwasm/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sei-cosmwasm"
3-
version = "0.4.14"
3+
version = "0.4.15"
44
edition = "2021"
55
description = "Bindings and helpers for cosmwasm contracts to interact with sei blockchain"
66
license = "Apache-2.0"

packages/sei-cosmwasm/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Add the sei-cosmwasm dependency to your smart contract's `Cargo.toml` file:
88

99
```toml
1010
[dependencies]
11-
sei-cosmwasm = { version = "0.4.14" }
11+
sei-cosmwasm = { version = "0.4.15" }
1212
```
1313

1414
## Functionality

packages/sei-cosmwasm/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub use query::{
1515
DenomAuthorityMetadataResponse, DenomsFromCreatorResponse, DexTwapsResponse, EpochResponse,
1616
EvmAddressResponse, ExchangeRatesResponse, GetLatestPriceResponse, GetOrderByIdResponse,
1717
GetOrdersResponse, OracleTwapsResponse, OrderSimulationResponse, PriceResponse,
18-
SeiAddressResponse, SeiQuery, SeiQueryWrapper,
18+
SeiAddressResponse, SeiQuery, SeiQueryWrapper, StaticCallResponse,
1919
};
2020
pub use route::SeiRoute;
2121
pub use sei_types::{

packages/sei-cosmwasm/src/msg.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub enum SeiMsg {
5858
/// The EVM (Solidity) contract `msg.sender` in this case will be the 32-byte long
5959
/// [`cosmwasm_std::CanonicalAddr`] of this contract.
6060
CallEvm {
61-
/// The amount to send along with the transaction
61+
/// The amount to send along with the transaction. 0 if non-payable function is called.
6262
value: Uint128,
6363
/// The address of the EVM contract to call
6464
to: String,

packages/sei-cosmwasm/src/query.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ pub struct DenomsFromCreatorResponse {
272272
pub struct StaticCallResponse {
273273
/// The result of the static call to the EVM contract. It's represented as a base64 encoded
274274
/// string.
275-
pub data: String, // base64
275+
pub encoded_data: String, // base64
276276
}
277277

278278
/// `ErcPayloadResponse` is a struct that represents a response containing the encoded payload for

0 commit comments

Comments
 (0)