Cosmos[4] (Core, SDK, Smart contract)

storemymem·2022년 4월 27일
0

Smart contract

  • Prerequisites

    • Rust (Cargo)
    • Docker
  • Set up rust

rustup default stable

rust target add wasm32-unknown-unknown

cargo install cargo-generate --features vendored-openssl
cargo install cargo-run-script
  • Start with a template
cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name contract-test
cd contract-test
  • Build the contract
# Smart contract build
cargo wasm build

# Optimizin build - 일반 wasm build와 바이트 차이가 많이 난다 1/10 이상?
# Optimize 후 chain에 등록할 것
# 스크립트 실행 시 ./artifacts 내에 wasm 파일 생성
cargo run-script optimize
  • Schema build
cargo schema
  • CosmWasm

    • Cosmos SDK 용 WebAsssembly(Wasm) VM 제공
  • CosmWasm 기반 smart contract 구조

    • src
      • contract.rs : Contract의 execute, query 등 기능 구현
      • error.rs : Error 정의
      • lib.rs : 사용되는 모듈 정의
      • msg.rs : 외부 comm.위한 메시지 형식 정의
      • state.rs : Chain 상 state 형식 정의

Contract example

간단히 최신 블록의 정보를 불러와서 state에 저장하는 것과 contract에 저장된 coin을 user 계좌로 전송하는 smart contract 예제.

Instantiate

Instantiate시에는 블록 정보만 저장, contract 외부 SDK에서 coin을 포함한 트랜잭션 or 포함하지 않는 transaction으로 선택하여 contract로 전송.

// state
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct BlockState {
    pub block_height: String,
    pub block_hash: String,
    pub time_stamp: String,
    pub owner: Addr,
}

pub const BLOCKSTATE: Item<BlockState> = Item::new("block_state");

// msg
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub block_height: String,
    pub block_hash: String,
    pub time_stamp: String,
}
// contract_Instnatiate

// CosmWasm entry point, 실행 시 library가 아닐때
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut, //Mutable dependencies (타 contract API, querier, etc.)
    _env: Env, // Environment (Block time, number, contract/tx info, etc.)
    info: MessageInfo, // Message info (sender 정보, etc.)
    msg: InstantiateMsg, // msg.rs 내 정의
) -> Result<Response, ContractError> {
    let block_state = BlockState {
        block_height: msg.block_height,
        block_hash: msg.block_hash,
        time_stamp: msg.time_stamp,
        owner: info.sender.clone(),
    };
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    BLOCKSTATE.save(deps.storage, &block_state)?;

    Ok(Response::new())
}

Execute

Execute는 3가지로 분류.
외부 SDK에서 전달해주는 최신 블록의 정보를 저장하는 기능, instantiate 시 생성되어 contract가 보유하고 있는 token/coin을 사용자의 계좌로 전송하는 기능(Airdrop 또는 토큰 분배를 위한), 외부 contract가 보유하고 있는 token/coin을 사용자의 계좌로 전송하는 기능.

// msg
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    SaveLatestBlockInfo {
        block_height: String,
        block_hash: String,
        time_stamp: String,
    },
    SendCoinFromTo {
        receiver: String,
        amount: String,
    },
    SendFromOthContract {
        contract_addr: String,
        receiver: String,
        amount: String,
    },
}
// contract_execute
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::SaveLatestBlockInfo {
            block_height,
            block_hash,
            time_stamp
        } => save_latest_block_info(deps, block_height, block_hash, time_stamp),

        ExecuteMsg::SendCoinFromTo {
            receiver,
            amount
        } => send_coin_from_to(deps, receiver, amount, info, _env),

        ExecuteMsg::SendFromOthContract{
            contract_addr,
            receiver,
            amount
        } => send_from_oth_contract(deps, contract_addr, receiver, amount),
    }
}

Rust의 match를 통해 ExecuteMsg가 기능을 사용할지 분류 후 실제 동작을 위한 func을 실행.

pub fn save_latest_block_info( 
    deps: DepsMut,
    block_height: String,
    block_hash: String,
    time_stamp: String
) -> Result<Response, ContractError> {
    let mut block_state = BLOCKSTATE.load(deps.storage)?;

    block_state.block_height = block_height;
    block_state.block_hash = block_hash;
    block_state.time_stamp = time_stamp;

    BLOCKSTATE.save(deps.storage, &block_state)?;

    Ok(Response::new().add_attribute("method", "save_latest_block_info"))
}

최신 블록 저장 시는 instantiate와 동일하게 msg로 수신한 블록 정보를 받아서 state에 update.
기존에 저장되어 있는 state를 load하여 parameter에 맞게 변환 후 다시 save함.
Ok시 CosmWasm Reponse struct로 반환.

