CosmWasm 기본 개념 및 구조

2023. 7. 6. 11:21Blockchain/Smart contract

728x90
반응형

CosmWasm은 Cosmos 기반 블록체인에서 사용하는 web assembly 기반 smart contract이다. Cosmos-SDK 기반 체인에서 CosmWasm contract를 운용하기 위해서는 wasmvm 사용이 필요하여 wasm module이 포함되어야 한다. 따라서 app chain에 wasm module이 없는 chain의 경우에는 cosmwasm contract가 동작할 수 없다.

이번 글에서는 CosmWasm 대한 기초적인 분석을 진행할 것이며, 이 컨트랙트를 처음 접하는 사람도 기본적인 내용은 얻을 수 있도록 해볼것이다.

Features

medium.com, Ethan Frey


가장 먼저 ComsWasm의 창시자 Ethan Frey가 게시한 CosmWasm과 Cosmos-SDK, solidity의 비교표를 확인하면 좋을것 같다. 표에서 확인할 수 있듯이 CosmWasm은 같은 smart contract 비교 대상인 solidity에 비해 많은 장점을 보유하고 있다. "Level of Customization"과 "Native Chain Features"는 기본적으로 CosmWasm 하나만 두고는 지원될 수 없지만 cosmos-sdk와 결합하면 충분히 지원할 수 있다 밝혔다. 이 외에 CosmWasm이 solidity에 비해 떨어지는 것은 "Learning Curve"이다. Solidity는 비교적 쉬운 문법과 구조로 처음 ethereum을 접하는 개발자들도 몇번 만져보면 동작 방식을 이해할 수 있다. 그에 비해 CosmWasm은 rust를 사용하며, 언어적 특성으로 인해 solidity보다 배우기 어렵다. 현 상황에서도 대부분의 dApp이 solidity이다 보니 가이드나 강의가 solidity로 구성된 것이 많고 CosmWasm의 경우는 자료가 비교적 없어서 이를 이해하는 데에도 힘이 든다.

개인적으로 두 contract를 모두 개발 해봤던 경험을 바탕으로 몇가지는 동의하고 몇가지는 동의하지 못할 것 같다.

Cosmwasm의 "Security"는 확실히 solidity보다 뛰어나다. 설계상 모든 재진입 공격을 방지할 수 있다. Solidity의 경우 contract를 실행시킬 때 다른 contract를 호출할 수 있어서 ethereum의 state 관리에 많은 주의를 요하고, 이를 통해 hacking 사례도 존재한다(Reentrancy attack). 하지만 CosmWasm의 경우는 contract가 다른 contract를 직접 호출하는 것이 불가능하다. Send, Receive처럼 다른 contract를 호출할 때에는 처음 contract를 완전히 다 실행시킨 후 Responseadd_message를 통해 다른 contract를 호출하는 방식을 취한다. 이 때 호출되는 message가 실패한다면 이전에 실행시켰던 모든 상태 변경 tx를 원복 시키기 때문에 state 관리가 더 용이하다.
"Ease of Testing" 또한 mock_dependencies등의 라이브러리를 통해 훌륭한 테스트 코드를 만들 수 있도록 지원하며 "Complexity of Logic" 또한 getrandom 등 일부를 제외한 deterministic을 해하지 않는 선에서의 rust 라이브러리를 활용할 수 있기 떄문에 더 복잡한 비즈니스 로직을 구현할 수 있다. 그 외 대부분의 의견에 동의한다.

위 표의 "Speed of Development"나 "Ease of Development"에 대해서는 동의하지 못할것 같다. 개발 속도나 용이성은 확실해 solidity보다 떨어지는 것 같다. Solidity의 경우 Remix라는 solidity 환경 최고의 IDE가 존재하고 Truffle 같은 deploy tool, Ganache 같은 쉬운 테스트넷 구현 SDK가 뒷받침하고 있다. Remix를 통해 개발 중간중간 진행 상황 테스트를 하며 개발할 수 있는 solidity에 반해 CosmWasm은 로직 변경 후 compile하고 테스트를 진행해야 하는데, 또 rust가 compile이 비교적 느린편이라 기다리다 답답한 적도 있었다. 존재하는지는 잘 모르겠지만 CosmWasm 전용 IDE가 없는 것도 개발 속도나 용이성에 마이너스 점수를 주고 싶다. 또한 Rust가 엄격한 언어이기 때문에 문법을 지켜 완벽하게 동작하도록 구성해야만 다음 단계로 넘어갈 수 있는것도 한 몫한다.
하지만 만약 개발의 범위가 security를 지키는 로직의 완성까지로 본다면 solidity와 비등하다고 생각한다. 앞서 언급한 reentrancy attack 말고도 solidity contract가 근본적으로 가지고 있는 문제점에 대해 보완을 하고 있기 때문에 해당 부분에 대해서는 많이 고민하지 않고도 로직을 구성할 수 있기에, 전체적인 개발 속도나 용이성에서는 solidity와 비견될 수 있다고 본다.

