Skip to main content

Solidity to CosmWasm

In this section

We will walk you through porting a simple Solidity smart contract to get you started on the concepts using the Safe Remote Purchase smart contract from the official Solidity documentation.

Source Solidity Contract

The original contract is designed as an example of a safe way to sell something in a trustless manner to anyone. It has its own state variables, public methods, modifiers and events. In this example, both parties, seller and buyer, have to put twice the value of the item into the contract as escrow. Once the buyer confirms that they have received the item, they will receive half of their original deposit, and the seller will get three times the value (their deposit plus the value of te item). While this does not solve everything, it works as a good example of utility.

State Variables

The state variables include: seller, buyer, value and state. These will maintain its state in the contract unless modified. The first three are self-explanatory, but the last one can be a bit confusing. The state variable in the Solidity contract has an ambiguous name, since it is a state variable named "state". Nevertheless, it simply represents the current status of the contract i.e. Created, Locked, Release and Inactive. These helps us keep track of what is going on.

Methods

For write methods we have: constructor, abort, confirmPurchase, confirmReceived and refundSeller. These are the main entry points of the contract and contain the logic for the transaction. Since this is a very simple contract, not much is required, and in fact, no arguments are used for any of the public methods.

Of course we also have our read methods which are created automatically as well when we declare our public state variables.

Events and errors

The events we have are Aborted, PurchaseConfirmed, ItemReceived and SellerRefunded. As for errors, we have OnlyBuyer, OnlySeller, InvalidState and ValueNotEven.

These are all empty logs since none of them contain any properties.

Modifiers

Finally we have our modifiers. To keep this example simple, we will be using a simple condition on each required instance, since none of them are really required on this example.

Key Differences

Normally when working with Solidity, you work on a single .sol file where you define everything: state variables, constants, modifiers, events, errors, interfaces, methods, etc. This can be a little messy and the usual workaround is having separate files according to your needs.

That workaround is similar to the CosmWasm structure. You start with a set of Rust .rs files, one for each specific purpose and can branch out if it helps readability and organization. The standard CosmWasm contract contains contract.rs, which is where the core logic and methods are. Then there is the state.rs file, which contains all your state variables. The lib.rs file which contains additional logic. The error.rs file which contains all your error handling. And finally the msg.rs file, which contains the structure of your arguments for execution or queries on methods.

It may sound a bit complicated at first, but it becomes second nature really quickly and results in much easier to read and analyze contracts when compared to a large Solidity project.

Creating CosmWasm Contract

We can create a new CosmWasm contract from a template easily so we can start writing code right away. To do so, go to your project desired directory and run the following:

cargo generate --git https://github.com/CosmWasm/cw-template.git --name safe-remote-purchase

This will create the bolierplate of a CosmWasm contract. Before beginning, we should add the following dependency to our Cargo.toml file, as we will be using it later.

Cargo.toml
[dependencies]
cw-utils = "0.13"

Have a look around so you can get familiar with the structure and on the next section we will begin by declaring our state variables.

State Variables

Let's begin with the state variables. In the Solidity contract, we have only four state variables to keep track of the purchase shown below:

uint public value;
address payable public seller;
address payable public buyer;

enum State { Created, Locked, Release, Inactive }
// The state variable has a default value of the first member, `State.created`
State public state;

Now we need to create our state variables on the CosmWasm contract. To do so, you can choose to do it in several ways. In this case I will use a struct to keep it simple.

Starting with value, we can store it as a u128 so we just keep track of the amount. Remember that the variable value in the context of this contract represents the value of the item, not the value of any transfer or current holdings of the contract.

Both seller and buyer will be Addr type of course, and for the original state variable used in the Solidity contract, let's use status instead, so it's clearer.

Finally, for status we will use an enum type just as the original.

So, first let's import all we will need into the state.rs file: Addr type, cw_serde and Item.

src/state.rs
use cosmwasm_std::Addr; // address type
use cosmwasm_schema::cw_serde; // attribute macro to (de)serialize and make schemas
use cw_storage_plus::Item; // analog of Singletons for storage

