[Cosmwasm] Cosmwasm은 처음이지?(1) - 구성요소 알아보기

[Cosmwasm] Cosmwasm은 처음이지?(1) -  구성요소 알아보기
pixabay.com

개발 경험이 있으나 Rust에 익숙하지 않은 사람들을 대상으로 작성된 글입니다.

[요약]

  • 보통 Cosmwasm 스마트 컨트랙트는 contract 모듈, msg 모듈, state 모듈로 이루어져 있다.
  • contract 모듈이 스마트 컨트랙트의 핵심 로직을 담당하며 나머지 두 모듈은 contract 모듈을 보조하는 역할이다.
  • contract 모듈은 무조건 instantiate(), execute(), query() 함수를 정의해야 하며 이 세 함수는 entry_point attribute를 가지고 있다.

[목차]

  1. 러스트의 크레이트 및 모듈 개념
  2. Cosmwasm의 컨트랙트 구성 요소
  • instantiate()
  • execute()
  • query()

러스트의 크레이트

러스트에서 크레이트는 하나의 바이너리 혹은 라이브러리이다. 그리고 패키지는 하나 이상의 크레이트로 구성된다. 러스트 패키지의 Cargo.toml은 크레이트를 빌드하는 방법을 서술하는 파일이다.

cargo new {project_name} --bin // 바이너리를 생성
cargo new {project_name} --lib // 라이브러리를 생성

바이너리 크레이트의 경우 src/main.rs를 가지고 있고, 라이브러리 크레이트는 src/lib.rs를 가지고 있다. 이 두 파일을 크레이트 루트라고 하며 라이브러리나 바이너리를 빌드할 때 rustc 컴파일러에게 크레이트 루트 파일을 전달한다.

모듈

cargo를 통해 라이브러리를 생성하면, src/lib.rs에서 모듈과 함수 시그니처를 정의한다.

러스트의 대표적인 비동기 프로그래밍 프레임워크인 tokio를 예시로 살펴보자.

.
├── CHANGELOG.md
├── Cargo.toml
├── LICENSE
├── README.md
├── build.rs
├── docs
│   └── reactor-refactor.md
├── external-types.toml
├── src
│   ├── blocking.rs
│   ├── doc
│   ├── fs
│   ├── future
│   ├── io
│   ├── lib.rs
│   ├── loom
│   ├── macros
│   ├── net
│   ├── process
│   ├── runtime
│   ├── signal
│   ├── sync
│   ├── task
│   ├── time
│   └── util
└── tests
tokio의 디렉토리 구조

tokio를 살펴보면 src/lib.rs가 존재하고 doc, fs, future, io 등 여러 디렉토리가 있다.

...
// Includes re-exports used by macros.
//
// This module is not intended to be part of the public API. In general, any
// `doc(hidden)` code is not part of Tokio's public and stable API.
#[macro_use]
#[doc(hidden)]
pub mod macros;

cfg_fs! {
    pub mod fs;
}

mod future;

pub mod io;
pub mod net;

mod loom;
...
src/lib.rs

lib.rs 에는 각 디렉토리 이름으로 mod가 정의된 것을 확인할 수 있다. 이제 각 디렉토리 안에 있는 코드들을 모듈로서 사용을 할 수 있게 된 것이다. pub 키워드는 모듈을 라이브러리 크레이트의 공개 API로 사용할 수 있게 해준다.

Cosmwasm 컨트랙트 살펴보기

GitHub - deus-labs/cw-contracts: Example contracts for using CosmWasm
Example contracts for using CosmWasm. Contribute to deus-labs/cw-contracts development by creating an account on GitHub.
Example contracts for using Cosmwasm

예시 컨트랙트인 cw20-pot 컨트랙트를 보면서 CosmWasm을 이해해보자.

cw20-pot 컨트랙트는 cw20 토큰을 모으는 pot을 생성하는 컨트랙트이다. 해당 pot에 코인이 어느 정도 모이면, pot에 적혀진 타켓 주소로 해당 pot의 코인들을 전부 보낸다.

./
├── Cargo.lock
├── Cargo.toml
├── Developing.md
├── Importing.md
├── LICENSE
├── NOTICE
├── Publishing.md
├── README.md
├── examples
│   └── schema.rs
├── rustfmt.toml
├── schema
│   └── cw20-pot.json
├── src
│   ├── contract.rs
│   ├── error.rs
│   ├── lib.rs
│   ├── msg.rs
│   └── state.rs
└── target
    ├── CACHEDIR.TAG
    ├── debug
    └── rls
cw20-pot 디렉토리 구조
pub mod contract;
mod error;
pub mod msg;
pub mod state;

pub use crate::error::ContractError;
src/lib.rs