Pros

CosmWasm의 장점에 대하여 의견을 곁들여 나열해본다.

Deploy

CosmWasm과 solidity는 배포하는 방식이 전혀 다르다. Solidity의 경우 개발한 contract를 deploy하면 바로 contract address가 생성이 되고 EVM 상에서 bytecode가 동작한다. 하지만 같은 기능을 가진 여러 contract 배포가 필요할 시에는 같은 bytecode를 필요할 때 마다 배포를 해야한다. Openzeppelin의 ERC20 코드는 똑같은 코드가 ethereum 상에 수도 없이 배포되었을 것이다.

하지만 CosmWasm은 이 deploy 과정을 두 단계로 나눴다. "Store"와 "Instantiate"이다. Store는 CosmWasm의 wasm파일을 체인 상에 등록하는 과정이다. Store tx를 전송하면 각 노드는 자신의 경로에 이 wasm 코드를 저장하며, 체인 상에서는 숫자로 표현되는 Code ID로 각 wasm을 구별할 수 있다. Instantiate는 store된 wasm을 이용해 사용자가 접근할 수 있는 contract address를 생성하며 기본적인 config 또한 설정할 수 있다. 같은 code ID를 이용하여 여러번 instantiate 할 수 있기 때문에 각기 다른 contract address를 가지고 있으면서도 하나의 wasm 만을 바라보고 동작을 할 수 있어 storage 낭비 방지에 큰 역할을 할 수 있다.

Interchain transfer (IBC transfer)

CosmWasm은 IBC를 지원한다. 그렇기 때문에 relayer를 통해서 인터체인 간 contract를 동작 시킬 수 있다. 그래서 wasm module을 기본 app chain에 추가할 때 app.go에서 wasm keeper는 IBC keeper를 mandatory로 받아야 한다. Solidity는 외부 체인에 접근하기 어려운 반면 IBC를 통해 CosmWasm은 체인 밖으로 영역을 넓힐 수 있기 때문에 scalability에 큰 장점을 가지고 있다.

Rust-optimizer

Rust optimizer는 개인적으로 CosmWasm이 제공하는 가장 큰 메리트라 생각한다.
그냥 cargo build를 사용해서도 wasm 파일을 생성할 수 있다. Rust optimizer는 같은 기능을 수행하지만 wasm 파일의 크기가 비교도 안되게 작아진다. 동일한 컨트랙트에 대해 기본 빌드를 수행하면 2MB가 넘어가는 반면 rust optimizer를 이용한 wasm 파일은 100k가 조금 넘는 수준이다. Cosmos chain에는 max tx byte나 max block size가 존재하기 때문에 contract의 사이즈가 커지면 배포 자체가 안된다. Optimizer가 사이즈를 확연히 줄여주기 때문에 조금 더 복잡한 비즈니스 로직도 contract에 구현할 수 있다. 또한 docker로 image를 받아 실행하면 되기 때문에 사용도 간편하다.

Migration

CosmWasm migration에 대한 앞선 글에서 처럼 CosmWasm은 배포된 contract에 대한 로직 수정 및 버그 fix가 가능하다. 물론 Solidity도 proxy contract를 사용하여 이 migration 비슷한 동작을 수행할 수 있지만 CosmWasm이 제공하는 migration 방법에 비해서는 많이 복잡한 것이 사실이다. Migration은 contract를 운영하는데 큰 도움이 되는 기능이다.

Complexity of Logic

