This commit is contained in:
Karl Kempe 2022-04-27 19:54:12 +00:00
parent 67b134b933
commit db2870a0d3
18 changed files with 751 additions and 126 deletions

23
terra/Cargo.lock generated
View File

@ -708,6 +708,28 @@ dependencies = [
"digest",
]
[[package]]
name = "icco"
version = "0.1.0"
dependencies = [
"bigint",
"cosmwasm-std",
"cosmwasm-vm",
"cw-storage-plus",
"cw20",
"cw20-base",
"generic-array",
"hex",
"k256",
"lazy_static",
"schemars",
"serde",
"serde_json",
"sha3",
"terraswap",
"thiserror",
]
[[package]]
name = "icco-contributor"
version = "0.1.0"
@ -720,6 +742,7 @@ dependencies = [
"cw20-base",
"generic-array",
"hex",
"icco",
"k256",
"lazy_static",
"schemars",

View File

@ -1,5 +1,8 @@
[workspace]
members = ["contracts/icco-contributor"]
members = [
"packages/*",
"contracts/icco-contributor"
]
[profile.release]
opt-level = 3

View File

@ -47,7 +47,7 @@ tools/node_modules: tools/package-lock.json
## Run linter and unit tests
test:
# cargo clippy
cargo test
cargo test -- --nocapture
.PHONY: clean
clean:

View File

@ -30,6 +30,7 @@ bigint = "4"
wormhole-bridge-terra = { git = "https://github.com/certusone/wormhole", branch = "feat/token-bridge-proxy", features = ["library"]}
token-bridge-terra = { git = "https://github.com/certusone/wormhole", branch="feat/token-bridge-proxy", features = ["library"] }
icco = { path = "../../packages/icco", features = ["library"] }
[dev-dependencies]
cosmwasm-vm = { version = "0.16.0", default-features = false }

View File

@ -15,9 +15,6 @@ use crate::{
state::{Config, CONFIG},
};
// Chain ID of Terra
pub const CHAIN_ID: u16 = 3;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::new())
@ -27,14 +24,15 @@ pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Respons
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let cfg = Config {
wormhole_contract: msg.wormhole_contract,
token_bridge_contract: msg.token_bridge_contract,
conductor_chain: msg.conductor_chain,
conductor_address: msg.conductor_address.as_slice().to_vec(),
conductor_address: msg.conductor_address.into(),
owner: info.sender.to_string(),
};
CONFIG.save(deps.storage, &cfg)?;

View File

