[CosmWasm] CosmWasm을 이용한 스마트컨트랙트 개발 - 2

iguigu·2022년 4월 28일
2

CosmWasm

목록 보기
2/3

Introduction

  • 스마트컨트랙트는 내부 state가 블록체인에 의해 유지되는 singleton object의 인스턴스라 할 수 있음
  • 유저는 JSON 메세지를 전달하여 state 변경을 야기하거나 state를 쿼리할 수 있음
  • CosmWasm으로 스마트컨트랙트 개발 시 다음 3가지 인터페이스를 정의해야함
    • instatiate() : 컨트랙트 인스턴스화 동안 intial state를 제공하기 위한 constructor
    • execute() : 스마트컨트랙트에서 사용자가 원하는 매서드를 실행시키기 위한 호출
    • query() : 스마트컨트랙트에서 원하는 데이터를 사용자가 얻기 위한 호출
  • 작업 디렉토리에서 cargo-generate를 사용하여 권장하는 폴더 디렉토리를 구성할 수 있음
# install cargo-generate
cargo install cargo-generate --features vendored-openssl
cargo generate --git https://github.com/CosmWasm/cosmwasm-template.git --name my-first-contract
cd my-first-contract

1. Contract Semantics

Execution

SDK Context

  • Cosmos SDK와 통합된 블록체인 프레임워크이기 때문에, CosmWasm은 해당 Context를 알아야함
  • SDK context
    • Tendermint 엔진은 다음 블록에서 포함된 트랜잭션에 대한 2/3+ 합의를 얻음
    • 이는 해당 트랜잭션을 실행하는 것 없이 진행 됨 (minimal pre-filter)
    • 블록이 commit되면 Cosmos SDK로 트랜잭션이 전달되어 순차적으로 실행됨
    • 이후 실행 완료된 후 AppHash가 다음 블록에 저장됨
    • x/wasm은 Cosmos SDK 모듈로, 특정 메세지를 처리하거나 스마트 컨트랙트를 upload, instantiate, execute하는 역할을 함
    • x/wasm은 서명 된 MsgExecuteContract를 받은 후 이를 Keeper.Execute로 라우팅하여 적절한 스마트 컨트랙트를 로드하고 excute를 호출 함. 만약 실패시엔 전체 트랜잭션이 revert 됨

Basic Execution

pub fn execute(
	deps : DepsMut,
    env : Env,
    info : MessageInfo,
    msg : ExecuteMsg,
)-> Result<Response, ContractError>
  • DepsMut : Storage에 읽거나 쓰기, address 유효성 검사를 위한 API, 다른 컨트랙트나 모듈에 대한 Query 담당
  • 실행 완료 후 Ok(Response)Err(ContractError)를 리턴함
  • err일 경우 모든 state change가 revert되고 x/wasm에 에러 메세지를 리턴함
  • 만약 Ok일 경우 Response가 파싱되고 처리됨
pub struct Response<T=Empty>
where T: Clone + fmt::Debug + PartialEq + JsonSchema,
{
	/// Optional list of "subcalls" to make. These will be executed in order
    /// (and this contract's subcall_response entry point invoked)
    /// *before* any of the "fire and forget" messages get executed.
	pub submessages : Vec<SubMsg<T>>,
    /// After any submessages are processed, these are all dispatched in the host blockchain.
    /// If they all succeed, then the transaction is committed. If any fail, then the transaction
    /// and any local contract state changes are reverted.
    pub message : Vec<CosmosMsg<T>>,
    /// The attributes that will be emitted as part of a "wasm" event
    pub attributes : Vec<Attribute>,
    pub data : Option<Binary>
}
  • Cosmos SDK에서 트랜잭션은 유저에게 result 데이터와 함께 많은 이벤트 로그를 전달 함
  • 해당 result는 해싱되어 다음 블록에서 provable한 형태로 저장 됨
  • ResultHash는 트랜잭션으로 부터 Code와 Result 만을 포함함
  • Event와 Log는 쿼리를 통해 가능하고, light-client proof는 존재하지 않는다.
  • Contract가 data를 저장하면 result를 반환하며, attributes는 {key, value}의 리스트 페어임. 아래는 클라이언트에게 전달되는 결과 예시
{
  "type": "wasm",
  "attributes": [
    { "key": "contract_addr", "value": "cosmos1234567890qwerty" },
    { "key": "custom-key-1", "value": "custom-value-1" },
    { "key": "custom-key-2", "value": "custom-value-2" }
  ]
}

Dispatching Message

  • message 필드의 경우, 다른 컨트랙트를 호출하거나 token을 옮기는 것과 같은 경우에 사용 됨
  • 컨트랙트가 만들 수 있는 외부 컨트랙트 콜의 직렬화된 표현인 CosmosMsg를 리턴 함
pub enum CosmosMsg<T = Empty>
where
    T: Clone + fmt::Debug + PartialEq + JsonSchema,
{
    Bank(BankMsg),
    /// This can be defined by each blockchain as a custom extension
    Custom(T),
    Staking(StakingMsg),
    Distribution(DistributionMsg),
    Stargate {
        type_url: String,
        value: Binary,
    },
    Ibc(IbcMsg),
    Wasm(WasmMsg),
}
  • 만약 컨트랙트가 메시지 M1, M2를 리턴한다면, 이 둘 모두 파싱되어 x/wasm에서 실행 됨
  • 성공할 경우 custom atrribute를 가진 새로운 이벤트를 생성하고 data field 는 무시됨
  • 실패할 경우 parent call은 에러를 리턴하고 전체 트랜잭션의 상태를 roll back 함
  • 메시지는 depth-first로 실행 됨
    • 만약 Contract A가 M1(WasmMsg::Excute)와 M2(BankMsg::Send)를 생성하고 WasmMsg::Excute로 부터 실행된 Contract B가 N1(StakingMsg), N2(DistributionMsg)를 실행할 경우
    • 메시지 실행 순서는 M1 → N1 → N2 → M2
    • 어째서 다른 컨트랙트를 바로 호출 할 수 없는지에 대한 의문이 생길 수 있음
    • 이는 이더리움 컨트랙트에서 reentrancy 보안 문제를 해결하기 위한 것임
    • actor 모델을 통해 nest function call을 방지하고, 나중에 실행된 메세지를 리턴함
    • 즉, 모든 스테이트는 하나의 콜과 다음 콜 사이에 실행이 완료되고, 메모리가 아닌 스토리지에서 진행 됨
    • 자세한 내용은 Actor model 내용 참고

Submessages

  • CosmWasm 0.14 부터 컨트랙트로 부터 콜을 호출하는 방을 하나 더 추가함
  • 예를 들어 WasmMsg::Instantiate와 함께 새로운 컨트랙트를 생성하길 원하며, caller에서 새롭게 생성된 컨트랙트의 주소를 저장하길 원할 때, Submessages가 해결책이 될 수 있음
  • 또한 이는 error 메시지를 저장하고, 전체 트랜잭션을 abort하는 대신 메시지에 마킹한 채로 실행하는 것을 가능하게 함. submessage의 가스사용을 제한하는 것도 가능함 (일반적이진 않음. cron contract 참고)
pub struct SubMsg<T = Empty>
where
    T: Clone + fmt::Debug + PartialEq + JsonSchema,
{
    pub id: u64,
    pub msg: CosmosMsg<T>,
    pub gas_limit: Option<u64>,
    pub reply_on: ReplyOn,
}

pub enum ReplyOn {
    /// Always perform a callback after SubMsg is processed
    Always,
    /// Only callback if SubMsg returned an error, no callback on success case
    Error,
    /// Only callback if SubMsg was successful, no callback on error case
    Success,
}
  • submessage 실행 순서
    • sub-transaction context를 생성하고 caller로 부터의 최신 state를 읽는 것을 허용함
    • 만약 gas_limit이 설정되어 있으면, 미리 샌드박스에서 실행되어 OutOfGasErr를 발생 시킴
    • 성공적으로 실행 시, temporary state는 커밋되고 Response는 정상처리됨
    • 실패 시에는, subcall이 해당 메시지로 인해 발생하는 부분적 상태 변경은 revert 하지만 calling contract에서는 상태변경이 발생하지 않음
  • submessage 사용 을 위해서는 calling contract는 추가적인 entry point가 있어야 함