위의 표에서 언급했던 것처럼 복잡한 비즈니스 로직이 구축가능하다. 이는 뛰어난 rust 라이브러리의 도움을 받을 수도 있는 것이 가장 크다. Solidity도 Openzeppelin처럼 좋은 라이브러리가 있지만 rust 생태계의 라이브러리에 비하면 수가 너무 적다. 만약 지원되는 라이브러리가 적다면 원하는 비즈니스 로직을 충족시키기 위해 바닥부터 쌩으로 개발 할 수도 있는데 CosmWasm은 rust 라이브러리로 이 문제를 해결할 수 있다.
또한 CosmWasm이 제공하는 cosmwasm-std 등의 기본 라이브러리가 너무 잘 되어 있다. Contract를 통한 이코노미 로직 구축에 필요한 대부분이 이 라이브러리가 제공한다.

Testing

테스트 지원 라이브러리도 굉장히 잘되어 있다. cosmwasm-std 내의 mock_dependencies, mock_env, mock_info 등을 가져오면 바로 블록체인 위에 올라가 있는 것처럼 동작을 지원한다.

이 밖에도 많은 장점이 있지만 대표적으로는 이 정도로 볼 수 있을 것 같다.

Cons

CosmWasm도 단점이 존재할 것이다. 사실 이는 rust에서 오는 단점이 대부분이다. Learning curve가 확실이 있다. 처음 rust를 접하면서 contract를 개발할 때에는 처음 solidity를 접했던 것과는 큰 차이가 있었다. 물론 rust를 알고 있는 사람이면 시작이 다를 수도 있겠지만.

배포를 장점으로 적었지만 거기서 오는 단점도 존재한다. 배포 과정이 한 단계 더 들어가 있어 복잡해 졌으며 tx를 두번 날려야 하기 때문에 fee가 결국 더 들어간다. 배포 다양성도 부족한데, solidity는 remix에서도 바로 deploy가 가능하고 truffle 등 배포 tool이 있는 반면에 CosmWasm은 CLI로 배포하거나 gRPC, HTTP REST 등으로 배포할 수 밖에 없다. 이 뜻은 기본적인 Cosmos-SDK의 API 기능 사용법을 알고 있는 사람만이 사용할 수 있으며, CosmWasm contract에 대한 지식 전에 Cosmos-SDK의 지식을 요하게 된다.

Contract 형식이 과도하게 복잡한 점도 있다. State type, execute/query message, function logic, lib export 등 contract를 구성하는 architecture 대부분이 개발자가 설정해야 한다. Solidity도 비슷하다 볼 수 있지만 rust의 어려움으로 인해 비교가 많이 된다.

앞서 계속 언급했던 지원 tool이 부족한것도 단점이다. dApp 개발자가 접근하기 어려운 것이 이런 SDK 등이 많이 없는것 때문도 있을 것 같다. 지원 tool이 개발 framework Sylvia 등 극소수이다.

Wasm module

Wasm module이 제공하는 기능을 CLI 상에서 살펴보려 한다.

Tx

Available Commands:
  clear-contract-admin Clears admin for a contract to prevent further migrations
  execute              Execute a command on a wasm contract
  instantiate          Instantiate a wasm contract
  migrate              Migrate a wasm contract to a new code version
  set-contract-admin   Set new admin for a contract
  store                Upload a wasm binary
  • store
    • wasmd tx wasm store [wasm-file-path]
    • Wasm file을 업로드 한다.
  • instantiate
    • wasmd tx wasm instantiate [code_id_int64] [json_encoded_init_args] --label [text] --admin [address,optional] --amount [coins,optional] [flags]
    • wasm 파일을 인스턴스화 한다. Store한 뒤 확인되는 code ID를 이용한다.
  • execute
    • wasmd tx wasm execute [contract-addr] [execute-msg]
    • contract를 실행한다. instantiate 이후 확인되는 contract address를 설정해야 하며, execute message는 contract상에 정의되어 있는 message에서 Option<>을 제외한 동일한 파라미터 key-value를 입력해야 한다.
  • migrate
    • wasmd tx wasm migrate [contract_addr_bech32] [new_code_id_int64] [json_encoded_migration_args] [flags]
    • wasm 파일을 변경하는 migrate 과정을 수행한다. 미리 store를 통해 변경할 wasm 파일을 업로드 한 뒤 code ID를 확인하여 migration을 진행한다.
  • admin
    • wasmd tx wasm set-contract-admin [contract_addr_bech32] [new_admin_addr_bech32] [flags]
    • wasmd tx wasm clear-contract-admin [contract_addr_bech32] [flags]
    • contract의 migrate 등 기능을 실행할 수 있는 권한을 가진 admin에 대한 설정 tx이다.