lib.rs에서 contract, msg, state를 퍼블릭 모듈로 노출시키는 것을 알 수 있다. ContractError는 pub use 키워드가 붙어 있는데, 이는 error 모듈을 외부에 노출시키지 않으면서도 외부에 error 모듈의 ContractError을 사용할 수 있게 해준다.

  • contract: 컨트랙트의 로직을 정의하는 모듈
  • msg: 컨트랙트에 트랜잭션을 보낼 때 담길 메시지를 정의하는 모듈
  • state: 컨트랙트의 state를 정의하는 모듈

msg와 state는 contract 모듈을 보조하는 모듈이다. contract 모듈을 살펴보면 컨트랙트가 어떻게 작동하는지 이해할 수 있다.

contract 모듈 살펴보기

Cosmwasm docs에 따르면 스마트 컨트랙트의 인터페이스는 3개의 함수로 구성이 되어 있다.

  • instantiate(): 생성자로써 컨트랙트 초기화와 초기 상태 구성
  • execute(): 사용자가 스마트 컨트랙트의 메소드가 호출할 때 호출이 되는 함수
  • query(): 사용자가 스마트 컨트랙트의 상태를 가져올 때 호출 되는 함수

cosmwasm 컨트랙트는 이더리움과 다르게 컨트랙트 코드를 체인에 배포하면 생성이 되는 것이 아니다. 코드를 업로드한 이후 초기화 해주는 트랜잭션을 보내야 비로소 컨트랙트가 인스턴스로서 존재하게 된다. 따라서 하나의 코드 베이스를 사용해도 다른 파라미터를 가진 트랜잭션들로 여러 인스턴스를 생성할 수 있다.

instantiate()

컨트랙트의 초기 상태를 만들어 준다.

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

1) Attribute

함수 위에 작성되어 있는 #[cfg_attr ~ ]을 attribute라고 한다.

cfg_attr은 컴파일이 되는 조건을 명시하고, 만족하면 뒤에 나오는 성질들을 적용한다. 즉, instantiate는 1) library feature가 없다면 2) entry_point 가 적용되어 컴파일이 된다.

library feature는 Cargo.toml에 작성이 되어 있다. library에 아무 라이브러리도 없기 때문에 instantiate가 컴파일이 될 것이라는 것을 예상할 수 있다.

...
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []

...
Cargo.toml

entry_point는 cosmwasm-std의 일부인 cosmwasm-derive에서 정의된 매크로이다. Wasm module로 진입할 수 있게 해주며 instantiate, execute, query 함수에 적용해야 한다.

2) 파라미터

DepsMut

pub struct DepsMut<'a, C: CustomQuery = Empty> {
    pub storage: &'a mut dyn Storage,
    pub api: &'a dyn Api,
    pub querier: QuerierWrapper<'a, C>,
}
cosmwasm-std/src/deps.rs

DepsMut는 컨트랙트가 읽고 쓸 수 있는 영구 저장소인 storage와 Wasm 모듈 밖에서 구현된 시스템 함수에 대한 콜백인 api, 쿼리 요청을 받아 처리하고 응답을 파싱해주는 querier로 구성되어 있습니다.

Env

pub struct Env {
    pub block: BlockInfo,
    pub transaction: Option<TransactionInfo>,
    pub contract: ContractInfo,
}
cosmwasm-std/src/typs.rs

Env는 블록 정보가 담긴 BlockInfo, 블록 안의 트랜잭션 인덱스를 가지고 있는 TransactionInfo, 컨트랙트 주소를 가지고 있는 ContractInfo로 이루어져 있습니다.

MessageInfo


#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct MessageInfo {
    pub sender: Addr,
    pub funds: Vec<Coin>,
}
cosmwasm-std/src/types.rs

MessageInfo는 Sender 주소와 컨트랙트에 보낸 코인에 대한 정보가 들어 있습니다.

InstantiateMsg

#[cw_serde]
pub struct InstantiateMsg {
    pub admin: Option<String>,
    /// cw20_addr is the address of the allowed cw20 token
    pub cw20_addr: String,
}
src/msg.rs

InstantiateMsg는 다른 타입들과 다르게 컨트랙트 crate의 모듈에 정의되어 있습니다. InstantiateMsg는 컨트랙트를 초기화할 때 컨트랙트에 제공할 데이터들을 정의하고 있기 때문에, 컨트랙트를 작성하는 개발자가 타입을 정의해야 합니다. [cw_serde] attribute를 통해 serialize와 deserialize를 가능하게 한 것이 눈에 띕니다.

3) instantiate() 로직

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
	// (1)
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

	// (2)
    let owner = msg
        .admin
        .and_then(|s| deps.api.addr_validate(s.as_str()).ok())
        .unwrap_or(info.sender);
    let config = Config {
        owner: owner.clone(),
        cw20_addr: deps.api.addr_validate(msg.cw20_addr.as_str())?,
    };
    CONFIG.save(deps.storage, &config)?;

    // init pot sequence
    POT_SEQ.save(deps.storage, &0u64)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("owner", owner)
        .add_attribute("cw20_addr", msg.cw20_addr))
}
src/contract.rs