Then let's define the status attribute in the struct we will use for keeping our state variables. This simply defines what stage of the purchase the contract is in.

src/state.rs
#[cw_serde]
pub enum Status {
Created,
Locked,
Release,
Inactive,
}

Now let's define the core state variable of the contract. I will name it ContractState to keep it clear. This contains all of our variables, value, seller, buyer and status.

src/state.rs
#[cw_serde]
pub struct ContractState {
pub value: u128,
pub seller: Addr,
pub buyer: Addr,
pub status: Status,
}

As you can see, the type of the status property in ContractState is Status, the enum we defined earlier.

So, now we have a struct named ContractState which we will be accessing, however, a struct is not an object we can read and write to the state as it is, since the state is accessed via deps (will talk about it more later), which is why we need to define it as an constant item as such:

src/state.rs
pub const CONTRACT_STATE: Item<ContractState> = Item::new("contract_state");

The declaration const can be a bit tricky here. The constant is not keeping the state itself, as it is stored in the blockchain, but it is not a constant value, like the tag would suggest. Instead, it merely means that it is a contant key to access a variable, since state storage in this context works like a big dictionary of key-stored values. In this case, we store a new instance of an Item of ContractState to "contract_state".

And just like that, we have defined the state in a simple to access variable for easy referencing. Let's continue to the next part.

Events and Errors

In the Solidity source contract we have Events and Errors declared for the common errors we expect and the events on any transactions for proper logging to the blockchain. This is what they look like:

... 
/// Only the buyer can call this function.
error OnlyBuyer();
/// Only the seller can call this function.
error OnlySeller();
/// The function cannot be called at the current state.
error InvalidState();
/// The provided value has to be even.
error ValueNotEven();

...

event Aborted();
event PurchaseConfirmed();
event ItemReceived();
event SellerRefunded();
...

In CosmWasm we will use error.rs to declare our errors, but we will not be using events in the same way. To handle events in CosmWasm, we will emit them straight on the methods that require them as Response.

First of course, we import our dependencies:

src/error.rs
use cosmwasm_std::StdError;
use cw_utils::PaymentError;
use thiserror::Error;

Both StdError and Error are common imports, and we use PaymentError for a utility we will use for handling the value in the contract methods. We could directly specify PaymentError later on the methods itself, but it's cleaner to implement it on our default ContractError object.

src/error.rs
#[derive(Error, Debug)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),

#[error("PaymentError")]
PaymentError(PaymentError),

#[error("Only the buyer can call this function")]
OnlyBuyer,

#[error("Only the seller can call this function")]
OnlySeller,

#[error("The function cannot be called at the current state")]
InvalidState,

#[error("The provided value has to be even")]
ValueNotEven,
}

impl From<PaymentError> for ContractError {
fn from(err: PaymentError) -> Self {
ContractError::PaymentError(err)
}
}

That is all we need to do on the error.rs file. By declaing our errors in this manner, we can specify the return types on our functions using this object only.

Messages

In Solidity, our public methods are exposed just by declaring the functions as public, but with CosmWasm, we need to declare the message structure and what kind of message it will be, and those will act as the declaration of all entry points. You can think of this as the call signature in Solidity.

There are three main types of messages: instantiate, execute, and query. There are of course other different types of messages such as sudo or migrate, but we will not talk about them here since it's not relevant for the example.

In this context we will only be using the main three, so let's explain what thay are:

  • instantiate - This is what you would know as a constructor in Solidity. It can only be called once at initialization.
  • execute - Execute messages are the equivalent of a write function in Solidity. They can modify the contract state and is what we use to perform any actions.
  • query - Just as the name says, this will be messages requesting data from the contract state. They are read only and cannot modify the contract state.

For our case, this is quite simple, since we have very little methods and queries. This is what our messages should look like:

src/msg.rs
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::Addr;

#[cw_serde]
pub enum ExecuteMsg {
ConfirmPurchase {},
ConfirmReceived {},
RefundSeller {},
AbortPurchase {},
}

