cw-plus
- Production level의 cosmwasm 컨트랙트 분석
- 공식 github 링크
프로젝트 구조
- src
- contract.rs : 컨트랙트의 로직을 포함
- lib.rs : 컨트랙트에 생성된 .rs 파일들 저장
- msg.rs : 컨트랙트가 받아들이는 메세지 정의
- state.rs : 컨트랙트가 저장하고 있는 상태 정의
- error.rs : 기본적으로 다양한 std error들이 지원되며 커스텀 에러까지 생성 가능
- schema : 스마트 컨트랙트와 인터랙트하는 메세지 형태를 보여 줌
- example/schema : schema.rs 에서 schema.json 들을 자동적으로 생성해 줌
컨트랙트 설계시 주의사항
- 컨트랙트로 해선 안되는 것들
- 랜덤 값 생성 : deterministic한 블록체인의 특성상 완전 랜덤한 값은 생성불가. pseudo-random한 값을 생성하는 다양한 기법에 대한 조사 필요
- 일시적인 값 저장 : 서비스에서 일시적으로 사용되는 (아바타의 옷 색깔, 다크/라이트 모드 등) 값들은 스마트 컨트랙트로 처리하는 것은 비효율적임. IPFS 같은 서비스를 써서 off-chain에서 분산화된 형태로 이런 값들을 저장하는 것이 맞음
- 복잡한 연산 : 모든 연산은 블록타임 안에 완료되야 함. 복잡한 연산이 필요할 경우엔 다른 방법을 찾아야함
- 컨트랙트가 지원하는 것
- immutability
- seizure resistance
- censorship resistance
- auditability
CosmWasm Library
- CosmWasm 기반 컨트랙트를 만들기 위해 필요한 다양한 라이브러리들 포함
- 자세한 내용은 코드, API docs 확인
cosmwasm_std::Addr
- Addr은 어드레스의 유효성 검사 등과 같은 유용한 기능을 제공하는 wrapper임
let checked : Addr = deps.api.addr_validate(input)?
- 어드레스는 bech32 형태로 인코딩 되어있음. 하지만 multi-chain 스마트 컨트랙트에서는 UTF-8 인코딩이나 합리적인 길이가 아닌 것은 고려되어선 안됨
- CosmWasm 컨트랙트는 HumanAddr (human-readable), CanonicalAddr (optional)을 사용
cosmwasm_std:Coin
pub struct Coin{
pub denom : String,
pub amount : Uint128,
}
let tip = vec![
coin(123,"ucosm"),
coin(24,"ustake"),
];
let mut response: Response = Default::default();
response.messages = vec![SubMsg::new(BankMsg::Send{
to_address: info.sender.into(),
amount: tip,
})]
cosmwasm_storage::Singleton
- 일반적인 singleton 패턴: 클래스가 최초에 메모리를 static하게 할당하고, 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴
Singleton
은 singleton storage key로 동작하는 TypedStorage와 함께 PrefixedStorag와 효과적으로 동작함
- 주어진 name에서 to_length_prefixed transformation을 collision없이 동작하고 표준 TypedStorage를 key를 요구하지 않으면서 제공함
cw_storage_plus::Item
- state storage에서 단일 리소스를 저장하는 아이템
- 단순한 state나 컨트랙트 셋팅과 같은 singleton이 사용되어지는 곳에 적절함
pub const STATE: Item<State> = Item::new(b"state")
cw_storage_plus::Map
- Map 은 Bucket 보다 robust한 key-value 시스템
- 이더리움과 달리 Map은 iterate하는 것이 가능. 즉, Map안의 아이템들은 list out되거나 process되는 것이 가능
- tuple이 composite key에 사용 가능. IndexedMap은 MultiIndex 기능을 사용하는 데 사용 됨
Contract 분석
Airdrop Contract
state.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub admin: String,
pub denom: String,
}
pub const CONFIG: Item<Config> = Item::new("config");
pub const LATEST_STAGE: Item<u8> = Item::new("latest_stage");
pub const MERKLE_ROOT: Map<&[u8], String> = Map::new("merkle_root");
pub const CLAIM_INDEX: Map<(&[u8], &[u8]), bool> = Map::new("claim_index");
Config
LATEST_STAGE
MERKLE_ROOT
CLAIM_INDEX
msg.rs
pub struct InstantiateMsg {
pub admin: String,
pub denom: String,
}
- contract의 admin과 token의 denom을 생성하는 메시지
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
UpdateConfig {
admin: Option<String>,
},
UpdateMerkleRoot {
stage: u8,
merkle_root: String,
},
RegisterMerkleRoot {
merkle_root: String,
},
Claim {
stage: u8,
amount: Uint128,
proof: Vec<String>,
},
CreateVestingAccount {
recipient: String,
periods: Vec<(i64, String)>,
},
}
- Config의 admin을 업데이트하거나, 각 storage들 업데이트 하는 executeMsg들
- ExecuteMsg가 실행되 그에 따른 Custom Response가 존재함
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
Config {},
MerkleRoot { stage: u8 },
LatestStage {},
IsClaimed { stage: u8, address: String },
}
- 컨트랙트의 storage에 저장된 값들을 알기 위한 메시지