@ -47,6 +47,12 @@ pub enum ContributorError {
#[error("WrongChain")]
WrongChain,
#[error("NonexistentToken")]
NonexistentToken,
#[error("NonexistentDenom")]
NonexistentDenom,
}
impl ContributorError {

View File

@ -1,24 +1,44 @@
use cosmwasm_std::{
to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, QueryRequest, Response,
StdError, StdResult, Storage, Uint256, WasmMsg, WasmQuery,
StdError, StdResult, Storage, Uint128, WasmMsg, WasmQuery,
};
use terraswap::{
asset::AssetInfo,
querier::{query_balance, query_token_balance},
};
use token_bridge_terra::contract::coins_after_tax;
use wormhole::{
msg::{ExecuteMsg as WormholeExecuteMsg, QueryMsg as WormholeQueryMsg},
state::ParsedVAA,
};
use icco::common::{
AcceptedToken, ContributionsSealed, SaleAborted, SaleInit, SaleSealed, SaleStatus, CHAIN_ID,
};
use crate::{
error::ContributorError,
shared::{AcceptedToken, ContributionsSealed, SaleAborted, SaleInit, SaleSealed, SaleStatus},
state::{
SaleMessage, TokenIndexKey, ACCEPTED_TOKENS, CHAIN_ID, CONFIG, SALES, SALE_STATUSES,
SALE_TIMES, TOTAL_ALLOCATIONS, TOTAL_CONTRIBUTIONS, ZERO_AMOUNT,
load_accepted_token,
SaleMessage,
TokenIndexKey,
UserAction,
ACCEPTED_ASSETS,
ACCEPTED_TOKENS,
CONFIG,
SALES,
SALE_STATUSES,
SALE_TIMES,
TOTAL_ALLOCATIONS,
TOTAL_CONTRIBUTIONS, //USER_ACTIONS,
ZERO_AMOUNT,
},
};
// nonce means nothing?
const WORMHOLE_NONCE: u32 = 0;
const NATIVE_DENOM_START_INDEX: usize = 28;
pub fn init_sale(deps: DepsMut, env: Env, _info: MessageInfo, vaa: &Binary) -> StdResult<Response> {
let parsed = parse_vaa(deps.as_ref(), env.block.time.seconds(), vaa)?;
@ -62,8 +82,28 @@ pub fn init_sale(deps: DepsMut, env: Env, _info: MessageInfo, vaa: &Binary) -> S
let key: TokenIndexKey = (sale_id, token_index.into());
if token.chain == CHAIN_ID {
// TODO: check if token is actual CW20
let querier = &deps.querier;
let this = env.contract.address.clone();
// attempt to check balances to verify existence
let asset_info = token.make_asset_info(deps.api)?;
match asset_info.clone() {
AssetInfo::NativeToken { denom } => {
if query_balance(querier, this, denom).is_err() {
return ContributorError::NonexistentDenom.std_err();
}
}
AssetInfo::Token { contract_addr } => {
let validated = deps.api.addr_validate(contract_addr.as_str())?;
if query_token_balance(querier, validated, this).is_err() {
return ContributorError::NonexistentToken.std_err();
}
}
}
ACCEPTED_ASSETS.save(storage, key.clone(), &asset_info)?;
}
ACCEPTED_TOKENS.save(storage, key.clone(), &token)?;
TOTAL_CONTRIBUTIONS.save(storage, key.clone(), &ZERO_AMOUNT)?;
TOTAL_ALLOCATIONS.save(storage, key.clone(), &ZERO_AMOUNT)?;
@ -77,22 +117,27 @@ pub fn init_sale(deps: DepsMut, env: Env, _info: MessageInfo, vaa: &Binary) -> S
.add_attribute("token_address", hex::encode(&sale.token_address)))
}
// TODO : add signature argument
pub fn contribute(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
sale_id: &[u8],
token_index: u8,
amount: Uint256,
amount: Uint128,
) -> StdResult<Response> {
// TODO : add signature argument
let storage = deps.storage;
let status = SALE_STATUSES.load(storage, sale_id)?;
//if USER_ACTIONS.load(deps.storage, sale_id).is_ok() {
// return Err(StdError::generic_err("user action exists"));
//}
//USER_ACTIONS.save(deps.storage, sale_id, &UserAction::CONTRIBUTE);
let status = SALE_STATUSES.load(deps.storage, sale_id)?;
if status.is_aborted {
return ContributorError::SaleAborted.std_err();
}
let times = SALE_TIMES.load(storage, sale_id)?;
let times = SALE_TIMES.load(deps.storage, sale_id)?;
let now = env.block.time.seconds();
if now < times.start {
return ContributorError::SaleNotStarted.std_err();
@ -100,17 +145,87 @@ pub fn contribute(
return ContributorError::SaleEnded.std_err();
}
let token = ACCEPTED_TOKENS.load(storage, (sale_id, token_index.into()))?;
if token.chain != CHAIN_ID {
return ContributorError::WrongChain.std_err();
let asset_info = ACCEPTED_ASSETS.load(deps.storage, (sale_id, token_index.into()))?;
//let token = ACCEPTED_TOKENS.load(deps.storage, (sale_id, token_index.into()))?;
//if token.chain != CHAIN_ID {
// return ContributorError::WrongChain.std_err();
//}
match asset_info {
AssetInfo::NativeToken { denom } => {
contribute_native(deps, env, info, sale_id, token_index, &denom, amount)
}
AssetInfo::Token { contract_addr } => contribute_token(
deps,
env,
info,
sale_id,
token_index,
&contract_addr,
amount,
),
}
/*
let new_balance: BalanceResponse =
deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: state.token_address.clone(),
msg: to_binary(&TokenQuery::Balance {
address: env.contract.address.to_string(),
})?,
}))?;
*/
//assert!(USER_ACTION.load(deps.storage).is_err());
}
pub fn contribute_native(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
sale_id: &[u8],
token_index: u8,
denom: &String,
amount: Uint128,
) -> StdResult<Response> {
// TODO: do I handle reply after this?
// USER_ACTIONS.remove(deps.storage, sale_id)?
Ok(Response::new()
.add_attribute("action", "contribute_native")
.add_attribute("sale_id", Binary::from(sale_id).to_base64())
.add_attribute("denom", denom)
.add_attribute("amount", amount.to_string()))
}
pub fn contribute_token(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
sale_id: &[u8],
token_index: u8,
contract_addr: &String,
amount: Uint128,
) -> StdResult<Response> {
// check balance
/*
let balance: BalanceResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: contract_addr.clone(),
msg: to_binary(&TokenQuery::Balance {
address: env.contract.address.to_string(),
})?,
}))?;
*/
let querier = &deps.querier;
let validated = deps.api.addr_validate(contract_addr.as_str())?;
let balance = query_token_balance(querier, validated, env.contract.address.clone())?;
Ok(Response::new()
.add_attribute("action", "contribute")
.add_attribute("action", "contribute_token")
.add_attribute("sale_id", Binary::from(sale_id).to_base64())
.add_attribute("token_address", Binary::from(token.address).to_base64()))
.add_attribute("contract_addr", contract_addr.clone())
.add_attribute("amount", amount.to_string()))
}
pub fn attest_contributions(
@ -354,3 +469,19 @@ fn parse_vaa(deps: Deps, block_time: u64, data: &Binary) -> StdResult<ParsedVAA>
}))?;
Ok(vaa)
}
/*
fn query_cw20_balance(
deps: Deps,
contract_addr: String,
wallet_addr: String,
) -> StdResult<BalanceResponse> {
let balance: BalanceResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: contract_addr.clone(),
msg: to_binary(&TokenQuery::Balance {
address: wallet_addr.clone(),
})?,
}))?;
Ok(balance)
}
*/

View File

@ -6,7 +6,6 @@ mod error;
mod execute;
mod msg;
mod query;
mod shared;
mod state;
#[cfg(test)]

View File