#[entry_point]
pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> { }

pub struct Reply {
    pub id: u64,
    /// ContractResult is just a nicely serializable version of `Result<SubcallResponse, String>`
    pub result: ContractResult<SubcallResponse>,
}

pub struct SubcallResponse {
    pub events: Vec<Event>,
    pub data: Option<Binary>,
}
  • submessage가 종료된 후, caller는 결과를 처리할 기회를 얻음
  • 기존 subcall의 id를 갖고 이를 어떻게 처리할지를 정하게 됨
  • Order an Rollback
    • submessages와 replies는 message 전에 모두 실행 됨
    • 이것 역시 message와 같이 depth first rule로 처리됨
    • 즉, Contract A가 submesaages S1, S2와 message M1을 리턴하고, S1이 message N1을 리던할 경우 실행 순서는 S1 → N1 → reply(S1) → S2 → reply(S2) → M1
    • submessage의 executionreply는 다른 submessage의 컨택스트 내에서 동작해야 함
    • 예를 들어 contract-A--submessage --> contract-B--submessage --> contract-C일 경우 contract-B는 err를 리턴하며 contract-C를 revert할 수 있지만, contract-A를 하긴 힘듦
    • 컨트랙트가 2가지 submessage(a-ReplyOn::Success, b-ReplyOn::Error)를 생성할 경우 다음과 같은 다이어그램이 이해에 도움이 됨

Query Semantics

  • 위에서는 Response에 집중하였는데, 이를 통해 컨트랙트가 순차적으로 실행되고 nested하게 동작하지 않음을 확인하였음
  • 하지만 많은 경우, 컨트랙트 실행 중 다른 컨트랙트의 정보에 접근을 해야하는 경우가 발생함
  • 이를 위해 read-only Querier를 제공하여 synchronous call이 컨트랙트 실행 도중에 가능하게 함
  • read-only를 통해 query가 어떠한 state나 실행을 변경할 수 없게 하여 reentrancy 가능성을 방지함
  • query를 할 때, 모든 가능한 호출을 가진 QueryRequest struct를 serialize하여 런타임 도중에 x/wasm에서 처리되게 함
  • 이는 CosmosMsg와 같은 blockchain-specific 커스텀 쿼리가 가능하도록 확장시키며, custom result를 받아들일 수 있음
pub enum QueryRequest<C: CustomQuery> {
    Bank(BankQuery),
    Custom(C),
    Staking(StakingQuery),
    Stargate {
        /// this is the fully qualified service path used for routing,
        /// eg. custom/cosmos_sdk.x.bank.v1.Query/QueryBalance
        path: String,
        /// this is the expected protobuf message type (not any), binary encoded
        data: Binary,
    },
    Ibc(IbcQuery),
    Wasm(WasmQuery),
}
  • 이는 유연하고 cross-language를 위한 인코딩을 요구하지만, bank balance를 찾을 때 마다 이를 필요로하는 것은 꽤나 번거로움
  • 이를 돕기 위해 QuerierWrapper를 주로 사용하며, Querier를 감싸서, QueryRequestQuerier.raw_query를 여러 신뢰할수 있는 메서드에 노출되는 것을 가능하게 함

Contract State

  • 스마트컨트랙트는 byte-based Key-value store인 native LevelDB에 영구적인 state를 저장하는 것이 가능함
  • 어떠한 데이터든 키를 할당하여 저장하는 것이 가능하고 indexing하여 원할 때 사용하는 것이 가능함
// src/state.rs
pub const STATE: Item<State> = Item::new("state");
  • 위의 예제에서는 "state"가 키의 prefix로 사용됨
  • 데이터는 raw byte로만 존재할 수 있으며 structure나 type과 같은 정보는 serializing/deserializing 함수의 쌍으로 표현되어야 함
  • 그렇기에 두 함수 모두 블록체인에 byte형태의 object로 인코딩하여 저장하고 컨트랙트에서 사용하기 위해 데이터로 디코딩할 수 있어야 함
  • 이를 위한 byte representation은 사용자가 결정할 수 있음 (amino, protobuf 등...)
  • CosmWasm은 cosmwasm_storage와 같이 편리한 데이터 컨테이너의 high-level abstraction을 제공하고 있음. 이를 통해 일반적으로 사용되는 타입들에 대해 자동적으로 serialization/deserialization을 제공함
// src/state.rs
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use cosmwasm_std::{CanonicalAddr, Storage};
use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
  pub count: i32,
  pub owner: Addr,
}
  • 예제인 state struct는 count, owner를 가지고 있으며, derive 어트리뷰트는 다음과 같은 유용한 기능을 제공함. Addr는 human-readable한 Bech32 address를 표현함
    • Serialize : serialization 제공
    • Deserialize : deserialization 제공
    • Clone : 카피 가능한 struct를 만듦
    • Debug : struct를 string으로 출력할 수 있게 해줌
    • PartialEq : equality comparison을 제공함
    • JsonSchema : 자동으로 JSON schema를 생성해줌

2. Message

InstantiateMsg

  • 사용자가 블록체인에서 MsgInstantiateContract을 통해 컨트랙트를 생성할 때 InstantiateMsg가 제공됨
  • initial state와 같은 configuration을 컨트랙트에 제공함
  • CosmWasm에서 컨트랙트 코드를 업로딩하고 컨트랙트를 인스턴스화하는 것은 이더리움과 달리 별개의 이벤트로 간주됨
  • 이는 작은 vetted contract archetype이 여러 인스턴스에서 동일한 base 코드를 공유하며 다른 파라미터를 사용하는 것이 가능하게 함 (ERC20 하나의 컨트랙트를 이용하여 여러 토큰을 만드는 것)

Example

  • Contract는 JSON 메세지 안에 initial state를 제공하는 contract creator를 예상
{
  "count" : 100
}

Message Definition

// src/msg.rs
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
  pub count: i32,
}

Logic

  • instantiate() 엔트리 포인트로 정의하였으며, 컨트랙트는 인스턴스화되고 IntantiateMsg를 전달함
  • count를 메세지에서 추출하고 initial state에 설정함
    • msg.count는 count에 할당됨
    • sender의 MsgInstantiateContractowner에 할당됨
// src/contract.rs
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
  deps: DepsMut,
  _env: Env,
  info: MessageInfo,
  msg: InstantiateMsg,
) -> Result<Response, ContractError> {
  let state = State {
    count: msg.count,
    owner: info.sender.clone(),
  };
  set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
  STATE.save(deps.storage, &state)?;

  Ok(Response::new()
    .add_attribute("method", "instantiate")
    .add_attribute("owner", info.sender)
    .add_attribute("count", msg.count.to_string()))
}

ExecuteMsg

  • ExecuteMsgMsgExecuteContract를 통해 execute() 함수에 전달된 JSON 메세지임
  • InstantiageMsg와 달리 ExecuteMsg는 여러 다른 종류의 메세지 타입으로 존재할 수 있음
  • execute() 함수는 이 같이 다른 메세지 타입을 그것들의 적절한 메세지 핸들러 로직으로 분배함

Example

  • Increment : 현재 count를 1 올림
{
  "increment": {}
}
  • Reset : owner가 특정한 숫자로 count를 초기화 함
{
  "reset": {
    "count": 5
  }
}

Message Definition

  • ExecuteMsg에서, enum을 사용하여 컨트랙트가 이해할 수 있는 다른 타입 메세지로 분배하도록 함
  • serde 속성은 snake case와 lower case인 attribute key로 다시 생성하고 incrementRest 대신 incrementreset를 JSON을 serializing, deserializing 할 때 사용함
// src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
  Increment {},
  Reset { count: i32 },
}

Logic

  • Rust의 패턴 매칭을 통해 적절한 로직으로 route