Query

Available Commands:
  code                  Downloads wasm bytecode for given code id
  code-info             Prints out metadata of a code id
  contract              Prints out metadata of a contract given its address
  contract-history      Prints out the code history for a contract given its address
  contract-state        Querying commands for the wasm module
  libwasmvm-version     Get libwasmvm version
  list-code             List all wasm bytecode on the chain
  list-contract-by-code List wasm all bytecode on the chain for given code id
  pinned                List all pinned code ids
  • contract-state
    • all : contract address가 가지고 있는 전체 internal state를 확인한다.
    • raw : contract address가 가지고 있는 state 중 key에 따라 확인한다.
    • smart : contract address가 가지고 있는 state를 query message를 통해 확인한다. 일반적인 contract query는 이 명령어를 수행한다.
  • list-code
    • 전체 wasm bytecode의 list를 확인한다.
  • list-contract-by-code
    • 한 code ID에서 instantiate된 contract의 list를 확인한다.
  • code-info
    • wasm byte code의 metadata를 확인한다.
  • contract
    • creator, admin, code ID, label 등 contract가 가지고 있는 정보를 확인한다.
  • pinned
    • 특정 wasm 파일을 메모리에 pinning한다. contract의 in-memory를 매 실행하다 실시하지 않아서 성능이 향상된다.

Architecture & Functions

CosmWasm contract의 기본적인 아키텍처와 가지고 있는 function을 확인한다. 아래 tree는 cw-nfts의 구조를 나타내며, 앞으로의 설명은 cw721-base가 기준이다.

├── Cargo.lock
├── Cargo.toml
├── LICENSE-APACHE
├── Makefile.toml
├── README.md
├── artifacts
│   ├── checksums.txt
│   ├── checksums_intermediate.txt
│   ├── cw721_base-aarch64.wasm
├── contracts
│   ├── cw721-base
│   │   ├── Cargo.toml
│   │   ├── NOTICE
│   │   ├── README.md
│   │   ├── examples
│   │   │   └── schema.rs
│   │   ├── helpers.ts
│   │   ├── schema
│   │   │   └── cw721-base.json
│   │   └── src
│   │       ├── contract_tests.rs
│   │       ├── error.rs
│   │       ├── execute.rs
│   │       ├── helpers.rs
│   │       ├── lib.rs
│   │       ├── msg.rs
│   │       ├── query.rs
│   │       └── state.rs

보이는 것처럶 cargo package manager를 사용하기 위한 Cargo.tomlCargo.lock을 확인할 수 있다. Cargo.toml에서 CosmWasm contract에 사용될 라이브러리를 등록하면 cargo package에서 버전별로 가져와 사용 가능하다.
Makefile.tomlcargo make 명령을 통해 실행할 수 있는 make 파일이다. cargo make optimize 등 사용자들이 쉽게 install, build 및 run을 할 수 있도록 지원한다.
artifacts 폴더는 기본적으로 존재하지 않으나 rust optimizer를 사용하면 wasm 파일이 해당 디렉토리에 생성된다. 배포할 시에 target 폴더 말고 artifacts에 생성된 wasm을 사용해야 한다.

contracts 폴더는 실제 CosmWasm contract를 구현하기 위한 코드가 존재한다. 위 tree는 cw721-base이지만 다른 CosmWasm contract도 유사한 구조를 가진다.
lib.rs는 contract의 entry point이다. 여기서 #[entry_point]로 지정된 function에서만 외부에서 진입할 수 있다.
state.rs는 contract가 사용하는 state DB의 structure를 관리한다.
execute.rs에는 contract의 execute functions가 있으며 query.rs에는 contract의 query functions가 존재한다.
msg.rs는 execute와 query를 하기 위한 contract의 message가 정의 되어 있다.
error.rs는 해당 contract가 ContractError를 사용하기 위한 에러 리스트가 정의 되어 있다.

Instantiate

pub fn instantiate(
    &self,
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response<C>> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    let info = ContractInfoResponse {
        name: msg.name,
        symbol: msg.symbol,
    };
    self.contract_info.save(deps.storage, &info)?;
    let minter = deps.api.addr_validate(&msg.minter)?;
    self.minter.save(deps.storage, &minter)?;
    Ok(Response::default())
}