@ -6,15 +6,13 @@ use std::string::String;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
// governance contract details
pub gov_chain: u16,
pub gov_address: Binary,
pub wormhole_contract: String,
pub token_bridge_contract: String,
pub conductor_chain: u16,
pub conductor_address: Binary,
pub owner: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@ -26,7 +24,7 @@ pub enum ExecuteMsg {
Contribute {
sale_id: Binary,
token_index: u8,
amount: Uint256,
amount: Uint128,
},
AttestContributions {
sale_id: Binary,
@ -78,6 +76,7 @@ pub struct SaleRegistryResponse {
pub token_chain: u16,
pub token_amount: Uint256,
pub min_raise: Uint256,
pub max_raise: Uint256,
pub sale_start: u64,
pub sale_end: u64,
pub recipient: Vec<u8>,
@ -115,7 +114,7 @@ pub struct AcceptedTokenResponse {
pub struct TotalContributionResponse {
pub id: Vec<u8>,
pub token_index: u8,
pub amount: Uint256,
pub amount: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@ -123,5 +122,5 @@ pub struct TotalContributionResponse {
pub struct TotalAllocationResponse {
pub id: Vec<u8>,
pub token_index: u8,
pub amount: Uint256,
pub amount: Uint128,
}

View File

@ -30,6 +30,7 @@ pub fn query_sale_registry(deps: Deps, sale_id: &Binary) -> StdResult<SaleRegist
token_chain: sale.token_chain,
token_amount: sale.token_amount,
min_raise: sale.min_raise,
max_raise: sale.max_raise,
sale_start: sale.times.start,
sale_end: sale.times.end,
recipient: sale.recipient,

View File

@ -1,10 +1,14 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::{StdError, StdResult, Uint128, Uint256};
use cosmwasm_std::{Api, CanonicalAddr, DepsMut, StdError, StdResult, Uint128, Uint256};
use terraswap::asset::{Asset, AssetInfo};
use wormhole::byte_utils::ByteUtils;
// Chain ID of Terra
pub const CHAIN_ID: u16 = 3;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SaleTimes {
pub start: u64,
@ -18,28 +22,12 @@ pub struct SaleCore {
pub token_chain: u16,
pub token_amount: Uint256,
pub min_raise: Uint256,
pub max_raise: Uint256,
pub times: SaleTimes,
pub recipient: Vec<u8>,
pub refund_recipient: Vec<u8>,
}
/*
impl Clone for SaleCore {
fn clone(&self) -> SaleCore {
SaleCore {
id: self.id.clone(),
token_address: self.token_address.clone(),
token_chain: self.token_chain,
token_amount: self.token_amount,
min_raise: self.min_raise,
times: self.times.clone(),
recipient: self.recipient.clone(),
refund_recipient: self.refund_recipient.clone(),
}
}
}
*/
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct AcceptedToken {
pub chain: u16,
@ -56,7 +44,7 @@ pub struct SaleStatus {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Contribution {
pub token_index: u8,
pub contributed: Uint256,
pub contributed: Uint256, // actually Uint128, but will be serialized as Uint256
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@ -94,6 +82,10 @@ impl AcceptedToken {
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
let address = data.get_bytes32(0).to_vec();
if address.len() != 32 {
return Err(StdError::generic_err("address.len() != 32"));
}
let chain = data.get_u16(32);
let conversion_rate = data.get_u128_be(34);
Ok(AcceptedToken {
@ -111,6 +103,43 @@ impl AcceptedToken {
]
.concat()
}
pub fn make_asset_info(&self, api: &dyn Api) -> StdResult<AssetInfo> {
if self.chain != CHAIN_ID {
return Err(StdError::generic_err("chain != terra"));
}
let addr = self.address.as_slice();
match addr[0] {
1u8 => {
// match first "u" (e.g. uusd)
match addr.iter().position(|&x| x == 117u8) {
Some(idx) => {
let denom = &addr[idx..32];
match String::from_utf8(denom.into()) {
Ok(denom) => Ok(AssetInfo::NativeToken { denom }),
_ => Err(StdError::generic_err("not valid denom")),
}
}
None => Err(StdError::generic_err("not valid denom")),
}
}
_ => {
let token_address = CanonicalAddr::from(&addr[12..32]);
let humanized = api.addr_humanize(&token_address)?;
Ok(AssetInfo::Token {
contract_addr: humanized.to_string(),
})
}
}
}
pub fn make_asset(&self, api: &dyn Api, amount: Uint128) -> StdResult<Asset> {
Ok(Asset {
info: self.make_asset_info(api)?,
amount: amount,
})
}
}
pub fn to_const_bytes32(data: &[u8], index: usize) -> [u8; 32] {
@ -123,7 +152,7 @@ pub fn to_u256(data: &[u8], index: usize) -> Uint256 {
impl SaleInit {
pub const PAYLOAD_ID: u8 = 1;
const INDEX_ACCEPTED_TOKENS_START: usize = 194;
const INDEX_ACCEPTED_TOKENS_START: usize = 226;
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
let sale_id = data.get_bytes32(0).to_vec();
@ -131,8 +160,9 @@ impl SaleInit {
let token_chain = data.get_u16(64);
let token_amount = to_u256(data, 66);
let min_raise = to_u256(data, 98);
let start = data.get_u64(130 + 24); // encoded as u256, but we only care about u64 time
let end = data.get_u64(162 + 24); // encoded as u256, but we only care about u64 for time
let max_raise = to_u256(data, 130);
let start = data.get_u64(162 + 24); // encoded as u256, but we only care about u64 time
let end = data.get_u64(194 + 24); // encoded as u256, but we only care about u64 for time
let accepted_tokens =
SaleInit::deserialize_tokens(&data[SaleInit::INDEX_ACCEPTED_TOKENS_START..])?;
@ -151,6 +181,7 @@ impl SaleInit {
token_chain,
token_amount,
min_raise,
max_raise,
times: SaleTimes { start, end },
recipient,
refund_recipient,
@ -187,12 +218,13 @@ impl ContributionsSealed {
}
}
pub fn add_contribution(&mut self, token_index: u8, contributed: Uint256) -> StdResult<usize> {
pub fn add_contribution(&mut self, token_index: u8, contributed: Uint128) -> StdResult<usize> {
// limit to len 255
if self.contributions.len() >= 256 {
return Err(StdError::generic_err("cannot exceed length 256"));
}
let contributed = Uint256::from(contributed.u128());
self.contributions.push(Contribution {
token_index,
contributed,

View File

@ -1,16 +1,17 @@
use cosmwasm_std::{StdResult, Storage, Uint256};
use cosmwasm_std::{StdResult, Storage, Uint128};
use cw_storage_plus::{Item, Map, U8Key};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use terraswap::asset::AssetInfo;
use wormhole::byte_utils::ByteUtils;
use crate::shared::{AcceptedToken, SaleCore, SaleStatus, SaleTimes};
use icco::common::{AcceptedToken, SaleCore, SaleStatus, SaleTimes};
// per sale_id and token_index, we need to track a buyer's contribution, as well as whether
// he has been refunded or his allocations have been claimed
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct BuyerStatus {
pub contribution: Uint256,
pub contribution: Uint128,
pub allocation_is_claimed: bool,
pub refund_is_claimed: bool,
}
@ -21,6 +22,7 @@ pub struct Config {
pub token_bridge_contract: HumanAddr,
pub conductor_chain: u16,
pub conductor_address: Vec<u8>,
pub owner: HumanAddr,
}
pub struct SaleMessage<'a> {
@ -33,24 +35,30 @@ pub struct UpgradeContract {
pub new_contract: u64,
}
*/
pub struct UserAction;
impl UserAction {
pub const CONTRIBUTE: u8 = 1;
}
pub type HumanAddr = String;
pub type SaleId<'a> = &'a [u8];
pub type TokenIndexKey<'a> = (SaleId<'a>, U8Key);
pub type BuyerTokenIndexKey<'a> = (SaleId<'a>, U8Key, &'a HumanAddr);
pub const CHAIN_ID: u16 = 3;
pub const CONFIG: Item<Config> = Item::new("config");
pub const SALES: Map<SaleId, SaleCore> = Map::new("sales");
pub const SALE_STATUSES: Map<SaleId, SaleStatus> = Map::new("sale_statuses");
pub const SALE_TIMES: Map<SaleId, SaleTimes> = Map::new("sale_times");
pub const ACCEPTED_TOKENS: Map<(SaleId, U8Key), AcceptedToken> = Map::new("accepted_tokens");
pub const TOTAL_CONTRIBUTIONS: Map<TokenIndexKey, Uint256> = Map::new("total_contributions");
pub const TOTAL_ALLOCATIONS: Map<TokenIndexKey, Uint256> = Map::new("total_allocations");
//const BUYER_STATUS: Map<BuyerTokenIndexKey, BuyerStatus> = Map::new("buyer_statuses");
pub const ACCEPTED_ASSETS: Map<TokenIndexKey, AssetInfo> = Map::new("accepted_tokens");
pub const ACCEPTED_TOKENS: Map<TokenIndexKey, AcceptedToken> = Map::new("accepted_tokens");
pub const TOTAL_CONTRIBUTIONS: Map<TokenIndexKey, Uint128> = Map::new("total_contributions");
pub const TOTAL_ALLOCATIONS: Map<TokenIndexKey, Uint128> = Map::new("total_allocations");
pub const BUYER_STATUSES: Map<BuyerTokenIndexKey, BuyerStatus> = Map::new("buyer_statuses");
//pub const USER_ACTIONS: Map<SaleId, u8> = Map::new("user_actions");
//pub const TOKEN_CONTRIBUTE_TMP = Item<> = Item::new("token_contribute_tmp");
pub const ZERO_AMOUNT: Uint256 = Uint256::zero();
pub const ZERO_AMOUNT: Uint128 = Uint128::zero();
pub fn load_accepted_token(
storage: &dyn Storage,
@ -64,7 +72,7 @@ pub fn load_total_contribution(
storage: &dyn Storage,
sale_id: &[u8],
token_index: u8,
) -> StdResult<Uint256> {
) -> StdResult<Uint128> {
TOTAL_CONTRIBUTIONS.load(storage, (sale_id, token_index.into()))
}
@ -72,7 +80,7 @@ pub fn load_total_allocation(
storage: &dyn Storage,
sale_id: &[u8],
token_index: u8,
) -> StdResult<Uint256> {
) -> StdResult<Uint128> {
TOTAL_ALLOCATIONS.load(storage, (sale_id, token_index.into()))
}

View File

@ -26,20 +26,6 @@ pub fn mock_dependencies(
pub struct WasmMockQuerier {
base: MockQuerier<Empty>,
minter_querier: MinterQuerier,
}
#[derive(Clone, Default)]
pub struct MinterQuerier {
minter_addr: String,
}
impl MinterQuerier {
pub fn new(minter: String) -> Self {
MinterQuerier {
minter_addr: minter,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@ -85,13 +71,6 @@ impl WasmMockQuerier {
impl WasmMockQuerier {
pub fn new(base: MockQuerier<Empty>) -> Self {
WasmMockQuerier {
base,
minter_querier: MinterQuerier::default(),
}
}
pub fn with_anc_minter(&mut self, minter: String) {
self.minter_querier = MinterQuerier::new(minter);
WasmMockQuerier { base }
}
}

View File

@ -1,3 +1,9 @@
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::{from_binary, Binary, StdResult, Uint128, Uint256};
use std::string::String;
use icco::common::{SaleAborted, SaleSealed};
use crate::{
contract::{execute, instantiate, query},
msg::{
@ -5,16 +11,9 @@ use crate::{
SaleRegistryResponse, SaleStatusResponse, SaleTimesResponse, TotalAllocationResponse,
TotalContributionResponse,
},
shared::{SaleAborted, SaleSealed},
state::SaleMessage,
testing::mock_querier::mock_dependencies,
};
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::{from_binary, to_binary, Binary, StdResult, Uint256};
use std::string::String;
use wormhole::byte_utils::ByteUtils;
/*
@ -142,11 +141,10 @@ use wormhole::byte_utils::ByteUtils;
saleAborted vaa 0x010000000001003f2c65c95e0309fec40c863e6f9d318141c7febc648f87f9f65ba43e64c33840574e461593dffc11a8d577bd0c8e9fe9368b5789fd2cc30e6fe0faf1699af53400000004900000000000020000000000000000000000005f8e26facc23fa4cbd87b8d9dbbd33d5047abde100000000000000030f040000000000000000000000000000000000000000000000000000000000000001
*/
const ADDRESS_ZERO: [u8; 32] = [0u8; 32];
#[test]
fn proper_initialization() -> StdResult<()> {
let mut deps = mock_dependencies(&[]);
let info = mock_info("creator", &[]);
let conductor_chain = 2u16;
let conductor_address = "0000000000000000000000005f8e26facc23fa4cbd87b8d9dbbd33d5047abde1";
@ -154,14 +152,12 @@ fn proper_initialization() -> StdResult<()> {
let conductor_address = conductor_address.as_slice();
let msg = InstantiateMsg {
gov_chain: 1,
gov_address: Binary::from(Vec::from(ADDRESS_ZERO)),
wormhole_contract: String::new(),
token_bridge_contract: String::new(),
conductor_chain: conductor_chain,
conductor_address: Binary::from(conductor_address),
owner: info.sender.to_string(),
};
let info = mock_info("creator", &[]);
let response = instantiate(deps.as_mut(), mock_env(), info, msg.clone())?;
assert_eq!(
@ -189,25 +185,22 @@ fn proper_initialization() -> StdResult<()> {
#[test]
fn init_sale() -> StdResult<()> {
let mut deps = mock_dependencies(&[]);
let env = mock_env();
let info = mock_info("creator", &[]);
let conductor_chain = 2u16;
let conductor_address = "0000000000000000000000005f8e26facc23fa4cbd87b8d9dbbd33d5047abde1";
let conductor_address = "000000000000000000000000f19a2a01b70519f67adb309a994ec8c69a967e8b";
let conductor_address = hex::decode(conductor_address).unwrap();
let conductor_address = conductor_address.as_slice();
let msg = InstantiateMsg {
gov_chain: 1,
gov_address: Binary::from(Vec::from(ADDRESS_ZERO)),
wormhole_contract: String::new(),
token_bridge_contract: String::new(),
conductor_chain: conductor_chain,
conductor_address: Binary::from(conductor_address),
owner: info.sender.to_string(),
};
let response = instantiate(deps.as_mut(), env.clone(), info, msg)?;
let response = instantiate(deps.as_mut(), mock_env(), info, msg)?;
assert_eq!(
response.messages.len(),
0,
@ -215,35 +208,33 @@ fn init_sale() -> StdResult<()> {
);
let signed_vaa = "\
01000000000100fd75a4cd9fe22519fbe7856267f59d0e0bf9931bf8a7733de4\
e7b576451ae1b3309c9adee5c96e82e9b06b03fa16e189e3fe9eff132c1f9ce9\
22710524a6476300000003880000000000020000000000000000000000005f8e\
26facc23fa4cbd87b8d9dbbd33d5047abde100000000000000000f0100000000\
01000000000100ae0eda623b8aae9bde03c68922ac218bb0c3aa9c5ea0a70a7a\
caea0a46d0915a7b3a06758250e44e6c543a37ea1097b85be6f75bd07127b802\
38ed6e6b73cbc00000000563000000000002000000000000000000000000f19a\
2a01b70519f67adb309a994ec8c69a967e8b00000000000000000f0100000000\
0000000000000000000000000000000000000000000000000000000000000000\
000000000000000083752ecafebf4707258dedffbd9c7443148169db00020000\
000000000000000000000000000000000000000000000de0b6b3a76400000000\
000000000000000000000000000000000000000000008ac7230489e800000000\
00000000000000000000000000000000000000000000000000000000038c0000\
0000000000000000000000000000000000000000000000000000000003c80600\
00000000000000000000000000000000000000000000c249fdd3277800000000\
0000000000000000000000000000000000000000000000000000000005670000\
0000000000000000000000000000000000000000000000000000000005a30400\
0000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e00\
0200000000000000000de0b6b3a7640000000000000000000000000000ddb64f\
e46a91d46ee29420539fc25fd07c5fea3e0004000000000000000002c68af0bb\
1400000000000000000000000000008a5bbc20ad253e296f61601e868a3206b2\
d4774c0002000000000000000002c68af0bb1400000000000000000000000000\
003d9e7a12daa29a8b2b1bfaa9dc97ce018853ab31000400000000000000000d\
e0b6b3a7640000000000000000000000000000ddb64fe46a91d46ee29420539f\
c25fd07c5fea3e0004000000000000000002c68af0bb14000000000000000000\
0000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e00040000000000\
00000002c68af0bb14000000000000000000000000000022d491bde2303f2f43\
325b2108d26f1eaba1e32b00000000000000000000000022d491bde2303f2f43\
325b2108d26f1eaba1e32b";
e0b6b3a764000000000000000000000000000022d491bde2303f2f43325b2108\
d26f1eaba1e32b00000000000000000000000022d491bde2303f2f43325b2108\
d26f1eaba1e32b";
let signed_vaa = hex::decode(signed_vaa).unwrap();
let info = mock_info("addr0001", &[]);
let msg = ExecuteMsg::InitSale {
data: Binary::from(signed_vaa.as_slice()),
};
let _response = execute(deps.as_mut(), env.clone(), info, msg)?;
let _response = execute(deps.as_mut(), mock_env(), info, msg)?;
let sale_id = "0000000000000000000000000000000000000000000000000000000000000000";
let sale_id = hex::decode(sale_id).unwrap();
@ -344,10 +335,13 @@ fn init_sale() -> StdResult<()> {
let min_raise = Uint256::from(10_000_000_000_000_000_000u128);
assert_eq!(sale.min_raise, min_raise, "sale.min_raise != expected");
let sale_start = 908u64;
let max_raise = Uint256::from(10_000_000_000_000_000_000u128);
assert_eq!(sale.min_raise, max_raise, "sale.max_raise != expected");
let sale_start = 1383u64;
assert_eq!(sale.sale_start, sale_start, "sale.sale_start != expected");
let sale_end = 968u64;
let sale_end = 1443u64;
assert_eq!(sale.sale_end, sale_end, "sale.sale_end != expected");
let recipient = "00000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b";
@ -380,8 +374,6 @@ fn init_sale() -> StdResult<()> {
"000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e",
"0000000000000000000000008a5bbc20ad253e296f61601e868a3206b2d4774c",
"0000000000000000000000003d9e7a12daa29a8b2b1bfaa9dc97ce018853ab31",
"000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e",
"000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e",
]);
let accepted_token_chains = Vec::from([2u16, 4u16, 2u16, 4u16, 4u16, 4u16]);
let accepted_token_conversion_rates = Vec::from([
@ -389,8 +381,6 @@ fn init_sale() -> StdResult<()> {
200_000_000_000_000_000u128,
200_000_000_000_000_000u128,
1_000_000_000_000_000_000u128,
200_000_000_000_000_000u128,
200_000_000_000_000_000u128,
]);
let n_accepted_tokens = accepted_token_addresses.len();
@ -432,7 +422,7 @@ fn init_sale() -> StdResult<()> {
let total_contribution: TotalContributionResponse = from_binary(&response)?;
assert_eq!(
total_contribution.amount,
Uint256::zero(),
Uint128::zero(),
"total_contribution.amount != 0"
);
@ -447,7 +437,7 @@ fn init_sale() -> StdResult<()> {
let total_allocation: TotalAllocationResponse = from_binary(&response)?;
assert_eq!(
total_allocation.amount,
Uint256::zero(),
Uint128::zero(),
"total_allocation.amount != 0"
);
}

View File

@ -0,0 +1,32 @@
[package]
name = "icco"
version = "0.1.0"
edition = "2018"
description = "Common ICCO Types"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
backtraces = ["cosmwasm-std/backtraces"]
library = []
[dependencies]
cosmwasm-std = { version = "0.16.0" }
schemars = "0.8.1"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
cw20 = "0.8.0"
cw20-base = { version = "0.8.0", features = ["library"] }
cw-storage-plus = { version = "0.8.0" }
terraswap = "2.4.0"
thiserror = { version = "1.0.20" }
k256 = { version = "0.9.4", default-features = false, features = ["ecdsa"] }
sha3 = { version = "0.9.1", default-features = false }
generic-array = { version = "0.14.4" }
hex = "0.4.2"
lazy_static = "1.4.0"
bigint = "4"
[dev-dependencies]
cosmwasm-vm = { version = "0.16.0", default-features = false }
serde_json = "1.0"

View File

@ -0,0 +1,97 @@
use cosmwasm_std::CanonicalAddr;
pub trait ByteUtils {
fn get_u8(&self, index: usize) -> u8;
fn get_u16(&self, index: usize) -> u16;
fn get_u32(&self, index: usize) -> u32;
fn get_u64(&self, index: usize) -> u64;
fn get_u128_be(&self, index: usize) -> u128;
/// High 128 then low 128
fn get_u256(&self, index: usize) -> (u128, u128);
fn get_address(&self, index: usize) -> CanonicalAddr;
fn get_bytes32(&self, index: usize) -> &[u8];
fn get_bytes(&self, index: usize, bytes: usize) -> &[u8];
fn get_const_bytes<const N: usize>(&self, index: usize) -> [u8; N];
}
impl ByteUtils for &[u8] {
fn get_u8(&self, index: usize) -> u8 {
self[index]
}
fn get_u16(&self, index: usize) -> u16 {
let mut bytes: [u8; 16 / 8] = [0; 16 / 8];
bytes.copy_from_slice(&self[index..index + 2]);
u16::from_be_bytes(bytes)
}
fn get_u32(&self, index: usize) -> u32 {
let mut bytes: [u8; 32 / 8] = [0; 32 / 8];
bytes.copy_from_slice(&self[index..index + 4]);
u32::from_be_bytes(bytes)
}
fn get_u64(&self, index: usize) -> u64 {
let mut bytes: [u8; 64 / 8] = [0; 64 / 8];
bytes.copy_from_slice(&self[index..index + 8]);
u64::from_be_bytes(bytes)
}
fn get_u128_be(&self, index: usize) -> u128 {
let mut bytes: [u8; 128 / 8] = [0; 128 / 8];
bytes.copy_from_slice(&self[index..index + 128 / 8]);
u128::from_be_bytes(bytes)
}
fn get_u256(&self, index: usize) -> (u128, u128) {
(self.get_u128_be(index), self.get_u128_be(index + 128 / 8))
}
fn get_address(&self, index: usize) -> CanonicalAddr {
// 32 bytes are reserved for addresses, but only the last 20 bytes are taken by the actual address
CanonicalAddr::from(&self[index + 32 - 20..index + 32])
}
fn get_bytes32(&self, index: usize) -> &[u8] {
&self[index..index + 32]
}
fn get_bytes(&self, index: usize, bytes: usize) -> &[u8] {
&self[index..index + bytes]
}
fn get_const_bytes<const N: usize>(&self, index: usize) -> [u8; N] {
let mut bytes: [u8; N] = [0; N];
bytes.copy_from_slice(&self[index..index + N]);
bytes
}
}
/// Left-pad a 20 byte address with 0s
pub fn extend_address_to_32(addr: &CanonicalAddr) -> Vec<u8> {
extend_address_to_32_array(addr).to_vec()
}
pub fn extend_address_to_32_array(addr: &CanonicalAddr) -> [u8; 32] {
let mut v: Vec<u8> = vec![0; 12];
v.extend(addr.as_slice());
let mut result: [u8; 32] = [0; 32];
result.copy_from_slice(&v);
result
}
/// Turn a string into a fixed length array. If the string is shorter than the
/// resulting array, it gets padded with \0s on the right. If longer, it gets
/// truncated.
pub fn string_to_array<const N: usize>(s: &str) -> [u8; N] {
let bytes = s.as_bytes();
let len = usize::min(N, bytes.len());
let zeros = vec![0; N - len];
let padded = [bytes[..len].to_vec(), zeros].concat();
let mut result: [u8; N] = [0; N];
result.copy_from_slice(&padded);
result
}
pub fn extend_string_to_32(s: &str) -> Vec<u8> {
string_to_array::<32>(s).to_vec()
}
pub fn get_string_from_32(v: &[u8]) -> String {
let s = String::from_utf8_lossy(v);
s.chars().filter(|c| c != &'\0').collect()
}

View File

@ -0,0 +1,321 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::{Api, CanonicalAddr, DepsMut, StdError, StdResult, Uint128, Uint256};
use terraswap::asset::{Asset, AssetInfo};
use crate::byte_utils::ByteUtils;
// Chain ID of Terra
pub const CHAIN_ID: u16 = 3;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SaleTimes {
pub start: u64,
pub end: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SaleCore {
pub id: Vec<u8>,
pub token_address: Vec<u8>,
pub token_chain: u16,
pub token_amount: Uint256,
pub min_raise: Uint256,
pub max_raise: Uint256,
pub times: SaleTimes,
pub recipient: Vec<u8>,
pub refund_recipient: Vec<u8>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct AcceptedToken {
pub chain: u16,
pub address: Vec<u8>,
pub conversion_rate: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SaleStatus {
pub is_sealed: bool,
pub is_aborted: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Contribution {
pub token_index: u8,
pub contributed: Uint256, // actually Uint128, but will be serialized as Uint256
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Allocation {
pub token_index: u8,
pub allocated: Uint256,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SaleInit {
pub core: SaleCore,
pub accepted_tokens: Vec<AcceptedToken>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ContributionsSealed {
pub sale_id: Vec<u8>,
pub chain_id: u16,
pub contributions: Vec<Contribution>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SaleSealed {
pub sale_id: Vec<u8>,
pub allocations: Vec<Allocation>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SaleAborted {
pub sale_id: Vec<u8>,
}
impl AcceptedToken {
pub const N_BYTES: usize = 50;
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
let address = data.get_bytes32(0).to_vec();
if address.len() != 32 {
return Err(StdError::generic_err("address.len() != 32"));
}
let chain = data.get_u16(32);
let conversion_rate = data.get_u128_be(34);
Ok(AcceptedToken {
chain,
address,
conversion_rate: conversion_rate.into(),
})
}
pub fn serialize(&self) -> Vec<u8> {
[
self.chain.to_be_bytes().to_vec(),
self.address.to_vec(),
self.conversion_rate.u128().to_be_bytes().to_vec(),
]
.concat()
}
pub fn make_asset_info(&self, api: &dyn Api) -> StdResult<AssetInfo> {
if self.chain != CHAIN_ID {
return Err(StdError::generic_err("chain != terra"));
}
let addr = self.address.as_slice();
match addr[0] {
1u8 => {
// match first "u" (e.g. uusd)
match addr.iter().position(|&x| x == 117u8) {
Some(idx) => {
let denom = &addr[idx..32];
match String::from_utf8(denom.into()) {
Ok(denom) => Ok(AssetInfo::NativeToken { denom }),
_ => Err(StdError::generic_err("not valid denom")),
}
}
None => Err(StdError::generic_err("not valid denom")),
}
}
_ => {
let token_address = CanonicalAddr::from(&addr[12..32]);
let humanized = api.addr_humanize(&token_address)?;
Ok(AssetInfo::Token {
contract_addr: humanized.to_string(),
})
}
}
}
pub fn make_asset(&self, api: &dyn Api, amount: Uint128) -> StdResult<Asset> {
Ok(Asset {
info: self.make_asset_info(api)?,
amount: amount,
})
}
}
pub fn to_const_bytes32(data: &[u8], index: usize) -> [u8; 32] {
data.get_bytes32(index).get_const_bytes(0)
}
pub fn to_u256(data: &[u8], index: usize) -> Uint256 {
Uint256::new(to_const_bytes32(data, index))
}
impl SaleInit {
pub const PAYLOAD_ID: u8 = 1;
const INDEX_ACCEPTED_TOKENS_START: usize = 226;
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
let sale_id = data.get_bytes32(0).to_vec();
let token_address = data.get_bytes32(32).to_vec();
let token_chain = data.get_u16(64);
let token_amount = to_u256(data, 66);
let min_raise = to_u256(data, 98);
let max_raise = to_u256(data, 130);
let start = data.get_u64(162 + 24); // encoded as u256, but we only care about u64 time
let end = data.get_u64(194 + 24); // encoded as u256, but we only care about u64 for time
let accepted_tokens =
SaleInit::deserialize_tokens(&data[SaleInit::INDEX_ACCEPTED_TOKENS_START..])?;
let index = SaleInit::INDEX_ACCEPTED_TOKENS_START
+ 1
+ AcceptedToken::N_BYTES * accepted_tokens.len();
let recipient = data.get_bytes32(index).to_vec();
let refund_recipient = data.get_bytes32(index + 32).to_vec();
Ok(SaleInit {
core: SaleCore {
id: sale_id,
token_address,
token_chain,
token_amount,
min_raise,
max_raise,
times: SaleTimes { start, end },
recipient,
refund_recipient,
},
accepted_tokens,
})
}
fn deserialize_tokens(data: &[u8]) -> StdResult<Vec<AcceptedToken>> {
let n_tokens = data.get_u8(0) as usize;
let expected_length = 1 + AcceptedToken::N_BYTES * n_tokens;
if data.len() <= expected_length {
return Err(StdError::generic_err("data.len() < expected_length"));
}
let mut tokens: Vec<AcceptedToken> = Vec::with_capacity(n_tokens);
for i in 0..n_tokens {
let start = 1 + AcceptedToken::N_BYTES * i;
let end = 1 + AcceptedToken::N_BYTES * (i + 1);
tokens.push(AcceptedToken::deserialize(&data[start..end])?);
}
Ok(tokens)
}
}
impl ContributionsSealed {
pub const PAYLOAD_ID: u8 = 2;
pub fn new(sale_id: &[u8], chain_id: u16) -> Self {
ContributionsSealed {
sale_id: sale_id.to_vec(),
chain_id,
contributions: Vec::new(),
}
}
pub fn add_contribution(&mut self, token_index: u8, contributed: Uint128) -> StdResult<usize> {
// limit to len 255
if self.contributions.len() >= 256 {
return Err(StdError::generic_err("cannot exceed length 256"));
}
let contributed = Uint256::from(contributed.u128());
self.contributions.push(Contribution {
token_index,
contributed,
});
Ok(self.contributions.len())
}
/*
pub fn deserialize(data: &Vec<u8>) -> StdResult<Self> {
let data = data.as_slice();
let payload_id = data.get_u8(0);
if payload_id != ContributionsSealed::PAYLOAD_ID {
return Err(StdError::generic_err(
"payload_id != ContributionsSealed::PAYLOAD_ID"
));
}
let sale_id = data.get_bytes32(1).to_vec();
let chain_id = data.get_u16(33);
// TODO: deserialize
let contributions: Vec<Contribution> = Vec::new();
Ok(ContributionsSealed {
sale_id,
chain_id,
contributions,
})
}
*/
pub fn serialize(&self) -> Vec<u8> {
[
ContributionsSealed::PAYLOAD_ID.to_be_bytes().to_vec(),
self.sale_id.to_vec(),
self.chain_id.to_be_bytes().to_vec(),
self.serialize_contributions(),
]
.concat()
}
fn serialize_contributions(&self) -> Vec<u8> {
// TODO
Vec::new()
}
}
impl SaleSealed {
pub const PAYLOAD_ID: u8 = 3;
pub fn add_allocation(&mut self, token_index: u8, allocated: Uint256) -> StdResult<u8> {
self.allocations.push(Allocation {
token_index,
allocated,
});
Ok(token_index)
}
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
let sale_id = data.get_bytes32(0).to_vec();
let allocations = SaleSealed::deserialize_allocations(&data[32..]);
Ok(SaleSealed {
sale_id,
allocations,
})
}
pub fn deserialize_allocations(data: &[u8]) -> Vec<Allocation> {
// TODO
Vec::new()
}
/*
pub fn serialize(&self) -> Vec<u8> {
[
SaleSealed::PAYLOAD_IDto_be_bytes().to_vec(),
self.sale_id.clone(),
// TODO: serialize allocations
]
.concat()
}
*/
}
impl SaleAborted {
pub const PAYLOAD_ID: u8 = 4;
pub fn deserialize(data: &[u8]) -> StdResult<Self> {
Ok(SaleAborted {
sale_id: data.get_bytes32(0).to_vec(),
})
}
/*
pub fn serialize(&self) -> Vec<u8> {
[self.payload_id.to_be_bytes().to_vec(), self.sale_id.clone()].concat()
}
*/
}

View File

@ -0,0 +1,5 @@
#[cfg(test)]
extern crate lazy_static;
pub mod byte_utils;
pub mod common;