// src/contract.rs
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
  deps: DepsMut,
  _env: Env,
  info: MessageInfo,
  msg: ExecuteMsg,
) -> Result<Response, ContractError> {
  match msg {
    ExecuteMsg::Increment {} => try_increment(deps),
    ExecuteMsg::Reset { count } => try_reset(deps, info, count),
  }
}
  • src/state.rs에 정의된 "state" 키에 있는 item을 업데이트하기 위해 mutable reference를 얻음
  • 현재 state의 count를 업데이트하고 새로운 state와 함께 Ok 결과가 반환됨
  • 컨트랙트 실행이 종료되면 기본 Response와 함께 Ok 결과를 통해 성공 여부를 알게 됨
  • 본 예제에서는 default Response가 단순하게 사용되지만, Response는 다음과 같은 정보들을 추가로 제공하는 것이 가능함
    • message : 메세지 리스트. 스마트컨트랙트가 다른 스마트 컨트랙트를 실행하거나 native 모듈을 실행할 수 있음
    • attribute : SDK 이벤트를 정의하는데 사용되는 key-value 리스트
    • event : 메인 wasm과 별개의 custom event. execution동안 중요한 이벤트나 state change를 알리는 어플리케이션이나 Explorer
    • data : 컨트랙트가 client에게 반환하는 추가적인 데이터
pub fn try_increment(deps: DepsMut) -> Result<Response, ContractError> {
  STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {
    state.count += 1;
    Ok(state)
  })?;

  Ok(Response::new().add_attribute("method", "try_increment"))
}
  • reset 로직은 increment와 유사하며 메시지 전달자는 reset 함수를 실행하는 것을 허용할지를 확인함
// src/contract.rs
pub fn try_reset(deps: DepsMut, info: MessageInfo, count: i32) -> Result<Response, ContractError> {
  STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {
    if info.sender != state.owner {
      return Err(ContractError::Unauthorized {});
    }
    state.count = count;
    Ok(state)
  })?;
  Ok(Response::new().add_attribute("method", "reset"))
}

QueryMsg

Example

  • request
{
  "get_count": {}
}
  • return
{
  "count": 5
}

Message Definition

  • 컨트랙트 데이터 쿼리를 지원하기 위해 query 결과인 CountResponse (해당 예제에서만)와 QueryMsg 포맷을 둘다 정의해야 함
  • query()는 정보를 사용자에게 JSON 형태로 전송하고 응답 결과 형태를 알아야만 하기에 때문에 이 같은 과정을 해야함
// src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
  // GetCount returns the current count as a json-encoded number
  GetCount {},
}

// We define a custom struct for each query response
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct CountResponse {
  pub count: i32,
}

Logic

  • query() 로직은 execute()와 유사하며 query()는 end-user가 트랜잭션을 생성하지 않는 차이가 있음
  • env 인자는 정보가 없기 때문에 빼먹었음
// src/contract.rs
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
  match msg {
    QueryMsg::GetCount {} => to_binary(&query_count(deps)?),
  }
}

fn query_count(deps: Deps) -> StdResult<CountResponse> {
  let state = STATE.load(deps.storage)?;
  Ok(CountResponse { count: state.count })
}

3. Submessage

  • message는 SDK 모듈이나 CW 스마트 컨트랙트와 인터랙트 하기 위해 사용됨
  • message는 set-and-forget 형태로 실행되어지기 때문에, call이 성공하지 않는다면 response를 얻을 수 없음
  • 다음과 같은 경우에 submessage를 통해 call의 결과를 얻는 것이 유용함
    • 새로운 컨트랙트를 생성하고 컨트랙트 주소를 가져올 때
    • auction을 실행하고 결과가 성공적이었는지 확인할 때 (특정 코인이 컨트랙트로 제대로 전달되었는지 확인 등)
    • 컨트랙트 간의 호출에서 트랜잭션을 롤백하는 것 대신, 에러 핸들링을 할 때

Creating a submessage

  • submessage가 CosmMsg를 해당 구조체 내에서 랩핑
pub struct SubMsg<T> {
    pub id: u64,                // reply_id that will be used to handle the reply
    pub msg: CosmosMsg<T>,      // message to be sent
    pub gas_limit: Option<u64>, // gas limit for the submessage
    pub reply_on: ReplyOn,      // a flag to determine when the reply should be sent
}
  • cw20 토큰을 submessage를 통해 인스턴스화 하는 예시
const INSTANTIATE_REPLY_ID = 1u64;

// Creating a message to create a new cw20 token
let instantiate_message = WasmMsg::Instantiate {
    admin: None,
    code_id: msg.cw20_code_id,
    msg: to_binary(&Cw20InstantiateMsg {
        name: "new token".to_string(),
        symbol: "nToken".to_string(),
        decimals: 6,
        initial_balances: vec![],
        mint: Some(MinterResponse {
            minter: env.contract.address.to_string(),
            cap: None,
        }),
    })?,
    funds: vec![],
    label: "".to_string(),
};

// Creating a submessage that wraps the message above
let submessage = SubMsg::reply_on_success(instantiate_message.into(), INSTANTIATE_REPLY_ID);

// Creating a response with the submessage
let response = Response::new().add_submessage(submessage);

4. Result and Option

Result

  • 많은 contract entry point는 Result<Response, ContractError>
  • 이뿐만이 아니라 enum match 시에도 자주 사용됨
pub fn execute_transfer(
  deps: DepsMut,
  _env: Env,
  info: MessageInfo,
  recipient: String,
  amount: Uint128,
) -> Result<Response, ContractError> {
  if amount == Uint128::zero() {
    return Err(ContractError::InvalidZeroAmount {});
  }

  let rcpt_addr = deps.api.addr_validate(&recipient)?;

  BALANCES.update(
    deps.storage,
    &info.sender,
    |balance: Option<Uint128>| -> StdResult<_> {
      Ok(balance.unwrap_or_default().checked_sub(amount)?)
    },
  )?;
  BALANCES.update(
    deps.storage,
    &rcpt_addr,
    |balance: Option<Uint128>| -> StdResult<_> { Ok(balance.unwrap_or_default() + amount) },
  )?;

  let res = Response::new()
    .add_attribute("action", "transfer")
    .add_attribute("from", info.sender)
    .add_attribute("to", recipient)
    .add_attribute("amount", amount);
  Ok(res)
}

StdResult

  • StdResult 는 query handler나 호출되어진 함수들에서 사용됨
pub fn query(deps : Deps, env : Env, msg : QueryMsg) -> StdResult<Binary>{
	match msg{
    	QueryMsg :: ResolveRecord {name} => query_resolver(deps,env,name),
        QueryMsg :: Config {} => to_binary(&config_read(deps.storage).load()?),
    }
}

fn query_resolver(deps: Deps, _env : Env, name : String ) -> StdResult<Binary>{
	let key = name.as_bytes();
    let address = match resolver_read(deps.storage).may_load(key)?{
    	Some(record) => Some(String::from(&record.owner)),
        None => None,
    };
    let resp = ResolveRecordResponse { address };
    to_binary(&resp)
}
  • line up되어있는 한 container type을 무시하는 것이 가능함
  • 타입이 정확하다면, 컴파일이 되며, 컨테이너에 저장되어진 값을 사용하기 위해서는 단순하게 컨테이너 타입을 match하거나 unwrap하면 됨

Option

  • Option은 struct내에 key가 존재하는지 아닌지를 표현하는 데 유용함
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
  pub purchase_price: Option<Coin>,
  pub transfer_price: Option<Coin>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
  pub purchase_price: Option<Coin>,
  pub transfer_price: Option<Coin>,
}
  • storage에서 값을 읽으려고 시도할 때, 이는 result가 나오거나 아무것도 없을 것임
  • 이 같은 상황을 처리하기위해, pattern match를 하는 것이 일반적으로 사용됨
let address = match resolver_read(deps.storage).may_load(key)?{
	Some(record) => Some(String::from(&record.owner)),
    None => None, // None일 경우에는 error 상태를 반환 함
}

5. State

cw-storage-plus