#[cw_serde]
pub struct StateResponse {
pub state: ContractState,
}

#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
#[returns(StateResponse)]
GetState {}
}

#[cw_serde]
pub struct InstantiateMsg {
}

You can see we have all three message types, but we also have a struct named StateResponse. This struct will be used as the return type of the only query we will use: get_state so we need to specify it as a QueryResponse.

Let's continue to the final section for building the contract.

Methods

Now comes the longest part, the public methods of the contract. These will contain the logic behind the messages we declared earlier.

Here we will need to import some common imports as well as our messages, errors and state variables that we declared earlier to be able to use them. We will go on each method one by one to fully understand everything done.

First step as always is importing our dependencies. In this case, we will be using the following:

src/contract.rs
use cosmwasm_std::entry_point;  // used to specify entry points of our contract
use cw_utils::must_pay; // this utility helps us with handling values we expect
// mostly standard imports
use cosmwasm_std::{coins, to_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, StdError};
// these below are our messages, errors and state we defined earlier
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{CONTRACT_STATE, ContractState, Status};

Let's now work on the InstantiateMsg.

Instantiate

The instantiate is the message we will use to call for instantiating a stored contract in the blockchain. Unlike EVMs, there are two steps if you wanna deploy a new contract.

First you need to deploy it or store the bytecode in the blockchain. This is a type of transaction called store, in which we will only push the bytecode of our contract to the blockchain. This will have an ID which we will use to execute the next transaction: instantiate. The advantage of doing this is that you can create multiple contracts from the same bytecode.

Anyway, let's get back to the InstantiateMsg. In the Solidity version we don't take any arguments, so we will do the same. The seller will be the one instantiating the contract and the value will be the coin value sent to the contract.

First let's get into how an entry point function is defined:

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
//
}

The first line is an attribute macro to mark the entry point of a contract for instantiate messages. We will use one on each type of message of course, but not on all functions. That is for the entry_point, but as for the cfg_attr(not(feature = "library"), ...) part, it is a conditional attribute that tells the Rust compiler to include this function in the binary output of the contract, but only if the library feature is not enabled.

The arguments a function takes of course depends on what will be used, however, when marking entry_points we need to use deps, env, info and msg. Even if not being used, which in that case, you should only prepend it with an underscore "_".

Now that that part is clear, here is the complete function:

src/contract.rs
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
_msg: InstantiateMsg,
) -> Result<Response, ContractError> {

// we speficy we are expecting only coins of denomination ueaura and
// this returns a u128 value
let value = must_pay(&info, "ueaura")?.u128();

// we make sure the value is divisible by 2 as in the original contract
if value % 2u128 != 0u128 {
// return the error ValueNotEven we declared if not divisible by 2
return Err(ContractError::ValueNotEven);
}

// declare the initial state
let state = ContractState {
value: value / 2u128, // value of the item will be half the funds for escrow
seller: info.sender, // this is equivaluent to msg.sender in Solidity,
// but with info here, msg is used for the arguments of the tx
buyer: Addr::unchecked("aura0"), // no buyer yet
status: Status::Created, // Created status
};

// save to state the ContractState variable we just declared
CONTRACT_STATE.save(deps.storage, &state)?;

// we return no events, just an empty Response
Ok(Response::default())
}