execute.rs 내에는 execute function 뿐만 아니라 instantiate function도 존재한다. Wasm contract를 인스턴스화 하기 위한 function이며 기본적인 configuration도 instantiate 시에 저장할 수 있다. cw721에서는 contract info와 minter를 instantiate시에 설정, 저장한다. 이 function을 실행하면 resposne로 contract address를 얻을 수 있다.

Execute

pub fn execute(
    &self,
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg<T, E>,
) -> Result<Response<C>, ContractError> {
    match msg {
        ExecuteMsg::Mint(msg) => self.mint(deps, env, info, msg),
        ...

Execute 메시지가 인입되면 각 msg의 타입에 맞게 match를 통하여 각각의 execute function들이 실행된다. Execute function 만이 contract의 state DB를 변경할 수 있으며 이는 오직 실제로 transaction을 send 해야지만 실행 가능하다. 따라서 이 execute function들은 gas fee가 필요하다.

Query

pub fn query(
    &self, 
        deps: Deps, 
        env: Env, 
        msg: QueryMsg<Q>
) -> StdResult<Binary> {
    match msg {
        QueryMsg::Minter {} => to_binary(&self.minter(deps)?),
        ...

query.rs에 정의되어 있는 function이다. Query 메시지가 인입되면 각 msg의 타입에 맞게 각각의 query function들이 실행된다. Execute와 반대로 state DB를 변경할 수 없으며 오직 state DB의 조회만이 가능하다. 따라서 transaction을 send 하지 않아도 동작 할 수 있으며 gas fee는 발생하지 않는다.

Message

msg.rs에 정의 되어 있는 structure이다. instantiate, execute, query function을 실행하기 위한 메시지가 정의 되어 있다.

InstantiateMsg

pub struct InstantiateMsg {
    /// Name of the NFT contract
    pub name: String,
    /// Symbol of the NFT contract
    pub symbol: String,

    /// The minter is the only one who can create new NFTs.
    /// This is designed for a base NFT that is controlled by an external program
    /// or contract. You will likely replace this with custom logic in custom NFTs
    pub minter: String,
}

cw721-base의 instantiate에는 NFT contract의 name 및 symbol과 NFT token ID를 발급할 수 있는 권한을 가진 minter의 account address를 설정한다. Name과 symbol은 contract_info state에 저장되며 minter는 minter state에 저장되는데, 이 초기 값들은 코드를 customize하지 않는 이상 변경 할 수는 없다.

ExecuteMsg

pub enum ExecuteMsg<T, E> {
    /// Transfer is a base message to move a token to another account without triggering actions
    TransferNft { recipient: String, token_id: String },
    /// Send is a base message to transfer a token to a contract and trigger an action
    /// on the receiving contract.
    SendNft {
        contract: String,
        token_id: String,
        msg: Binary,
    },
    /// Allows operator to transfer / send the token from the owner's account.
    /// If expiration is set, then this allowance has a time/height limit
    Approve {
        spender: String,
        token_id: String,
        expires: Option<Expiration>,
    },
    /// Remove previously granted Approval
    Revoke { spender: String, token_id: String },
    /// Allows operator to transfer / send any token from the owner's account.
    /// If expiration is set, then this allowance has a time/height limit
    ApproveAll {
        operator: String,
        expires: Option<Expiration>,
    },
    /// Remove previously granted ApproveAll permission
    RevokeAll { operator: String },

    /// Mint a new NFT, can only be called by the contract minter
    Mint(MintMsg<T>),

    /// Burn an NFT the sender has access to
    Burn { token_id: String },

    /// Extension msg
    Extension { msg: E },
}

#[cw_serde]
pub struct MintMsg<T> {
    /// Unique ID of the NFT
    pub token_id: String,
    /// The owner of the newly minted NFT
    pub owner: String,
    /// Universal resource identifier for this NFT
    /// Should point to a JSON file that conforms to the ERC721
    /// Metadata JSON Schema
    pub token_uri: Option<String>,
    /// Any custom extension used by this contract
    pub extension: T,
}

cw721-base에서는 기본적으로 9가지의 message가 정의되어 있다. 먼저 token ID를 새롭게 발급할 수 있는 MintMsg가 있다. 새로 생성되는 token_id는 contract에 유일한 값으로 존재해야 하며 따로 형식은 존재하지 않고 contract minter가 알아서 설정 가능하다. 그리고 token ID의 주인인 owner 설정이 필요한데, 특정 token ID에 대한 execute를 수행할 수 있기 때문에 발급된 token ID에 대해서는 minter도 transferNft 등을 수행할 수 없고 오직 여기 설정된 owner만이 수행 가능하다. Option으로 NFT를 특정할 수 있는 token_uri 파라미터가 존재하며 generic type으로 extension 필드가 있다. 이 genenric은 lib.rs에서 설정한 type으로 받아 들일 수 있고, 대표적으로 cw721-metadata-onchain에서 NFT의 opensea metadata를 지원할 때 extension 필드에서 사용된다.

TransferNftSendNft는 token ID owner만 수행 가능하며, token ID의 소유권을 다른 account로 넘기고자 할 때 사용된다. TransferNft는 owner가 recipient로 state에서 변경되며 SendNft는 owner가 다른 CosmWasm contract address로 변경된다. SendNft의 receipient contract는 Cw721ReceiveMsg를 전달 받으며 이를 통해 token ID를 수령했을 때 원하는 로직을 구현할 수 있다.

ApproveApproveAll은 token ID에 대한 execute를 owner가 지정한 다른 account도 수행할 수 있도록 제공하는 기능으로, Cosmos-SDK의 authz나 feegrant 모듈이 제공하는 기능과 비슷한 역할을 한다. Approve는 A라는 owner가 가지고 있는 것 중 message내에 입력한 특정 token ID에 대한 transfer/send 등의 execute를 수행할 수 있는 권한을 가지며, ApproveAll은 A라는 owner가 가지고 있는 모든 token ID에 대한 execute 수행 권한을 가진다. 이 권한을 가지는 account를 operator라 부르며, 만약 expires가 존재한다면 operator는 미래의 특정시간 또는 블록에 도달하기 전까지만 권한을 가질 수 있다. RevokeRevokeAll은 approve로 주었던 권한을 다시 회수하는 메시지이다.

Burn은 mint된 token ID를 삭제하는 기능으로써 이것도 token ID의 owner만 수행할 수 있다.

QueryMsg

pub enum QueryMsg<Q: JsonSchema> {
    /// Return the owner of the given token, error if token does not exist
    #[returns(cw721::OwnerOfResponse)]
    OwnerOf {
        token_id: String,
        /// unset or false will filter out expired approvals, you must set to true to see them
        include_expired: Option<bool>,
    },
    /// Return operator that can access all of the owner's tokens.
    #[returns(cw721::ApprovalResponse)]
    Approval {
        token_id: String,
        spender: String,
        include_expired: Option<bool>,
    },
    /// Return approvals that a token has
    #[returns(cw721::ApprovalsResponse)]
    Approvals {
        token_id: String,
        include_expired: Option<bool>,
    },
    /// List all operators that can access all of the owner's tokens
    #[returns(cw721::OperatorsResponse)]
    AllOperators {
        owner: String,
        /// unset or false will filter out expired items, you must set to true to see them
        include_expired: Option<bool>,
        start_after: Option<String>,
        limit: Option<u32>,
    },
    /// Total number of tokens issued
    #[returns(cw721::NumTokensResponse)]
    NumTokens {},

    /// With MetaData Extension.
    /// Returns top-level metadata about the contract
    #[returns(cw721::ContractInfoResponse)]
    ContractInfo {},
    /// With MetaData Extension.
    /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema*
    /// but directly from the contract
    #[returns(cw721::NftInfoResponse<Q>)]
    NftInfo { token_id: String },
    /// With MetaData Extension.
    /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization
    /// for clients
    #[returns(cw721::AllNftInfoResponse<Q>)]
    AllNftInfo {
        token_id: String,
        /// unset or false will filter out expired approvals, you must set to true to see them
        include_expired: Option<bool>,
    },

    /// With Enumerable extension.
    /// Returns all tokens owned by the given address, [] if unset.
    #[returns(cw721::TokensResponse)]
    Tokens {
        owner: String,
        start_after: Option<String>,
        limit: Option<u32>,
    },
    /// With Enumerable extension.
    /// Requires pagination. Lists all token_ids controlled by the contract.
    #[returns(cw721::TokensResponse)]
    AllTokens {
        start_after: Option<String>,
        limit: Option<u32>,
    },

    /// Return the minter
    #[returns(MinterResponse)]
    Minter {},

    /// Extension query
    #[returns(())]
    Extension { msg: Q },
}

위는 query의 리스트이며 각각의 message를 contract에 전송했을 때의 결과에 대해서 정리를 해본다.

  • OwnerOf: 특정 token ID에 대한 owner 정보를 확인하며, owner address 및 approve address가 확인힌다.
  • Approval: spender로 요청한 account에 대해서 token ID의 approve를 확인한다.
  • Approvals: 요청한 token ID에 대해 approve를 가지고 있는 모든 operator를 확인한다.
  • AllOperators: 요청한 owner에 대해 approve를 가지고 있는 모든 operator를 확인한다.
  • NumTokens: 현재 contract가 가지고 있는 token ID의 개수를 확인한다.
  • ContractInfo: Instantiate시 설정한 contract name 및 symbol등의 config를 확인한다.
  • NftInfo: 요청한 token ID에 대한 token URI와 extension 정보를 확인한다.
  • AllNftInfo: 요청한 token ID의 OwnerOf결과와 NftInfo 결과를 함께 확인한다.
  • Tokens: 요청한 owner가 가지고 있는 모든 NFT 정보를 확인한다.
  • AllTokens: 현재 contract가 가지고 있는 모든 NFT 정보를 확인한다.
  • Minter: 현재 contract의 minter를 확인한다.

Message request JSON type

위에서 살펴본 instantiate, execute, query message는 모두 json 형식으로 생성하여 호출한다. 각 메시지 파라미터가 가진 type에 맞춰 전송하면 된다. CLI 상에서 메시지를 전송 시에 base64로 json을 encoding하여 전송해야 한다.

아래 instantiate를 예시로 든다.

pub struct InstantiateMsg {
    pub name: String,
    pub symbol: String,
    pub minter: String,
}
{
    "name":"nft name example",
    "symbol":"NNE",
    "minter":"wasm12q6f4kkqttydpm9xgf4mzd4h97gh28f6evnpu2"
}

Params

cosmwasm-std에서 제공되어 CosmWasm contract의 parameter들을 정리한다. 아래 나열된 것 말고도 많은 param이 있으나 주로 사용되는 것만 정리해 보았다.

Deps

Deps는 CosmWasm contract의 모든 외부 dependency를 유지 및 관리하는 기능이다. 파라미터의 데이터 필드는 동일하지만 Mutable한 DepsMut는 state를 변경할 수 있는 execute에서 사용되며, Deps는 query에서 사용된다.

pub struct DepsMut<'a, C: CustomQuery = Empty> {
    pub storage: &'a mut dyn Storage,
    pub api: &'a dyn Api,
    pub querier: QuerierWrapper<'a, C>,
}

#[derive(Clone)]
pub struct Deps<'a, C: CustomQuery = Empty> {
    pub storage: &'a dyn Storage,
    pub api: &'a dyn Api,
    pub querier: QuerierWrapper<'a, C>,
}
  • Storage: state DB 접근
  • Api: human readable address 생성 및 sig verification 등 수행
  • QuerierWrapper: contract query instance

Env

현재 block의 상태 정보 및 contract 정보 관리를 담당한다.

pub struct Env {
    pub block: BlockInfo,
    /// Information on the transaction this message was executed in.
    /// The field is unset when the `MsgExecuteContract`/`MsgInstantiateContract`/`MsgMigrateContract`
    /// is not executed as part of a transaction.
    pub transaction: Option<TransactionInfo>,
    pub contract: ContractInfo,
}
  • BlockInfo: 실행 시점의 height, time 등의 블록 정보
  • TransactionInfo: 실행 시점의 transaction 정보(index)
  • ContractInfo: contract의 정보(address)

MessageInfo

Transaction을 통해 전달된 정보들을 확인할 수 있다.

pub struct MessageInfo {
    /// The `sender` field from `MsgInstantiateContract` and `MsgExecuteContract`.
    /// You can think of this as the address that initiated the action (i.e. the message). What that
    /// means exactly heavily depends on the application.
    ///
    /// The x/wasm module ensures that the sender address signed the transaction or
    /// is otherwise authorized to send the message.
    ///
    /// Additional signers of the transaction that are either needed for other messages or contain unnecessary
    /// signatures are not propagated into the contract.
    pub sender: Addr,
    /// The funds that are sent to the contract as part of `MsgInstantiateContract`
    /// or `MsgExecuteContract`. The transfer is processed in bank before the contract
    /// is executed such that the new balance is visible during contract execution.
    pub funds: Vec<Coin>,
}
  • sender: transaction sender (account or contract)
  • funds: transaction에 포함된 deposit token amount, 이 amount 만큼의 소유자가 contract가 된다.

Response

Transaction에 의한 contract 실행 종료 시 수행되는 파라미터, 메시지 및 확인 가능 정보를 제공한다.

pub struct Response<T = Empty> {
    /// Optional list of messages to pass. These will be executed in order.
    /// If the ReplyOn variant matches the result (Always, Success on Ok, Error on Err),
    /// the runtime will invoke this contract's `reply` entry point
    /// after execution. Otherwise, they act like "fire and forget".
    /// Use `SubMsg::new` to create messages with the older "fire and forget" semantics.
    pub messages: Vec<SubMsg<T>>,
    /// The attributes that will be emitted as part of a "wasm" event.
    ///
    /// More info about events (and their attributes) can be found in [*Cosmos SDK* docs].
    ///
    /// [*Cosmos SDK* docs]: https://docs.cosmos.network/main/core/events.html
    pub attributes: Vec<Attribute>,
    /// Extra, custom events separate from the main `wasm` one. These will have
    /// `wasm-` prepended to the type.
    ///
    /// More info about events can be found in [*Cosmos SDK* docs].
    ///
    /// [*Cosmos SDK* docs]: https://docs.cosmos.network/main/core/events.html
    pub events: Vec<Event>,
    /// The binary payload to include in the response.
    pub data: Option<Binary>,
}
  • messages: contract 실행 이후 수행할 cosmos message
  • attributes: contract 실행 이후 제공할 customized info
  • events: contract 실행 이후 contract events
  • data: binay payload data

Item, Map

State에 저장될 data 타입이다.

pub token_count: Item<'a, u64>,
pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>,
  • Item: 단일 data
  • Map: map data

Addr

Cosmos 주소를 표현하는 CosmWasm 전용 data 타입이다. Addr에는 cosmos 주소 형식이 맞는지 검증하는 기능을 보유하고 있기 때문에 오류 발생을 저하 시킨다.

pub struct Addr(String);

Uint128 / Uint256 ...

Contract 내에서 amount를 다룰 때 사용되는 데이터 타입이다. Overflow 시 CosmWasm error를 리턴하기 때문에 contract 내에서 exception 처리가 가능하도록 해준다. 값이 큰 amount 특성상 계산 로직을 작성할 때 도움이 된다.

pub struct Uint128(#[schemars(with = "String")] u128);


pub fn checked_add(self, other: Self) -> Result<Self, OverflowError> {
    self.0
        .checked_add(other.0)
        .map(Self)
        .ok_or_else(|| OverflowError::new(OverflowOperation::Add, self, other))
}

Conclusion

CosmWasm contract를 개발하기 위해 기본적으로 알면 좋을 것들에 대해서 정리해보았다. CosmWasm은 내부 vm도 contract 보안에 힘을 써서 security 걱정을 많이 덜어주고, 또한 rust 자체도 스레드 변수 동시성 관리 등 안정적 애플리케이션 개발을 위한 기능을 자체적으로 지원하기 때문에 CosmWasm을 통해 dApp을 개발할 시에는 기존의 solidity 보단 이러한 부분에 대해서는 고민을 덜 할 수 있을 것으로 생각된다. (물론 audit 받으면 좋겠지만) Cosmos 기반 blockchain들은 module이라는 개념을 통해 쉽게 체인 내에서 동작이 필요한 로직 구현이 가능하지만 CosmWasm을 통한 smart contract 개발보다는 어렵고 ERC20, 721 등 토크노믹스 구현을 위해서도 smart contract가 필수적으로 사용되기 때문에 Cosmos와 관련된 비즈니스 개발을 할 때에는 CosmWasm을 통한 개발을 1순위로 하는 것이 좋을 것이라 생각된다.

참고
https://docs.cosmwasm.com/
https://medium.com/cosmwasm/cosmwasm-for-ctos-f1ffa19cccb8
https://medium.com/cosmwasm/cosmwasm-for-ctos-i-the-architecture-59a3e52d9b9c

728x90
반응형