Item

  • 하나의 데이터베이스 키로 typed wrapper로 raw byte를 다루는 것없이 인터랙트 가능한 함수 기능 제공
  • 다른 아이템에서 사용되지 않았으며 적절한 타입의 key를 제공해야함
  • singleton과 달리 Item은 더 이상 Storage 안에 저장하지 않아 여러 object를 읽거나 쓸 필요가 없고 하나의 타입으로 충분함
  • Item을 생성하는 const fn을 사용하여 global compile-time constant를 정의하는 것을 가능하게 함 (gas 사용량을 줄이는 것이 가능)
  struct Config{
  	pub owner: String,
    pub max_tokens : i32,
  }
  
  const CONFIG: Item<Config> = Item::new("config");
  fn demo() -> StdResult<()> {
  	let mut store = MockStorage::new()
  }

Map

  • typed value를 가진 key-value lookup을 허용하는 storage-backed BTreeMap라 생각할 수 있음
  • Ethereum에선 지원되지 않는 Iteration 제공
  • 복수의 unique typed object를 prefix를 가진 채 저장하는 것을 가능하게 함
  • simple (&[u8]) 또는 compound (eg. (&[u8], &[u8])) primary key로 인덱싱 됨. 이를 통해 (owner,spender) 같은 composite key를 이용하여 balance를 탐색하는 것이 가능
  • Simple Key
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct Data {
    pub name: String,
    pub age: i32,
}

const PEOPLE: Map<&str, Data> = Map::new("people");

fn demo() -> StdResult<()> {
    let mut store = MockStorage::new();
    let data = Data {
        name: "John".to_string(),
        age: 32,
    };

    // load and save with extra key argument
    let empty = PEOPLE.may_load(&store, "john")?;
    assert_eq!(None, empty);
    PEOPLE.save(&mut store, "john", &data)?;
    let loaded = PEOPLE.load(&store, "john")?;
    assert_eq!(data, loaded);

    // nothing on another key
    let missing = PEOPLE.may_load(&store, "jack")?;
    assert_eq!(None, missing);

    // update function for new or existing keys
    let birthday = |d: Option<Data>| -> StdResult<Data> {
        match d {
            Some(one) => Ok(Data {
                name: one.name,
                age: one.age + 1,
            }),
            None => Ok(Data {
                name: "Newborn".to_string(),
                age: 0,
            }),
        }
    };

    let old_john = PEOPLE.update(&mut store, "john", birthday)?;
    assert_eq!(33, old_john.age);
    assert_eq!("John", old_john.name.as_str());

    let new_jack = PEOPLE.update(&mut store, "jack", birthday)?;
    assert_eq!(0, new_jack.age);
    assert_eq!("Newborn", new_jack.name.as_str());

    // update also changes the store
    assert_eq!(old_john, PEOPLE.load(&store, "john")?);
    assert_eq!(new_jack, PEOPLE.load(&store, "jack")?);

    // removing leaves us empty
    PEOPLE.remove(&mut store, "john");
    let empty = PEOPLE.may_load(&store, "john")?;
    assert_eq!(None, empty);

    Ok(())
}
  • Composite key
    • allowance를 account owner와 spender 기반으로 저장할 경우 유용함
// Note the tuple for primary key. We support one slice, or a 2 or 3-tuple
// adding longer tuples is quite easy but unlikely to be needed.
const ALLOWANCE: Map<(&str, &str), u64> = Map::new("allow");

fn demo() -> StdResult<()> {
    let mut store = MockStorage::new();

    // save and load on a composite key
    let empty = ALLOWANCE.may_load(&store, ("owner", "spender"))?;
    assert_eq!(None, empty);
    ALLOWANCE.save(&mut store, ("owner", "spender"), &777)?;
    let loaded = ALLOWANCE.load(&store, ("owner", "spender"))?;
    assert_eq!(777, loaded);

    // doesn't appear under other key (even if a concat would be the same)
    let different = ALLOWANCE.may_load(&store, ("owners", "pender")).unwrap();
    assert_eq!(None, different);

    // simple update
    ALLOWANCE.update(&mut store, ("owner", "spender"), |v| {
        Ok(v.unwrap_or_default() + 222)
    })?;
    let loaded = ALLOWANCE.load(&store, ("owner", "spender"))?;
    assert_eq!(999, loaded);

    Ok(())
}
  • Path
    • key에 접근할 때 Map으로 부터 Path를 생성하는 것이 가능함
    • Map.key()는 동일한 인터페이스(Item) 갖는 Path를 리턴하며 해당 키로 가는 path를 계산하는 데 재사용됨
    • simple key의 경우에는 동일한 key를 여러 곳에서 사용 시 less typing 하고 가스를 덜 사용함
    • (b"owner", b"spender") 같은 composite key는 훨씬 덜 typing함. composite key를 두번 사용하는 곳에서 사용되기를 적극 권장됨
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct Data {
    pub name: String,
    pub age: i32,
}

const PEOPLE: Map<&str, Data> = Map::new("people");
const ALLOWANCE: Map<(&str, &str), u64> = Map::new("allow");

fn demo() -> StdResult<()> {
    let mut store = MockStorage::new();
    let data = Data {
        name: "John".to_string(),
        age: 32,
    };

    // create a Path one time to use below
    let john = PEOPLE.key("john");

    // Use this just like an Item above
    let empty = john.may_load(&store)?;
    assert_eq!(None, empty);
    john.save(&mut store, &data)?;
    let loaded = john.load(&store)?;
    assert_eq!(data, loaded);
    john.remove(&mut store);
    let empty = john.may_load(&store)?;
    assert_eq!(None, empty);

    // Same for composite keys, just use both parts in key().
    // Notice how much less verbose than the above example.
    let allow = ALLOWANCE.key(("owner", "spender"));
    allow.save(&mut store, &1234)?;
    let loaded = allow.load(&store)?;
    assert_eq!(1234, loaded);
    allow.update(&mut store, |x| Ok(x.unwrap_or_default() * 2))?;
    let loaded = allow.load(&store)?;
    assert_eq!(2468, loaded);

    Ok(())
}
  • Prefix
    • map에서의 특정 아이템을 가져오는 것 이외에, map을 iterate하는 것도 가능함
    • 이를 통해 "모든 토큰을 보여줘" 같은 명령을 처리하는 것이 가능
    • map.prefix(k)를 호출하여 prefix를 가져오는 것이 일반적
#[derive(Copy, Clone, Debug)]
pub enum Bound {
    Inclusive(Vec<u8>),
    Exclusive(Vec<u8>),
    None,
}

#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct Data {
    pub name: String,
    pub age: i32,
}

const PEOPLE: Map<&str, Data> = Map::new("people");
const ALLOWANCE: Map<(&str, &str), u64> = Map::new("allow");

fn demo() -> StdResult<()> {
    let mut store = MockStorage::new();

    // save and load on two keys
    let data = Data { name: "John".to_string(), age: 32 };
    PEOPLE.save(&mut store, "john", &data)?;
    let data2 = Data { name: "Jim".to_string(), age: 44 };
    PEOPLE.save(&mut store, "jim", &data2)?;

    // iterate over them all
    let all: StdResult<Vec<_>> = PEOPLE
        .range(&store, Bound::None, Bound::None, Order::Ascending)
        .collect();
    assert_eq!(
        all?,
        vec![("jim".to_vec(), data2), ("john".to_vec(), data.clone())]
    );

    // or just show what is after jim
    let all: StdResult<Vec<_>> = PEOPLE
        .range(
            &store,
            Bound::Exclusive("jim"),
            Bound::None,
            Order::Ascending,
        )
        .collect();
    assert_eq!(all?, vec![("john".to_vec(), data)]);

    // save and load on three keys, one under different owner
    ALLOWANCE.save(&mut store, ("owner", "spender"), &1000)?;
    ALLOWANCE.save(&mut store, ("owner", "spender2"), &3000)?;
    ALLOWANCE.save(&mut store, ("owner2", "spender"), &5000)?;

    // get all under one key
    let all: StdResult<Vec<_>> = ALLOWANCE
        .prefix("owner")
        .range(&store, Bound::None, Bound::None, Order::Ascending)
        .collect();
    assert_eq!(
        all?,
        vec![("spender".to_vec(), 1000), ("spender2".to_vec(), 3000)]
    );

    // Or ranges between two items (even reverse)
    let all: StdResult<Vec<_>> = ALLOWANCE
        .prefix("owner")
        .range(
            &store,
            Bound::Exclusive("spender1"),
            Bound::Inclusive("spender2"),
            Order::Descending,
        )
        .collect();
    assert_eq!(all?, vec![("spender2".to_vec(), 3000)]);

    Ok(())
}