pub fn send_coin_from_to(
    _deps: DepsMut,
    receiver: String,
    amount: String,
    _info: MessageInfo,
    _env: Env,
) -> Result<Response, ContractError> {

    let mut messages: Vec<CosmosMsg> = vec![];

    let amount_int: u128 = amount.parse().unwrap();
    let _amount_uint128 = Uint128::from(amount_int);
    
    let denom_def = String::from("uluna");

    messages.push(CosmosMsg::Bank(BankMsg::Send{
        to_address: receiver,
        amount: coins(
            amount_int, 
            denom_def
        ),
    }));

    Ok(
        Response::new()
        .add_attribute("method", "send_coin_from_to")
        .add_messages(messages)
    )
}

Contract가 보유한 coin을 SDK에서 전달해준 사용자 계좌로 전송하는 예제.
Response 내에 .add_messages를 사용하기 위해 변수 messages를 Vec<>타입의 CosmosMsg로 선언하고 messages.push를 통해 CosmosMsg내의 기능을 사용.
CosmosMsg내 BankMsg는 native token을 contract에서 다른 주소로 옮기는 역할을 수행하며 Send와 Burn을 사용가능.

pub enum BankMsg {
  Send {
    to_address: String,
    amount: Vec<Coin>,
  },
  ...
}

위 코드블럭처럼 Send는 인자로 보낼 주소와 보낼 양을 받기 때문에 contract에 두 인자를 SDK로 보내줘야 하며, 보낼 양을 선언하는 amount의 경우 Coin struct를 사용하기 때문에 절대적인 수량 뿐만아니라 denom으로 단위 또한 보내줘야 함.

pub fn send_from_oth_contract(
    _deps: DepsMut,
    contract_addr: String,
    receiver: String,
    amount: String,
) -> Result<Response, ContractError> {

    let mut messages: Vec<CosmosMsg> = vec![];
    let amount_int: u128 = amount.parse().unwrap();
    let amount_uint128 = Uint128::from(amount_int);

    messages.push(CosmosMsg::Wasm(WasmMsg::Execute{
        contract_addr: contract_addr,
        msg: to_binary(&Cw20ExecuteMsg::Transfer{
            recipient: receiver,
            amount: amount_uint128
        })?,
        funds: vec![]
    }));

    Ok(
        Response::new()
        .add_attribute("method", "send_from_oth_contract")
        .add_messages(messages)
    )
}

외부 Contract가 보유한 token을 다른 주소로 보내기 위한 예제.
위의 기능과 같지만 BankMsg가 아닌 WasmMsg로 execute를 사용한 차이.
SDK에서 외부 contract가 보유한 token을 본 contract를 실행함으로써 다른 주소로 보낼 수 있다.
Cw20ExecuteMsg의 Transfer를 사용, 외부 contract의 보유 token을 전송하는 것이니까 TransferFrom같은 권한이 필요한 어떤 조건이 있어야 맞지 않나 싶은데 그냥 Transfer로 해도 외부 contract 보유 token이 전송 되더라... 따로 approval을 받거나 하진 않았는데 contract 등록(sender)가 같은 사람이라 가능한것인가? 이 부분은 조금더 조사해보며 추가적으로 확인해야겠다.

Query

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

Query는 state 내부에 저장한 최신 블록 정보와 contract가 보유하고 있는 token/coin 양을 확인하는 용도.
State query는 CosmWasm 예제 contract와 차이 없이 deps.storage를 load 후 StdResult Binary로 response.

fn query_balance_of_token(
    address: String, 
    denom: String,
    _env: Env,
    deps:Deps,
) -> StdResult<Coin> {

    let querier = deps.querier.query_balance(address, denom)?;

    Ok(querier)  
}

Contract가 보유하고 있는 token/coin을 확인하려면 querier를 사용. SDK에서 확인이 필요한 contract addr과 denom을 선언해주어 contract로 인자 전달.
QuerierWrapper중 query_balance를 통해 contract 내의 보유 재화를 확인하고 마찬가지로 binary로 싸서 response.


Cosmos contract는 ETH나 Fabric chaincode에 비해 ref가 너무 없는것이 불편쓰. Query할 때도 QuerierWrapper 쓰기위해 Trait Querier 코드 다 분석하고 QuerierResult로 반환해보기도 하고 삽질했는데 Deps 내에 querier가 있는걸 한참뒤에 발견... 각 인자들을 제대로 안 훑어본 내 잘못이지만 contract를 작성하기 위한 마땅한 가이드가 없는 것 같...

참고
http://wiki.hash.kr/index.php/%EB%8C%80%EB%AC%B8

profile
ConsensusMyMemoies

0개의 댓글