Introduction
- 스마트컨트랙트는 내부 state가 블록체인에 의해 유지되는
singleton object
의 인스턴스라 할 수 있음
- 유저는 JSON 메세지를 전달하여 state 변경을 야기하거나 state를 쿼리할 수 있음
- CosmWasm으로 스마트컨트랙트 개발 시 다음 3가지 인터페이스를 정의해야함
instatiate()
: 컨트랙트 인스턴스화 동안 intial state를 제공하기 위한 constructor
execute()
: 스마트컨트랙트에서 사용자가 원하는 매서드를 실행시키기 위한 호출
query()
: 스마트컨트랙트에서 원하는 데이터를 사용자가 얻기 위한 호출
- 작업 디렉토리에서
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,
{
pub submessages : Vec<SubMsg<T>>,
pub message : Vec<CosmosMsg<T>>,
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),
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,
Error,
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,
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의
execution
과 reply
는 다른 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 {
path: String,
data: Binary,
},
Ibc(IbcQuery),
Wasm(WasmQuery),
}
- 이는 유연하고 cross-language를 위한 인코딩을 요구하지만, bank balance를 찾을 때 마다 이를 필요로하는 것은 꽤나 번거로움
- 이를 돕기 위해
QuerierWrapper
를 주로 사용하며, Querier
를 감싸서, QueryRequest
와 Querier.raw_query
를 여러 신뢰할수 있는 메서드에 노출되는 것을 가능하게 함
Contract State
- 스마트컨트랙트는 byte-based Key-value store인 native
LevelDB
에 영구적인 state를 저장하는 것이 가능함
- 어떠한 데이터든 키를 할당하여 저장하는 것이 가능하고 indexing하여 원할 때 사용하는 것이 가능함
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을 제공함
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
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의
MsgInstantiateContract
는 owner
에 할당됨
#[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
ExecuteMsg
는 MsgExecuteContract
를 통해 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로 다시 생성하고 increment
와 Rest
대신 increment
와 reset
를 JSON을 serializing, deserializing 할 때 사용함
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
Increment {},
Reset { count: i32 },
}
Logic
- Rust의 패턴 매칭을 통해 적절한 로직으로 route
#[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 함수를 실행하는 것을 허용할지를 확인함
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
{
"get_count": {}
}
{
"count": 5
}
Message Definition
- 컨트랙트 데이터 쿼리를 지원하기 위해 query 결과인
CountResponse
(해당 예제에서만)와 QueryMsg
포맷을 둘다 정의해야 함
query()
는 정보를 사용자에게 JSON 형태로 전송하고 응답 결과 형태를 알아야만 하기에 때문에 이 같은 과정을 해야함
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
GetCount {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct CountResponse {
pub count: i32,
}
Logic
query()
로직은 execute()
와 유사하며 query()
는 end-user가 트랜잭션을 생성하지 않는 차이가 있음
env
인자는 정보가 없기 때문에 빼먹었음
#[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,
pub msg: CosmosMsg<T>,
pub gas_limit: Option<u64>,
pub reply_on: ReplyOn,
}
cw20
토큰을 submessage를 통해 인스턴스화 하는 예시
const INSTANTIATE_REPLY_ID = 1u64;
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(),
};
let submessage = SubMsg::reply_on_success(instantiate_message.into(), INSTANTIATE_REPLY_ID);
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,
}
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,
};
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);
let missing = PEOPLE.may_load(&store, "jack")?;
assert_eq!(None, missing);
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());
assert_eq!(old_john, PEOPLE.load(&store, "john")?);
assert_eq!(new_jack, PEOPLE.load(&store, "jack")?);
PEOPLE.remove(&mut store, "john");
let empty = PEOPLE.may_load(&store, "john")?;
assert_eq!(None, empty);
Ok(())
}
- Composite key
- allowance를 account owner와 spender 기반으로 저장할 경우 유용함
const ALLOWANCE: Map<(&str, &str), u64> = Map::new("allow");
fn demo() -> StdResult<()> {
let mut store = MockStorage::new();
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);
let different = ALLOWANCE.may_load(&store, ("owners", "pender")).unwrap();
assert_eq!(None, different);
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,
};
let john = PEOPLE.key("john");
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);
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();
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)?;
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())]
);
let all: StdResult<Vec<_>> = PEOPLE
.range(
&store,
Bound::Exclusive("jim"),
Bound::None,
Order::Ascending,
)
.collect();
assert_eq!(all?, vec![("john".to_vec(), data)]);
ALLOWANCE.save(&mut store, ("owner", "spender"), &1000)?;
ALLOWANCE.save(&mut store, ("owner", "spender2"), &3000)?;
ALLOWANCE.save(&mut store, ("owner2", "spender"), &5000)?;
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)]
);
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
pub struct TokenIndexes<'a> {
pub owner: MultiIndex<'a, Addr, TokenInfo>,
}
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())
}
}
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::new("tokens", indexes)
}
- Usage
- Index keys deserialization
UniqueIndex
와 MultiIndex
에게 primary key 타입은 primary key를 deserialize하기 위해 specified 되어야함
- 이는 backward compatibility를 위한 것
6. Entry Points
- 컨트랙트가 핸들링하는 메세지나 쿼리를 다루는 곳(핸들러)
- 3가지 핸들러
- Initiate message :
InstantiateMsg
구조체가 정의하는 것으로 instantiate
에 처리 됨
- Message :
ExecuteMsg
enum으로 정의 되며 execute
함수에서 처리됨
- Query :
QueryMsg
enum으로 정의되어 query
함수에서 처리됨
execute
와 query
는 모든 enum안의 경우가 패턴 매칭이 되어야함
instantiate
와 execute
는 Result<Response, ContractError>
타입을 가지고, query
는 Cosmos SDK Querier
기반이기에 StdResult<Binary>
를 가짐
7. Query
- 쿼리 가능한 메세지는
msg.rs
나 query.rs
에서 찾는 것이 가능함
- 대부분의 쿼리는 커스텀 쿼리이며, 이는 컨트랙트의 데이터를 read-only 모드로 접근함
- 이 같은 쿼리는 data를 찾거나 추가적인 연산이나 필요한 처리를 할 수 있기 때문에 (
query
함수 내에서), 이런 쿼리들에 대해서는 gas limit이 설정되어야 함
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
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>
를 가져야 함
Ok(Response::default ())
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
RUSTFLAGS='-C link-arg=-s' cargo wasm
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
wasmd tx wasm store <your-contract>.wasm --from <your-key> --chain-id <chain-id> --gas auto
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-base
에 0.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")
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 {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct ContractVersion {
pub contract: String,
pub version: String,
}
Setting up a contract for migration
- 업데이트할 새로운 버전의 컨트랙트를 생성
- 이전에 한 것과 같이 새로운 코드를 업로드하되(store) instantiate 하지 않음
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> {
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)?;
if ver.contract != CONTRACT_NAME {
return Err(StdError::generic_err("Can only upgrade from same type").into());
}
if ver.version >= CONTRACT_VERSION {
return Err(StdError::generic_err("Cannot upgrade from a newer version").into());
}
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
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)?;
}
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
필드 사용
UpdateState
나 UpdateVerifier
같은 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> {
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);
}
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
message PinCodesProposal {
string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
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"
message UnpinCodesProposal {
string title = 1 [ (gogoproto.moretags) = "yaml:\"title\"" ];
string description = 2 [ (gogoproto.moretags) = "yaml:\"description\"" ];
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"));
let res = init(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
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);
}
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)]),
}
}
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),
},
}
}
pub fn mock_info<U: Into<HumanAddr>>(sender: U, sent: &[Coin]) -> MessageInfo {
MessageInfo {
sender: sender.into(),
sent_funds: sent.to_vec(),
}
}
Test 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"));
let res = init(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(0, res.messages.len());
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),
}
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"));
let res = query_config(deps.as_ref()).unwrap();
assert_eq!("someone", res.owner.as_str());
assert_eq!("creator", res.creator.as_str());
}
#[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);
let _ = init(deps.as_mut(), mock_env(), info, msg).unwrap();
let info = mock_info("creator", &[]);
let _ = handle_transfer(deps.as_mut(), mock_env(), info, HumanAddr::from("owner")).unwrap();
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),
}
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),
}
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),
}
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,
})
);
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를 제공 가능
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-test
는 ContractWrapper
를 통해 컨트랙트를 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 ContractData
나 Contract '<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() {
let owner = Addr::unchecked("owner");
let mut router = mock_app();
let counter_contract_code_id = router.store_code(contract_counter());
let init_msg = InstantiateMsg {
count: Uint128::zero()
}
let mocked_contract_addr = router
.instantiate_contract(counter_contract_code_id, owner.clone(), &init_msg, &[], "counter", None)
.unwrap();
let msg = ExecuteMsg::Increment {}
let _ = router.execute_contract(
owner.clone(),
mocked_contract_addr.clone(),
&msg,
&[],
)
.unwrap();
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)
let msg = ExecuteMsg::Reset {}
let _ = router.execute_contract(
owner.clone(),
mocked_contract_addr.clone(),
&msg,
&[],
)
.unwrap();
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
Building the Contract
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를 등록해야함
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);
}
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
}
]
}