IndexedMap

  • Definition
// owner라는 인덱스(MultiIndex) 생성 
// MultiIndex는 반복되는 값을 키로 가질수 있음
// MultiIndex key의 가장 마지막 엘리먼트가 내부적으로 primary key로 쓰임
pub struct TokenIndexes<'a> {
  pub owner: MultiIndex<'a, Addr, TokenInfo>,
}


// boiler plate로, 수정하지 말것
// 이는 get_indexes를 통해 인덱스가 쿼리될 수 있게 함
impl<'a> IndexList<TokenInfo> for TokenIndexes<'a> {
  fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<TokenInfo>> + '_> {
    let v: Vec<&dyn Index<TokenInfo>> = vec![&self.owner];
    Box::new(v.into_iter())
  }
}

// TokenIndexes 가 IndexList로 취급되며, IndexedMap 생성 중 패러미터를전달 하는 것이 가능함
pub fn tokens<'a>() -> IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>> {
  let indexes = TokenIndexes {
    owner: MultiIndex::new(
      |d: &TokenInfo| d.owner.clone(),
      "tokens",
      "tokens__owner",
    ),
  };
  
  // IndexedMap 생성 완료
  IndexedMap::new("tokens", indexes)
}
  • Usage
  • Index keys deserialization
    • UniqueIndexMultiIndex에게 primary key 타입은 primary key를 deserialize하기 위해 specified 되어야함
    • 이는 backward compatibility를 위한 것

6. Entry Points

  • 컨트랙트가 핸들링하는 메세지나 쿼리를 다루는 곳(핸들러)
  • 3가지 핸들러
    • Initiate message : InstantiateMsg 구조체가 정의하는 것으로 instantiate에 처리 됨
    • Message : ExecuteMsg enum으로 정의 되며 execute 함수에서 처리됨
    • Query : QueryMsg enum으로 정의되어 query 함수에서 처리됨
  • executequery는 모든 enum안의 경우가 패턴 매칭이 되어야함
  • instantiateexecuteResult<Response, ContractError> 타입을 가지고, query는 Cosmos SDK Querier기반이기에 StdResult<Binary>를 가짐

7. Query

  • 쿼리 가능한 메세지는 msg.rsquery.rs에서 찾는 것이 가능함
  • 대부분의 쿼리는 커스텀 쿼리이며, 이는 컨트랙트의 데이터를 read-only 모드로 접근함
  • 이 같은 쿼리는 data를 찾거나 추가적인 연산이나 필요한 처리를 할 수 있기 때문에 (query 함수 내에서), 이런 쿼리들에 대해서는 gas limit이 설정되어야 함
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
  // ResolveAddress returns the current address that the name resolves to
  ResolveRecord { name: String },
  Config {},
}

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

8. Events

  • 대부분의 entry point 함수는 Result<Response, ContractError> 타입을 반환 함
  • Response는 Cosmos SDK에서 event에 대한 wrapper임
  • Response 타입은 컨트랙트 entry point의 성공 결과를 반환해야함
  • Ok로 결과가 랩핑되어야 하며, Response는 Right나 success branch를 표현하는 Result 타입이어야 함
  • query는 Cosmos SDK 인터페이스 때문에 StdResult<Binary>를 가져야 함
// 일반적인 `Response` 사용 (instantiate 함수에서 일반적)
Ok(Response::default ()) 

// 대부분의 excute handling 케이스
// Response 생성 -> Key/value 페어 생성 및 추가 ->  Ok로 Result을 랩핑
// .add_message, .add_event 등도 가능
let res = Response::new()
.add_attribute("action", "transfer")
.add_attribute("from", info.sender)
.add_attribute("to", recipient)
.add_attribute("amount", amount);
Ok(res)

9. Math

  • CosmWasm 에서 사용되는 math 함수들은 standard rust 기반이지만, u128이나 u64나 decimal을 위한 helper function들 존재 (링크)
  • Uint128
    • checked
    • saturating
    • wrapping
  • Uint64
    • checked
    • saturating
    • wrapping
  • Decimal

10. Compilation

  • 컨트랙트를 사용하기 전에 컨트랙트 코드를 컴파일 하여 체인에 저장해야함
  • cargo를 이용하는 것이 가장 쉬움
# 단순 테스팅 시
cargo wasm 

# 불필요한 코드를 strip하여 최적화 시킴 
RUSTFLAGS='-C link-arg=-s' cargo wasm 

# docker optimiser 이용 
sudo docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/workspace-optimizer:0.12.4

11. Deployment

  • 컴파일을 성공한 후 체인에 deploy
wasmd tx wasm store <your-contract>.wasm  --from <your-key> --chain-id <chain-id> --gas auto

# 컨트랙트의 code id가 필요할 때 JSON 형태로 값 보여줄 때 사용
cd artifacts
RES=$(wasmd tx wasm store <your-contract>.wasm  --from <your-key> --chain-id=<chain-id> --gas auto -y)
CODE_ID=$(echo $RES | jq -r '.logs[0].events[0].attributes[-1].value')

12. Verifying Smart Contracts

  • 스마트 컨트랙트가 체인에 배포된 후, 그것을 사용하는 다른 유저들은 해당 컨트랙트가 정확한지를 확신해야함

Inspect Code

  • Juno의 uni 네트워크에 juno1unclk8rny4s8he4v2j826rattnc7qhmhwlv3wm9qlc2gamhad0usxl7jnd 주소인 컨트랙트가 있다고할 때, 컨트랙트 정보를 다음과 같은 쿼리로 가져올 수 있음
junod query wasm contract-state raw juno1unclk8rny4s8he4v2j826rattnc7qhmhwlv3wm9qlc2gamhad0usxl7jnd 636F6E74726163745F696E666F --node $RPC --output json | jq  -r .data | base64 -d | jq
{
  "contract": "crates.io:cw20-base",
  "version": "0.10.3"
}
  • cw20-base0.10.3 버전임을 확인
  • 추가적으로 hash를 얻기위해 Code ID가 필요함
junod query wasm contract juno1unclk8rny4s8he4v2j826rattnc7qhmhwlv3wm9qlc2gamhad0usxl7jnd  --node $RPC --output json  | jq
{
  "address": "juno1unclk8rny4s8he4v2j826rattnc7qhmhwlv3wm9qlc2gamhad0usxl7jnd",
  "contract_info": {
    "code_id": "122",
    "creator": "juno1d3axtckm7f777vlu5v8dy8dsd6fefhhnmsrrps",
    "admin": "",
    "label": "Hidden",
    "created": null,
    "ibc_port_id": "",
    "extension": null
  }
}
  • 컨트랙트의 정보를 알게 되었으며 이에 따라 실제 코드가 필요함
junod query wasm code 122 122_code.wasm --node $RPC
Downloading wasm code to 122_code.wasm
  • 다음가 같이 해시(46bd624fff7f11967aac6ddaecf29201d1897be5216335ccddb659be5b524c52)를 확인하는 것이 가능
sha256sum 122_code.wasm
46bd624fff7f11967aac6ddaecf29201d1897be5216335ccddb659be5b524c52  122_code.wasm

Find the Original Code

  • 만약 컨트랙트 제공자가 배포하였으면 소스코드 레포에서 해시발견이 가능. cw-plus 레포에 있는 컨트랙트들의 해시가 공개되어 있음
