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.
[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.
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.
#[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
.
#[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:
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:
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.
#[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:
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:
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:
#[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
.
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.
#[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.
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.
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.
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
.
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!