The instantiate method works just as the original Solidity contract, checking that the value is even (only accepting ETH or in this case EAURA (since we're working on Aura Euphoria test network), and storing the message sender as the seller.

note

We are using ueaura here instead of eaura as coin denomination since ueaura is the smallest unit (10^-6). Therefore, all transactions will expect ueaura.

Easy, right? Alright now for the execute part.

Execute

Here we will use our previously declared execute type messages. There will be a single entry point and one function for each one in the original Solidity contract.

ExecuteMsg Entry Point

For execute, let's first declare our entry_point. In here we will only redirect to the matching ExecuteMsg to the corresponding handler function. And to that function, we will pass the contract state as an argument, so that we don't have to call for it at all functions individually. We also need to pass the required arguments to each, whether it will be deps, env, info or msg.

For this contract, we do not take any input arguments for any callable execute function, so we only use the msg argument to match the current messge to the correct function.

src/contract.rs
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
// load the state from deps
let state = CONTRACT_STATE.load(deps.storage)?.clone();
// match the received msg to the handler function
match msg {
ExecuteMsg::ConfirmPurchase {} => confirm_purchase(deps, info, state),
ExecuteMsg::ConfirmReceived {} => confirm_received(deps, info, state),
ExecuteMsg::RefundSeller {} => refund_seller(deps, info, state),
ExecuteMsg::AbortPurchase {} => abort_purchase(deps, env, info, state),
}
}

This single entry point for execution type messages makes it much more readable and easy to audit.

Confirm Purchase

Now let's check out each function. Starting with confirm_purchase, we don't need to do much here but to handle the funds sent to the contract and verify it's correct. Anyone can call this function, so first come first serve.

src/contract.rs
pub fn confirm_purchase(
deps: DepsMut,
info: MessageInfo,
mut state: ContractState,
) -> Result<Response, ContractError> {
// emit an error if the contract is not in the Created state
if state.status != Status::Created {
return Err(ContractError::InvalidState);
}
// ensure the funds are correct or return an error
let value = must_pay(&info, "ueaura")?.u128();
// buyer must pay twice the value for escrow
if value != state.value * 2u128 {
return Err(ContractError::Std(StdError::generic_err("Incorrect funds sent")));
}

// modify the status to Locked once the requirements are met
state.status = Status::Locked;
// set the seller to the caller
state.buyer = info.sender;
// save the state of the contract
CONTRACT_STATE.save(deps.storage, &state)?;

// emit the current status and the action performed
Ok(
Response::new()
.add_attribute("action", "purchase_confirmed")
.add_attribute("status", "Locked")
)
}

As you can see, pretty straight forward. Same behaviour as the original contract. The event is emitted as an attribute of the Response with the key status. A bit different than Solidity but more convenient if you ask me.

Confirm Received

Now let's work on the next function: confirm_received. This is where only the buyer can confirm the item has been received and the purchase can be completed, and it can only be called if the current status of the contract is Locked.

After all checks have passed, we can then return item value back to the buyer, keeping the other half as payment of course.

src/contract.rs
pub fn confirm_received(
deps: DepsMut,
info: MessageInfo,
mut state: ContractState,
) -> Result<Response, ContractError> {
// emit an error if caller is not buyer
if state.buyer != info.sender {
return Err(ContractError::OnlyBuyer);
}
// emit an error if contract is not in the Locked state
if state.status != Status::Locked {
return Err(ContractError::InvalidState);
}

// modify the status to Release once the requirements are met
state.status = Status::Release;
// save the state of the contract
CONTRACT_STATE.save(deps.storage, &state)?;

// refund the value of the item to the buyer
let response = BankMsg::Send {
to_address: state.buyer.to_string(),
amount: coins(state.value, "ueaura"),
};

// emit the current status and the action performed, as well as
// the response of the BankMsg call
Ok(
Response::new()
.add_message(response)
.add_attribute("action", "item_received")
.add_attribute("status", "Release")
)
}

Here we use BankMsg::Send to send the value of the item back to the buyer, since the purchase is now complete. The only step left for the contract to be finalized in the Inactive status is for the seller to call the method refund_seller to collect their original funds as well as the funds the buyer sent for the value of the item.

Then let's continue to that. We're almost done!

Refund Seller

To completely finalize the contract we just need to allow the seller to claim the funds of the contract which should include their original deposit plus the payment for the item. After that, we are done!

First we need to check that the current status of the contract mathces and allow only the seller to call this function. After that we just modify and save the state and send the funds to the seller and that's it.

src/contract.rs
pub fn refund_seller(
deps: DepsMut,
info: MessageInfo,
mut state: ContractState,
) -> Result<Response, ContractError> {
// emit an error if contract is not in the Release state
if state.status != Status::Release {
return Err(ContractError::InvalidState);
}
// emit an error if caller is not seller
if info.sender != state.seller {
return Err(ContractError::OnlySeller);
}

// modify the state.status to Inactive
state.status = Status::Inactive;
// save the contract state
CONTRACT_STATE.save(deps.storage, &state)?;

// send three times the value of the item to the seller
let response = BankMsg::Send {
to_address: state.seller.to_string(),
amount: coins(state.value * 3u128, "ueaura"),
};

// emit the current status and the aciton performed as well as
// the reponse of the BankMsg call
Ok(
Response::new()
.add_message(response)
.add_attribute("action", "seller_refunded")
.add_attribute("status", "Inactive")
)
}

Once the seller successfully calls this, the contract will be in the Inactive state and it will be finished.

Abort Purchase

For the final function, abort_purchase, we need to send the funds back to the seller for their original deposit.

This is a simple function which just verifies the caller and status of the contract once again, and terminates the contract by setting the status to Inactive.

src/contract.rs
fn abort_purchase(
deps: DepsMut,
env: Env,
info: MessageInfo,
mut state: ContractState
) -> Result<Response, ContractError> {
// emit an error if the contract state is not Created
if state.status != Status::Created {
return Err(ContractError::InvalidState);
}
// emit an error if caller is not seller
if info.sender != state.seller {
return Err(ContractError::OnlySeller);
}

// terminate the contract by setting the state to inactive and saving
state.status = Status::Inactive;
CONTRACT_STATE.save(deps.storage, &state)?;

// query the whole balance of the contract and send to seller
let balance = deps.querier.query_balance(env.contract.address, "ueaura")?;
let response = BankMsg::Send {
to_address: state.seller.to_string(),
amount: coins(balance.amount.u128(), "ueaura"),
};

// emit the current status and the aciton performed as well as
// the reponse of the BankMsg call
Ok(
Response::new()
.add_message(response)
.add_attribute("action", "aborted")
.add_attribute("status", "Inactive")
)
}

And that's it! Our contract is finished. We could also add tests on it to ensure everything works as it should, just as any properly built code, but we will skip that for this tutorial.

There is one more step we need to take care of in the contract.rs file: queries.

Query

In the original Solidity contract we don't explicitly delcare any read methods, but when declaring a public state variable, the compiler automatically creates one for it. This does not happen in CosmWasm, we need to explicitly declare our queries.

In this case we only need one, since we declared the state of the contract as a single struct. This makes it much more easier for both readability of code and usage.

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::GetState {} => to_binary(&CONTRACT_STATE.load(deps.storage)?),
}
}