fe34cfff1cbc24594740164abb953f87735afcdecbe8cf79a70310e36fc13aa0  cw1155_base.wasm
de49426e9deed6acf23d5e930a81241697b77b18131c9aea5c3ca800c028459e  cw1_subkeys.wasm
c424b66e7f289cef69e1408ec18732e034b0604e4b22bfcca7546cc9d57875e3  cw1_whitelist.wasm
e462d44a086a936c681f3b3389d50b8404ce2152c8f0fb32b257064576210c03  cw1_whitelist_ng.wasm
0b2e5d5dc895f8f49f833b076a919774bb5b0d25bf72819e9a1cbdf70f9bf79b  cw20_atomic_swap.wasm
6c1fa5872e1db821ee207b5043d679ad1f57c40032d2fd01834cd04d0f3dbafb  cw20_base.wasm
f00759aa9a221efeb58b61a1a1d4cc4281cdce39d71ac4d8d78d234f03b3b0eb  cw20_bonding.wasm
b6041789cc227472c801763c3fab57a81005fb0c30cf986185aba5e0b429d2e6  cw20_escrow.wasm
91b35168d761de9b0372668dd8fa8491f2c8faedf95da602647f4bade7cb9f57  cw20_ics20.wasm
d408a2195df29379b14c11277f785b5d3f57b71886b0f72e0c90b4e84c2baa4a  cw20_merkle_airdrop.wasm
934ba53242e158910a2528eb6c6b82deb95fe866bbc32a8c9afa7b97cfcb9af4  cw20_staking.wasm
ac1f2327f3c80f897110f0fca0369c7022586e109f856016aef91f3cd1f417c1  cw3_fixed_multisig.wasm
785340c9eff28e0faeb77df8cca0fafee6b93a1fa033d41bda4074cd97600ec1  cw3_flex_multisig.wasm
87b3ad1dee979afc70e5c0f19e8510d9dcc8372c8ef49fc1da76725cad706975  cw4_group.wasm
4651e90405917897f48d929198278f238ec182ac018c414ee22f2a007a052c1e  cw4_stake.wasm

Compile yourself

  • 옛날 버전의 코드해시를 찾으려하면, minor release 버전인 경우에는 어려울 수 있음
  • rust-optimizer는 코드사이즈를 작게해줄 뿐만 아니라 comparable하도록 코드 결과를 deterministic하게 해줌. 해시는 ./artifacts/checksums.txt에 생성됨
docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/workspace-optimizer:0.12.4
  
cat ./artifacts/checksums.txt | grep cw20_base.wasm
46bd624fff7f11967aac6ddaecf29201d1897be5216335ccddb659be5b524c52  cw20_base.wasm

diff  <(echo "46bd624fff7f11967aac6ddaecf29201d1897be5216335ccddb659be5b524c52" ) <(echo "46bd624fff7f11967aac6ddaecf29201d1897be5216335ccddb659be5b524c52")
# 해시가 동일할 경우 컨트랙트가 정상적으로 verify된 것

13. Migration

  • Migration은 스마트컨트랙트 코드를 제거하거나 upgrade하는 과정임
  • 컨트랙트를 instantiate할 때 admin 필드가 없으면 코드는 immutable해짐
  • contract를 migrate할 때 이전에 state가 어떻게 인코딩 되었는지 알 필요가 있음
  • CW2 spec을 통해 설명하면 다음과 같음
    • CW2는 singleton이라는 것을 정의하여 인스턴스화 된 모든 컨트랙트가 저장되어 짐
    • migration 함수가 호출 되면 새로운 컨트랙트 데이터를 읽을 수 있고 볼수 있음
    • 여러 migration 버전이 있으면 기타 버전 정보를 포함할 수 있음
const CONTRACT_NAME: &str = "crates.io:my-crate-name";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");


#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(deps: DepsMut, env: Env, info: MessageInfo, msg: InstantiateMsg) -> Response {
    // Use CW2 to set the contract version, this is needed for migrations
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct ContractVersion {
    /// contract is the crate name of the implementing contract, eg. `crate:cw20-base`
    /// we will use other prefixes for other languages, and their standard global namespacing
    pub contract: String,
    /// version is any string that this implementation knows. It may be simple counter "1", "2".
    /// or semantic version on release tags "v0.7.0", or some custom feature flag list.
    /// the only code that needs to understand the version parsing is code that knows how to
    /// migrate from the given contract (and is tied to it's implementation somehow)
    pub version: String,
}

Setting up a contract for migration

    1. 업데이트할 새로운 버전의 컨트랙트를 생성
    1. 이전에 한 것과 같이 새로운 코드를 업로드하되(store) instantiate 하지 않음
    1. MsgMigrateContract 트랜잭션을 사용하여 컨트랙트가 새로운 코드를 가르키도록 함
  • Migration 프로세스동안 새 코드에 정의된 migrate 함수가 실행 되고 이전 것들은 실행되지 않음
  • 새로운 코드는 migrate 함수가 정확하게 정의되어있어야하며 entry_point:#[cfg_attr(not(feature="library"))]가 노출되어 있어야함
  • migrate 함수는 state 같은데서 필요로하는 어떤 변경사항이든 만들 수 있게 됨

Basic Contract Migration

  • 간단한 방법이며, cw2::set_contract_version을 사용하지 않으면 safety check가 실행되지 않을 수 있음
const CONTRACT_NAME: &str = "crates.io:my-crate-name";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    // No state migrations performed, just returned a Response
    Ok(Response::default())
}

Restricted Migration by code version and name

  • migrate 함수가 다음 조건을 충족 시켜야 함
    • 동일한 타입의 컨트랙트를 migrating 해야하기에 이름을 확인해야함
    • 이전 버전의 컨트랙트를 업그레이딩하기에 버전을 체크해야함
const CONTRACT_NAME: &str = "crates.io:my-crate-name";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let ver = cw2::get_contract_version(deps.storage)?;
    // ensure we are migrating from an allowed contract
    if ver.contract != CONTRACT_NAME {
        return Err(StdError::generic_err("Can only upgrade from same type").into());
    }
    // note: better to do proper semver compare, but string compare *usually* works
    if ver.version >= CONTRACT_VERSION {
        return Err(StdError::generic_err("Cannot upgrade from a newer version").into());
    }

    // set the new version
    cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    // do any desired state migrations...

    Ok(Response::default())
}

Migrate which updates the version only if newer

  • migrate 함수가 다음 조건을 충족 시켜야 함
    • 컨트랙트 버전이 저장된 것에서 증가된다면 필요로한 migration을 수행하고 새로운 버전을 저장함
    • Semver를 String 비교 대신 사용할 것
const CONTRACT_NAME: &str = "crates.io:my-crate-name";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let version: Version = CONTRACT_VERSION.parse()?;
    let storage_version: Version = get_contract_version(deps.storage)?.version.parse()?;

    if storage_version < version {
        set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

        // If state structure changed in any contract version in the way migration is needed, it
        // should occur here
    }
    Ok(Response::default())
}
  • 이 예시에서는 semver dependency를 cargo에 추가하고 컨트랙트 패키지에 커스텀 에러 추가 필요
[dependencies]
semver = "1"
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {

    #[error("Semver parsing error: {0}")]
    SemVer(String),

}
impl From<semver::Error> for ContractError {
    fn from(err: semver::Error) -> Self {
        Self::SemVer(err.to_string())
    }
}

Using migrate to update otherwise immutable state

  • 일반적으로 변경되어선 안되는 값을 업데이트하기 위해 migration이 사용될 수 있음
  • MigrateMsg가 state에 존재하는 컨트랙트의 verifer 필드에 새로운 값을 갖게 하는 verifier 필드 사용
  • UpdateStateUpdateVerifier 같은 ExecuteMsg 없이 verifier 필드를 migrate 과정에서 변경
#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, HackError> {
    let data = deps
        .storage
        .get(CONFIG_KEY)
        .ok_or_else(|| StdError::not_found("State"))?;
    let mut config: State = from_slice(&data)?;
    config.verifier = deps.api.addr_validate(&msg.verifier)?;
    deps.storage.set(CONFIG_KEY, &to_vec(&config)?);

    Ok(Response::default())
}

Using migration to 'burn' a contract

  • 오래된 컨트랙트를 완전히 버리고 state를 burn하는 migration