(1) : contract의 storage에 컨트랙트의 이름과 버전을 저장한다.

(2) : insantiateMsg에 담겨있는 admin의 주소를 검증하고 owner로 정의한다. owner와 cw20의 주소를 config 형태로 storage에 저장한다.

execute()

cw20-pot 컨트랙트에서 핵심 로직을 담당한다.

1) pot을 생성하는 로직

2) pot에 코인을 모으고, 전송하는 로직

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::CreatePot {
            target_addr,
            threshold,
        } => execute_create_pot(deps, info, target_addr, threshold),
        ExecuteMsg::Receive(msg) => execute_receive(deps, info, msg),
    }
}
src/contract.rs

ExecuteMsg는 enum으로 CreatePot과 Receive 두 가지가 해당된다.

pub fn execute_create_pot(
    deps: DepsMut,
    info: MessageInfo,
    target_addr: String,
    threshold: Uint128,
) -> Result<Response, ContractError> {
    // owner authentication
    let config = CONFIG.load(deps.storage)?;
    if config.owner != info.sender {
        return Err(ContractError::Unauthorized {});
    }
    // create and save pot
    let pot = Pot {
        target_addr: deps.api.addr_validate(target_addr.as_str())?,
        threshold,
        collected: Uint128::zero(),
    };
    save_pot(deps, &pot)?;

    Ok(Response::new()
        .add_attribute("action", "execute_create_pot")
        .add_attribute("target_addr", target_addr)
        .add_attribute("threshold_amount", threshold))
}

pub fn execute_receive(
    deps: DepsMut,
    info: MessageInfo,
    wrapped: Cw20ReceiveMsg,
) -> Result<Response, ContractError> {
    // cw20 address authentication
    let config = CONFIG.load(deps.storage)?;
    if config.cw20_addr != info.sender {
        return Err(ContractError::Unauthorized {});
    }

    let msg: ReceiveMsg = from_binary(&wrapped.msg)?;
    match msg {
        ReceiveMsg::Send { id } => receive_send(deps, id, wrapped.amount, info.sender),
    }
}

ExecuteMsg가 CreatePot이면 타겟 주소가 정의된 Pot을 만들고 스토리지에 저장한다.

ExecuteMsg가 Receive면 receive_send를 실행시키는데, pot의 id로 저장된 pot을 찾아 wrapped.amount만큼 pot에 저장된 코인을 늘린다. 이때 pot의 threshold보다 pot의 저장된 코인이 많다면 타겟 주소에 pot에 저장된 코인을 보낸다. 해당 로직을 살펴보자.

pub fn receive_send(
    deps: DepsMut,
    pot_id: Uint64,
    amount: Uint128,
    cw20_addr: Addr,
) -> Result<Response, ContractError> {
    // load pot
    let mut pot = POTS.load(deps.storage, pot_id.u64())?;

    pot.collected += amount;

    ...

    if pot.collected >= pot.threshold {
        // Cw20Contract is a function helper that provides several queries and message builder.
        let cw20 = Cw20Contract(cw20_addr);
        // Build a cw20 transfer send msg, that send collected funds to target address
        let msg = cw20.call(Cw20ExecuteMsg::Transfer {
            recipient: pot.target_addr.into_string(),
            amount: pot.collected,
        })?;
        res = res.add_message(msg);
    }

    Ok(res)
}
src/contract.rs

CW20 컨트랙트 주소를 Cw20Contract 구조체로 감싸 해당 컨트랙트로 함수 호출을 할 수 있게 만들었다. 호출 대상 CW20 컨트랙트에 정의된 execute를 호출할 것이기 때문에 ExecuteMsg를 만들어 호출하는 것을 확인할 수 있다.

query()

컨트랙트의 storage를 조회해 사용자에게 정보를 반환한다.

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

fn query_pot(deps: Deps, id: Uint64) -> StdResult<PotResponse> {
    let pot = POTS.load(deps.storage, id.u64())?;
    Ok(PotResponse {
        target_addr: pot.target_addr.into_string(),
        collected: pot.collected,
        threshold: pot.threshold,
    })
}
src/contract.rs

QueryMsg는 GetPot 하나의 enum으로 구성되어 있다.

GetPot이 가지고 있는 id로 pot을 로드한 후 해당 pot에 정의되어 있는 타겟 주소, 모은 코인 수, threshold를 사용자에게 돌려준다.

정리하며

Rust에서 crate의 모듈 구조에 대해 먼저 알아보고, 이를 통해 Cosmwasm 스마트 컨트랙트가 어떤 모듈로 구성되어 있는지 살펴보았다.

다음 글에서는 스마트 컨트랙트를 배포하고, 트랜잭션을 보내 인스턴스를 생성하고 실행해볼 것이다.


Reference

Learn Rust
A language empowering everyone to build reliable and efficient software.