We only need to match the query type message to one that matches our QueryMsg we declared and return the state of the contract in it.

Can't get any simpler than that, right? Now we are completely done! Our contract is ready for compiling and storing it in the blockchain to instantiate it.

Deploying

To deploy our contract we need to compile it first. To do so, you can run the following in your source dir:

rustup default stable
RUSTFLAGS='-C link-arg=-s' cargo wasm

After it's complete, we can execute a store transaction. To do so, you can run:

RES=$(aurad tx wasm store ./target/wasm32-unknown-unknown/release/safe-remote-purchase.wasm --from wallet $TXFLAG --output json)

Replacing of course "YOUR_WALLET_NAME" with your actual wallet name you crated in aurad. You can run echo $RES to view the transaction hash. If you query that, you should see the contract CODE_ID.

Then, once the bytecode is stored, we can instantiate the contract! To do so, we must execute an instantiate transaction with the CODE_ID of your stored contract bytecode, and with an empty JSON object {} since there are no arguments in our InstantiateMsg, however we do need to specify the funds to be sent to it and a label to easily identify.

You run it like shown below:

aurad tx wasm instantiate CODE_ID {} --from wallet --label "safe-remote-purchase" --no-admin --amount 4000ueaura

Join us

If you have any questions related to Aura or simply would like to chat with us, come join us in the Aura Discord server or any of our official channels. We love dev chatter!

Telegram
Telegram
@AuraNetwork