#[entry_point]
pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> StdResult<Response> {
    // delete all state
    let keys: Vec<_> = deps
        .storage
        .range(None, None, Order::Ascending)
        .map(|(k, _)| k)
        .collect();
    let count = keys.len();
    for k in keys {
        deps.storage.remove(&k);
    }

    // get balance and send all to recipient
    let balance = deps.querier.query_all_balances(env.contract.address)?;
    let send = BankMsg::Send {
        to_address: msg.payout.clone(),
        amount: balance,
    };

    let data_msg = format!("burnt {} keys", count).into_bytes();

    Ok(Response::new()
        .add_message(send)
        .add_attribute("action", "burn")
        .add_attribute("payout", msg.payout)
        .set_data(data_msg))
}

14. Code pinning

  • Code Pinning은 코드를 메모리에 pin 될 수 있게 하는 것
  • 이를 통해 코드가 매 실행마다 메모리에 올라가지 않게 하여 성능향상 가능

Proposal

  • PinCodesProposal
// PinCodesProposal gov proposal content type to pin a set of code ids in the
// wasmvm cache.
message PinCodesProposal {
  // Title is a short summary
  string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
  // Description is a human readable text
  string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
  // CodeIDs references the new WASM codes
  repeated uint64 code_ids = 3 [
    (gogoproto.customname) = "CodeIDs",
    (gogoproto.moretags) = "yaml:\"code_ids\""
  ];
}
wasmd tx gov submit-proposal pin-codes 1 --from wallet --title "Pin code 1" --description "Pin code 1 plss"
  • UnpinCodesProposal
// UnpinCodesProposal gov proposal content type to unpin a set of code ids in
// the wasmvm cache.
message UnpinCodesProposal {
  // Title is a short summary
  string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
  // Description is a human readable text
  string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
  // CodeIDs references the WASM codes
  repeated uint64 code_ids = 3 [
    (gogoproto.customname) = "CodeIDs",
    (gogoproto.moretags) = "yaml:\"code_ids\""
  ];
}
 wasmd tx gov submit-proposal unpin-codes 1 --title "Unpin code 1" --description "Unpin code 1 plss" --from wallet

15. Testing

Unit Testing

Basic import

#[cfg(test)]
mod tests {
  use super::*;
  use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR};
  use cosmwasm_std::{attr, coins, CosmosMsg};

Test Initialization

#[test]
fn proper_initialization() {
  let mut deps = mock_dependencies(&[]);

  let msg = InitMsg {
    counter_offer: coins(40, "ETH"),
    expires: 100_000,
  };
  let info = mock_info("creator", &coins(1, "BTC"));

  // we can just call .unwrap() to assert this was a success
  let res = init(deps.as_mut(), mock_env(), info, msg).unwrap();
  assert_eq!(0, res.messages.len());

  // it worked, let's query the state
  let res = query_config(deps.as_ref()).unwrap();
  assert_eq!(100_000, res.expires);
  assert_eq!("creator", res.owner.as_str());
  assert_eq!("creator", res.creator.as_str());
  assert_eq!(coins(1, "BTC"), res.collateral);
  assert_eq!(coins(40, "ETH"), res.counter_offer);
}
  • Mock Dependecies
/// All external requirements that can be injected for unit tests.
/// It sets the given balance for the contract itself, nothing else
pub fn mock_dependencies(
  contract_balance: &[Coin],
) -> OwnedDeps<MockStorage, MockApi, MockQuerier> {
  let contract_addr = HumanAddr::from(MOCK_CONTRACT_ADDR);
  OwnedDeps {
    storage: MockStorage::default(),
    api: MockApi::default(),
    querier: MockQuerier::new(&[(&contract_addr, contract_balance)]),
  }
}
  • Environment
/// Returns a default enviroment with height, time, chain_id, and contract address
/// You can submit as is to most contracts, or modify height/time if you want to
/// test for expiration.
///
/// This is intended for use in test code only.
pub fn mock_env() -> Env {
  Env {
    block: BlockInfo {
      height: 12_345,
      time: 1_571_797_419,
      time_nanos: 879305533,
      chain_id: "cosmos-testnet-14002".to_string(),
    },
    contract: ContractInfo {
      address: HumanAddr::from(MOCK_CONTRACT_ADDR),
    },
  }
}
  • Message Info
/// Just set sender and sent funds for the message. The essential for
/// This is intended for use in test code only.
pub fn mock_info<U: Into<HumanAddr>>(sender: U, sent: &[Coin]) -> MessageInfo {
  MessageInfo {
    sender: sender.into(),
    sent_funds: sent.to_vec(),
  }
}

Test Handler

  • Test Transfer Handler
#[test]
fn transfer() {
  let mut deps = mock_dependencies(&[]);

  let msg = InitMsg {
    counter_offer: coins(40, "ETH"),
    expires: 100_000,
  };
  let info = mock_info("creator", &coins(1, "BTC"));

  // we can just call .unwrap() to assert this was a success
  let res = init(deps.as_mut(), mock_env(), info, msg).unwrap();
  assert_eq!(0, res.messages.len());

  // random cannot transfer
  let info = mock_info("anyone", &[]);
  let err = handle_transfer(deps.as_mut(), mock_env(), info, HumanAddr::from("anyone"))
    .unwrap_err();
  match err {
    ContractError::Unauthorized {} => {}
    e => panic!("unexpected error: {}", e),
  }

  // owner can transfer
  let info = mock_info("creator", &[]);
  let res =
    handle_transfer(deps.as_mut(), mock_env(), info, HumanAddr::from("someone")).unwrap();
  assert_eq!(res.attributes.len(), 2);
  assert_eq!(res.attributes[0], attr("action", "transfer"));

  // check updated properly
  let res = query_config(deps.as_ref()).unwrap();
  assert_eq!("someone", res.owner.as_str());
  assert_eq!("creator", res.creator.as_str());
}
  • Test Execute
#[test]
fn execute() {
  let mut deps = mock_dependencies(&[]);

  let amount = coins(40, "ETH");
  let collateral = coins(1, "BTC");
  let expires = 100_000;
  let msg = InitMsg {
    counter_offer: amount.clone(),
    expires: expires,
  };
  let info = mock_info("creator", &collateral);

  // we can just call .unwrap() to assert this was a success
  let _ = init(deps.as_mut(), mock_env(), info, msg).unwrap();

  // set new owner
  let info = mock_info("creator", &[]);
  let _ = handle_transfer(deps.as_mut(), mock_env(), info, HumanAddr::from("owner")).unwrap();

  // random cannot execute
  let info = mock_info("creator", &amount);
  let err = handle_execute(deps.as_mut(), mock_env(), info).unwrap_err();
  match err {
    ContractError::Unauthorized {} => {}
    e => panic!("unexpected error: {}", e),
  }

  // expired cannot execute
  let info = mock_info("owner", &amount);
  let mut env = mock_env();
  env.block.height = 200_000;
  let err = handle_execute(deps.as_mut(), env, info).unwrap_err();
  match err {
    ContractError::OptionExpired { expired } => assert_eq!(expired, expires),
    e => panic!("unexpected error: {}", e),
  }

  // bad counter_offer cannot execute
  let msg_offer = coins(39, "ETH");
  let info = mock_info("owner", &msg_offer);
  let err = handle_execute(deps.as_mut(), mock_env(), info).unwrap_err();
  match err {
    ContractError::CounterOfferMismatch {
      offer,
      counter_offer,
    } => {
      assert_eq!(msg_offer, offer);
      assert_eq!(amount, counter_offer);
    }
    e => panic!("unexpected error: {}", e),
  }

  // proper execution
  let info = mock_info("owner", &amount);
  let res = handle_execute(deps.as_mut(), mock_env(), info).unwrap();
  assert_eq!(res.messages.len(), 2);
  assert_eq!(
    res.messages[0],
    CosmosMsg::Bank(BankMsg::Send {
      from_address: MOCK_CONTRACT_ADDR.into(),
      to_address: "creator".into(),
      amount,
    })
  );
  assert_eq!(
    res.messages[1],
    CosmosMsg::Bank(BankMsg::Send {
      from_address: MOCK_CONTRACT_ADDR.into(),
      to_address: "owner".into(),
      amount: collateral,
    })
  );

  // check deleted
  let _ = query_config(deps.as_ref()).unwrap_err();
}

Test Handler

Integration Testing

  • cw-multi-test 이용
  • testnet에 deploy하지 않고도 스마트 컨트랙트를 테스트할 수 있게 해줌
  • 기본 testing import 파일
use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR};
use cw_multi_test::{App, BankKeeper, Contract, ContractWrapper};

App

  • blockchain app을 표현하는 기본적인 entry point가 App
  • 이는 여러 블록을 시뮬레이트 할수 있게하는 block height, time 정보를 담고 있음
  • app.update_block(next_block)을 사용하여 timestamp를 5초 증가 시키고, 블록 height를 1 올리는 것이 가능
  • CosmosMsg를 실행하는 App.execute 엔트리 포인트를 노출함
  • Querier 인터페이스를 구현하는 엔트리 포인트 또한 존재
  • App.wrap()을 통해 QuerierWrapper를 가져와서 블록체인에 쿼리 가능한 API를 제공 가능
// Test code를 위한 App 생성
fn mock_app() -> App {
    let env = mock_env();
    let api = Box::new(MockApi::default());
    let bank = BankKeeper::new();

    App::new(api, env.block, bank, Box::new(MockStorage::new()))
}

Mocking contracts

  • 우선 어떤 컨트랙트이던 테스트를 위해선 mocked 되거나 wrapped up 되어야함
  • cw-multi-testContractWrapper를 통해 컨트랙트를 wrap 하여 mocked network에 배포함
  • 아래 예시에서는 execute, instantiate, query, reply 함수를 import 하여 런타임에 컨트랙트에 의해 사용되고, wrapper를 테스트에 사용함 (컨트랙트가 reply 함수를 구현하지 않았으면 with_reply가 필요없을 수 있음)
  • 컨트랙트를 mocking한 후에 코드를 저장하고 code object로 부터 컨트랙트를 세팅해야 함 (testnet이나 mainnet에 배포할 때도 동일한 프로세스를 거침)
use crate::contract::{execute, instantiate, query, reply};

pub fn contract_stablecoin_exchanger() -> Box<dyn Contract<Empty>>{
    let contract = ContractWrapper::new_with_empty(
        execute,
        instantiate,
        query,
    ).with_reply(reply);
    Box::new(contract)
}

Storing and Instantiating a Contract

  • No ContractDataContract '<contract>' does not exist 에러 발생 시 목킹이 잘못된 것
// 컨트랙트 저장
let contract_code_id = router.store_code(contract_stablecoin_exchanger());

// 컨트랙트 인스턴스화
let mocked_contract_addr = router
        .instantiate_contract(contract_code_id, owner.clone(), &msg, &[], "super-contract", None)
        .unwrap();

Putting it all Together

use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR};
use cw_multi_test::{App, BankKeeper, Contract, ContractWrapper};
use crate::contract::{execute, instantiate, query, reply};
use crate::msg::{InstantiateMsg, QueryMsg}

fn mock_app() -> App {
    let env = mock_env();
    let api = Box::new(MockApi::default());
    let bank = BankKeeper::new();

    App::new(api, env.block, bank, Box::new(MockStorage::new()))
}

pub fn contract_counter() -> Box<dyn Contract<Empty>>{
    let contract = ContractWrapper::new_with_empty(
        execute,
        instantiate,
        query,
    );
    Box::new(contract)
}

pub fn counter_instantiate_msg(count: Uint128) -> InstantiateMsg {
    InstantiateMsg {
        count: count
    }
}

#[test]
fn counter_contract_multi_test() {
    // Create the owner account
    let owner = Addr::unchecked("owner");
    let mut router = mock_app();

    let counter_contract_code_id = router.store_code(contract_counter());
    // Setup the counter contract with an initial count of zero
    let init_msg = InstantiateMsg {
        count: Uint128::zero()
    }
    // Instantiate the counter contract using its newly stored code id 
    let mocked_contract_addr = router
        .instantiate_contract(counter_contract_code_id, owner.clone(), &init_msg, &[], "counter", None)
        .unwrap();

    // We can now start executing actions on the contract and querying it as needed
    let msg = ExecuteMsg::Increment {}
    // Increment the counter by executing the above prepared msg on the previously setup contract
    let _ = router.execute_contract(
            owner.clone(),
            mocked_contract_addr.clone(),
            &msg,
            &[],
        )
        .unwrap();
    // Query the contract to verify the counter was incremented
    let config_msg =  QueryMsg::Count{};
    let count_response: CountResponse = router
        .wrap()
        .query_wasm_smart(mocked_contract_addr.clone(), &config_msg)
        .unwrap();
    asserteq!(count_response.count, 1)

    // Now lets reset the counter with the other ExecuteMsg
    let msg = ExecuteMsg::Reset {}
    let _ = router.execute_contract(
            owner.clone(),
            mocked_contract_addr.clone(),
            &msg,
            &[],
        )
        .unwrap();

    // And again use the available contract query to verify the result 
    // Query the contract to verify the counter was incremented
    let config_msg =  QueryMsg::Count{};
    let count_response: CountResponse = router
        .wrap()
        .query_wasm_smart(mocked_contract_addr.clone(), &config_msg)
        .unwrap();
    asserteq!(count_response.count, 0)
}

Mocking 3rd party contracts

pub fn contract_ping_pong_mock() -> Box<dyn Contract<Empty>> {
    let contract = ContractWrapper::new(
        |deps, _, info, msg: MockExecuteMsg| -> StdResult<Response> {
            match msg {
                MockExecuteMsg::Receive(Cw20ReceiveMsg {
                    sender: _,
                    amount: _,
                    msg,
                }) => {
                    let received: PingMsg = from_binary(&msg)?;
                    Ok(Response::new()
                        .add_attribute("action", "pong")
                        .set_data(to_binary(&received.payload)?))
                }
                }})}

        |_, _, msg: MockQueryMsg| -> StdResult<Binary> {
            match msg {
                MockQueryMsg::Pair {} => Ok(to_binary(&mock_pair_info())?),

16. Sudo Execution

Extra

Building the Contract

  • optimizing 하기 전에 에러 체크
cargo wasm
  • optimizing
    • WASM binary 결과를 fee를 최대한 줄이고 블록체인 상에서의 크기를 줄이기 위해 가능한 작게 해야함
    • optimized 결과는 artifacts/my_first_contract.wasm 디렉토리에 있음
    • Cargo.toml을 수정하여 손쉽게 실행하는 것도 가능
docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/rust-optimizer:0.12.0

Schemas

  • JSON-schema를 자동 생성하기 위해, 우리가 사용하는 schema를 위한 각각의 데이터 struct를 등록해야함
// examples/schema.rs

use std::env::current_dir;
use std::fs::create_dir_all;

use cosmwasm_schema::{export_schema, remove_schemas, schema_for};

use my_first_contract::msg::{CountResponse, ExecuteMsg, InstantiateMsg, QueryMsg};
use my_first_contract::state::State;

fn main() {
  let mut out_dir = current_dir().unwrap();
  out_dir.push("schema");
  create_dir_all(&out_dir).unwrap();
  remove_schemas(&out_dir).unwrap();

  export_schema(&schema_for!(InstantiateMsg), &out_dir);
  export_schema(&schema_for!(ExecuteMsg), &out_dir);
  export_schema(&schema_for!(QueryMsg), &out_dir);
  export_schema(&schema_for!(State), &out_dir);
  export_schema(&schema_for!(CountResponse), &out_dir);
}
  • schema build
cargo schema
  • 새롭게 생성된 schema는 schema/ 디렉토리에서 볼수 있어야하며 아래는 schema/query_msg.json 예시
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "QueryMsg",
  "anyOf": [
    {
      "type": "object",
      "required": [
        "get_count"
      ],
      "properties": {
        "get_count": {
          "type": "object"
        }
      },
      "additionalProperties": false
    }
  ]
}

profile
2929

0개의 댓글