Add sale_sealed (need to add test)

This commit is contained in:
Karl Kempe 2022-05-13 14:22:49 +00:00
parent fba9fc57f3
commit f0872f8ac6
15 changed files with 3090 additions and 288 deletions

View File

@ -1,8 +1,10 @@
use cosmwasm_std::{
entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult,
};
use icco::common::CHAIN_ID;
use crate::{
error::ContributorError,
execute::{
attest_contributions, claim_allocation, claim_refund, contribute, init_sale, sale_aborted,
sale_sealed,
@ -30,16 +32,23 @@ pub fn instantiate(
) -> StdResult<Response> {
let wormhole = deps.api.addr_validate(msg.wormhole.as_str())?;
let token_bridge = deps.api.addr_validate(msg.token_bridge.as_str())?;
let cfg = Config {
wormhole,
token_bridge,
conductor_chain: msg.conductor_chain,
conductor_address: msg.conductor_address.into(),
owner: info.sender,
};
CONFIG.save(deps.storage, &cfg)?;
Ok(Response::default())
// we know there is no terra conductor existing. So prevent user
// from instantiating with one defined
match msg.conductor_chain {
CHAIN_ID => return ContributorError::UnsupportedConductor.std_err(),
_ => {
let cfg = Config {
wormhole,
token_bridge,
conductor_chain: msg.conductor_chain,
conductor_address: msg.conductor_address.into(),
owner: info.sender,
};
CONFIG.save(deps.storage, &cfg)?;
Ok(Response::default())
}
}
}
// When CW20 transfers complete, we need to verify the actual amount that is being transferred out

View File

@ -48,15 +48,24 @@ pub enum ContributorError {
#[error("SaleEnded")]
SaleEnded,
#[error("SaleNonexistent")]
SaleNonexistent,
#[error("SaleNotFinished")]
SaleNotFinished,
#[error("SaleNotStarted")]
SaleNotStarted,
#[error("SaleStillActive")]
SaleStillActive,
#[error("TooManyAcceptedTokens")]
TooManyAcceptedTokens,
#[error("UnsupportedConductor")]
UnsupportedConductor,
#[error("WrongBuyerStatus")]
WrongBuyerStatus,

View File

@ -1,11 +1,11 @@
use cosmwasm_std::{
to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, QuerierWrapper,
QueryRequest, Response, StdError, StdResult, Storage, Uint128, WasmMsg, WasmQuery,
to_binary, Addr, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, QuerierWrapper,
QueryRequest, Response, StdError, StdResult, Uint128, Uint256, WasmMsg, WasmQuery,
};
use cw20::Cw20ExecuteMsg;
use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, TokenInfoResponse};
use serde::de::DeserializeOwned;
use terraswap::{
asset::AssetInfo,
asset::{Asset, AssetInfo},
querier::{query_balance, query_token_balance},
};
@ -17,16 +17,20 @@ use wormhole::{
state::ParsedVAA,
};
use icco::common::{
make_asset_info, ContributionsSealed, SaleAborted, SaleInit, SaleSealed, SaleStatus, CHAIN_ID,
use icco::{
common::{
make_asset_info, to_uint128, AssetAllocation, ContributionsSealed, SaleAborted, SaleInit,
SaleSealed, SaleStatus, CHAIN_ID,
},
error::CommonError,
};
use crate::{
error::ContributorError,
msg::ExecuteMsg,
state::{
is_sale_active, sale_asset_indices, update_buyer_contribution, AssetKey, BuyerStatus,
PendingContributeToken, TokenIndexKey, ACCEPTED_ASSETS, ASSET_INDICES, CONFIG,
sale_asset_indices, throw_if_active, throw_if_inactive, update_buyer_contribution,
BuyerStatus, PendingContributeToken, TokenIndexKey, ACCEPTED_ASSETS, ASSET_INDICES, CONFIG,
PENDING_CONTRIBUTE_TOKEN, SALES, SALE_STATUSES, SALE_TIMES, TOTAL_ALLOCATIONS,
TOTAL_CONTRIBUTIONS,
},
@ -44,7 +48,7 @@ pub fn init_sale(
let payload = parse_and_verify_vaa(deps.as_ref(), &env, signed_vaa)?;
let sale_id = SaleInit::get_sale_id(&payload)?;
if is_sale_active(deps.storage, sale_id) {
if let Ok(_) = SALE_STATUSES.load(deps.storage, sale_id) {
return ContributorError::SaleAlreadyExists.std_err();
}
@ -99,7 +103,6 @@ pub fn init_sale(
// store other things associated with accepted tokens
TOTAL_CONTRIBUTIONS.save(deps.storage, token_key.clone(), &Uint128::zero())?;
TOTAL_ALLOCATIONS.save(deps.storage, token_key, &Uint128::zero())?;
}
let sale = &sale_init.core;
@ -123,9 +126,7 @@ pub fn contribute(
token_index: u8,
amount: Uint128,
) -> StdResult<Response> {
if !is_sale_active(deps.storage, sale_id) {
return ContributorError::SaleEnded.std_err();
}
throw_if_inactive(deps.storage, sale_id)?;
let times = SALE_TIMES.load(deps.storage, sale_id)?;
let now = env.block.time.seconds();
@ -158,15 +159,19 @@ pub fn attest_contributions(
_info: MessageInfo,
sale_id: &[u8],
) -> StdResult<Response> {
if !is_sale_active(deps.storage, sale_id) {
return ContributorError::SaleEnded.std_err();
}
throw_if_inactive(deps.storage, sale_id)?;
let times = SALE_TIMES.load(deps.storage, sale_id)?;
if env.block.time.seconds() <= times.end {
return ContributorError::SaleNotFinished.std_err();
}
// Do we care if the orchestrator can attest contributions multiple times?
// The conductor can only collect contributions once per chain, so there
// is no need to add a protection against attesting more than once.
// If we want to add a protection, we can cache the serialized payload
// and check if this is already in storage per sale_id.
let asset_indices = sale_asset_indices(deps.storage, sale_id);
let mut contribution_sealed = ContributionsSealed::new(sale_id, CHAIN_ID, asset_indices.len());
@ -181,7 +186,7 @@ pub fn attest_contributions(
let cfg = CONFIG.load(deps.storage)?;
Ok(Response::new()
.add_message(execute_contract(
.add_message(execute_contract_without_funds(
&cfg.wormhole,
to_binary(&WormholeExecuteMsg::PostMessage {
message: Binary::from(serialized),
@ -201,12 +206,9 @@ pub fn sale_sealed(
let payload = parse_and_verify_vaa(deps.as_ref(), &env, signed_vaa)?;
let sale_id = SaleSealed::get_sale_id(&payload)?;
let sale = match is_sale_active(deps.storage, sale_id) {
true => SALES.load(deps.storage, sale_id)?,
_ => {
return ContributorError::SaleEnded.std_err();
}
};
throw_if_inactive(deps.storage, sale_id)?;
let sale = SALES.load(deps.storage, sale_id)?;
// sale token handling
let asset_info = match sale.token_chain {
@ -221,17 +223,27 @@ pub fn sale_sealed(
}
};
let balance = match asset_info {
let (balance, token_decimals) = match asset_info {
AssetInfo::NativeToken { denom } => {
// this should never happen, but...
query_balance(&deps.querier, env.contract.address, denom)?
(
query_balance(&deps.querier, env.contract.address, denom)?,
6u8,
)
}
AssetInfo::Token { contract_addr } => {
let contract_addr = Addr::unchecked(contract_addr);
let token_info: TokenInfoResponse = query_contract(
&deps.querier,
&contract_addr,
to_binary(&Cw20QueryMsg::TokenInfo {})?,
)?;
let balance = query_token_balance(&deps.querier, contract_addr, env.contract.address)?;
(balance, token_info.decimals)
}
AssetInfo::Token { contract_addr } => query_token_balance(
&deps.querier,
Addr::unchecked(contract_addr),
env.contract.address,
)?,
};
// save some work if we don't have any of the sale tokens
if balance == Uint128::zero() {
return ContributorError::InsufficientSaleTokens.std_err();
@ -243,10 +255,14 @@ pub fn sale_sealed(
let parsed_allocations =
SaleSealed::deserialize_allocations_safely(&payload, sale.num_accepted, &asset_indices)?;
// sum total allocations and check against balance in the contract
// Sum total allocations and check against balance in the contract.
// Keep in mind, these are Uint256. Need to adjust this down to Uint128
let total_allocations = parsed_allocations
.iter()
.fold(Uint128::zero(), |total, (_, next)| total + next.allocated);
.fold(Uint256::zero(), |total, (_, next)| total + next.allocated);
// need to adjust total_allocations based on decimal difference
let total_allocations = sale.adjust_token_amount(total_allocations, token_decimals)?;
if balance < total_allocations {
return ContributorError::InsufficientSaleTokens.std_err();
@ -258,19 +274,51 @@ pub fn sale_sealed(
// transfer contributions to conductor
let cfg = CONFIG.load(deps.storage)?;
if cfg.conductor_chain == CHAIN_ID {
// lol
} else {
for &token_index in asset_indices.iter() {
// do token bridge transfers here
}
return ContributorError::UnsupportedConductor.std_err();
}
let mut token_bridge_msgs: Vec<CosmosMsg> = Vec::with_capacity(sale.num_accepted as usize);
for &token_index in asset_indices.iter() {
// bridge assets to conductor
let amount = TOTAL_CONTRIBUTIONS.load(deps.storage, (sale_id, token_index.into()))?;
let asset = Asset {
info: ACCEPTED_ASSETS.load(deps.storage, (sale_id, token_index.into()))?,
amount,
};
token_bridge_msgs.push(execute_contract_without_funds(
&cfg.token_bridge,
to_binary(&TokenBridgeExecuteMsg::InitiateTransfer {
asset,
recipient_chain: cfg.conductor_chain,
recipient: Binary::from(cfg.conductor_address.clone()),
fee: Uint128::zero(), // does this matter?
nonce: WORMHOLE_NONCE,
})?,
));
}
// now update TOTAL_ALLOCATIONS (fix to use AssetAllocation)
for (token_index, allocation) in parsed_allocations.iter() {
//TOTAL_ALLOCATIONS.save(deps.storage, (sale_id, token_index.into()), )
// adjust values from uint256 to uint128
let allocated = sale.adjust_token_amount(allocation.allocated, token_decimals)?;
let excess_contributed = match to_uint128(allocation.excess_contributed) {
Some(value) => value,
None => return CommonError::AmountExceedsUint128Max.std_err(),
};
TOTAL_ALLOCATIONS.save(
deps.storage,
(sale_id, (*token_index).into()),
&AssetAllocation {
allocated,
excess_contributed,
},
)?;
}
Ok(Response::new()
.add_messages(token_bridge_msgs)
.add_attribute("action", "sale_sealed")
.add_attribute("sale_id", Binary::from(sale_id).to_base64())
.add_attribute("total_allocations", total_allocations.to_string()))
@ -283,7 +331,7 @@ pub fn claim_allocation(
sale_id: &Binary,
token_index: u8,
) -> StdResult<Response> {
// TODO devs do something
throw_if_active(deps.storage, sale_id)?;
Ok(Response::new().add_attribute("action", "claim_allocation"))
}
@ -297,9 +345,7 @@ pub fn sale_aborted(
let payload = parse_and_verify_vaa(deps.as_ref(), &env, signed_vaa)?;
let sale_id = SaleAborted::get_sale_id(&payload)?;
if !is_sale_active(deps.storage, sale_id) {
return ContributorError::SaleEnded.std_err();
};
throw_if_inactive(deps.storage, sale_id)?;
SALE_STATUSES.save(deps.storage, sale_id, &SaleStatus::Aborted)?;
@ -315,7 +361,7 @@ pub fn claim_refund(
sale_id: &Binary,
token_index: u8,
) -> StdResult<Response> {
// TODO devs do something
throw_if_active(deps.storage, sale_id)?;
Ok(Response::new().add_attribute("action", "claim_refund"))
}
@ -417,7 +463,7 @@ fn contribute_token(
)?;
Ok(Response::new()
.add_message(execute_contract(
.add_message(execute_contract_without_funds(
&contract_addr,
to_binary(&Cw20ExecuteMsg::TransferFrom {
owner: info.sender.to_string(),
@ -425,7 +471,7 @@ fn contribute_token(
amount,
})?,
))
.add_message(execute_contract(
.add_message(execute_contract_without_funds(
&env.contract.address,
to_binary(&ExecuteMsg::EscrowUserContributionHook)?,
))
@ -490,8 +536,17 @@ fn query_contract<T: DeserializeOwned>(
}))
}
// assume only one coin for funds
fn execute_contract_with_funds(contract: &Addr, msg: Binary, funds: Coin) -> CosmosMsg {
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: contract.to_string(),
funds: vec![funds],
msg,
})
}
// no need for funds
fn execute_contract(contract: &Addr, msg: Binary) -> CosmosMsg {
fn execute_contract_without_funds(contract: &Addr, msg: Binary) -> CosmosMsg {
CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: contract.to_string(),
funds: vec![],

View File

@ -3,7 +3,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use terraswap::asset::AssetInfo;
use icco::common::{SaleCore, SaleStatus, SaleTimes};
use icco::common::{AssetAllocation, SaleCore, SaleStatus, SaleTimes};
use crate::state::BuyerStatus;
@ -136,7 +136,7 @@ pub struct TotalContributionResponse {
pub struct TotalAllocationResponse {
pub id: Vec<u8>,
pub token_index: u8,
pub amount: Uint128,
pub allocation: AssetAllocation,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]

View File

@ -60,7 +60,7 @@ pub fn query_total_allocation(
Ok(TotalAllocationResponse {
id: sale_id.to_vec(),
token_index,
amount: TOTAL_ALLOCATIONS.load(deps.storage, (sale_id, token_index.into()))?,
allocation: TOTAL_ALLOCATIONS.load(deps.storage, (sale_id, token_index.into()))?,
})
}

View File

@ -4,7 +4,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use terraswap::asset::AssetInfo;
use icco::common::{SaleCore, SaleStatus, SaleTimes};
use icco::common::{AssetAllocation, SaleCore, SaleStatus, SaleTimes};
use crate::error::ContributorError;
@ -73,7 +73,7 @@ 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_ASSETS: Map<TokenIndexKey, AssetInfo> = Map::new("accepted_assets");
pub const TOTAL_CONTRIBUTIONS: Map<TokenIndexKey, Uint128> = Map::new("total_contributions");
pub const TOTAL_ALLOCATIONS: Map<TokenIndexKey, Uint128> = Map::new("total_allocations");
pub const TOTAL_ALLOCATIONS: Map<TokenIndexKey, AssetAllocation> = Map::new("total_allocations");
// per buyer
pub const BUYER_STATUSES: Map<BuyerTokenIndexKey, BuyerStatus> = Map::new("buyer_statuses");
@ -175,10 +175,27 @@ pub fn refund_is_claimed(
)
}
pub fn is_sale_active(storage: &dyn Storage, sale_id: &[u8]) -> bool {
pub fn throw_if_active(storage: &dyn Storage, sale_id: &[u8]) -> StdResult<()> {
match SALE_STATUSES.load(storage, sale_id) {
Ok(status) => status == SaleStatus::Active,
Err(_) => false,
Ok(active) => {
if active == SaleStatus::Active {
return ContributorError::SaleStillActive.std_err();
}
Ok(())
}
Err(_) => return ContributorError::SaleNonexistent.std_err(),
}
}
pub fn throw_if_inactive(storage: &dyn Storage, sale_id: &[u8]) -> StdResult<()> {
match SALE_STATUSES.load(storage, sale_id) {
Ok(active) => {
if active != SaleStatus::Active {
return ContributorError::SaleEnded.std_err();
}
Ok(())
}
Err(_) => return ContributorError::SaleNonexistent.std_err(),
}
}

View File

@ -208,7 +208,7 @@ fn init_sale() -> StdResult<()> {
3057543273fcb20100000001000000020002000000000000000000000000f19a\
2a01b70519f67adb309a994ec8c69a967e8b0000000000000003010100000000\
0000000000000000000000000000000000000000000000000000000200000000\
000000000000000083752ecafebf4707258dedffbd9c7443148169db00020000\
000000000000000083752ecafebf4707258dedffbd9c7443148169db0002120000\
000000000000000000000000000000000000000000000de0b6b3a76400000000\
000000000000000000000000000000000000000000008ac7230489e800000000\
00000000000000000000000000000000000000000000c249fdd3277800000000\
@ -407,21 +407,6 @@ fn init_sale() -> StdResult<()> {
Uint128::zero(),
"total_contribution.amount != 0"
);
let response = query(
deps.as_ref(),
mock_env(),
QueryMsg::TotalAllocation {
sale_id: Binary::from(sale_id),
token_index,
},
)?;
let total_allocation: TotalAllocationResponse = from_binary(&response)?;
assert_eq!(
total_allocation.amount,
Uint128::zero(),
"total_allocation.amount != 0"
);
}
Ok(())

View File

@ -19,6 +19,7 @@ pub struct SaleTimes {
pub struct SaleCore {
pub token_address: Vec<u8>,
pub token_chain: u16,
pub token_decimals: u8,
pub token_amount: Uint256,
pub min_raise: Uint256,
pub max_raise: Uint256,
@ -50,7 +51,7 @@ pub struct Contribution {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Allocation {
pub allocated: Uint256, // actually Uint128, but will be serialized as Uint256
pub allocated: Uint256,
pub excess_contributed: Uint256,
}
@ -85,6 +86,24 @@ pub struct SaleAborted<'a> {
pub id: &'a [u8],
}
impl SaleCore {
pub fn adjust_token_amount(&self, amount: Uint256, decimals: u8) -> StdResult<Uint128> {
let adjusted: Uint256;
if self.token_decimals > decimals {
let x = self.token_decimals - decimals;
adjusted = amount / Uint256::from(10u128).pow(x as u32);
} else {
let x = decimals - self.token_decimals;
adjusted = amount * Uint256::from(10u128).pow(x as u32);
}
match to_uint128(adjusted) {
Some(value) => return Ok(value),
None => return CommonError::AmountExceedsUint128Max.std_err(),
}
}
}
impl AcceptedToken {
pub const N_BYTES: usize = 50;
@ -107,14 +126,6 @@ impl AcceptedToken {
}
}
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 Contribution {
pub const NUM_BYTES: usize = 33; // 1 + 32
}
@ -125,7 +136,7 @@ impl Allocation {
impl<'a> SaleInit<'a> {
pub const PAYLOAD_ID: u8 = 1;
const INDEX_ACCEPTED_TOKENS_START: usize = 227;
const INDEX_ACCEPTED_TOKENS_START: usize = 228;
pub fn get_sale_id(data: &'a [u8]) -> StdResult<&[u8]> {
match data[0] {
@ -138,11 +149,12 @@ impl<'a> SaleInit<'a> {
pub fn deserialize(id: &'a [u8], data: &'a [u8]) -> StdResult<Self> {
let token_address = data.get_bytes32(33).to_vec();
let token_chain = data.get_u16(65);
let token_amount = to_u256(data, 67);
let min_raise = to_u256(data, 99);
let max_raise = to_u256(data, 131);
let start = data.get_u64(163 + 24); // encoded as u256, but we only care about u64 time
let end = data.get_u64(195 + 24); // encoded as u256, but we only care about u64 for time
let token_decimals = data[67]; // TODO: double-check this is correct
let token_amount = to_u256(data, 68);
let min_raise = to_u256(data, 100);
let max_raise = to_u256(data, 132);
let start = data.get_u64(164 + 24); // encoded as u256, but we only care about u64 time
let end = data.get_u64(196 + 24); // encoded as u256, but we only care about u64 for time
let accepted_tokens =
SaleInit::deserialize_tokens(&data[SaleInit::INDEX_ACCEPTED_TOKENS_START..])?;
@ -159,6 +171,7 @@ impl<'a> SaleInit<'a> {
core: SaleCore {
token_address,
token_chain,
token_decimals,
token_amount,
min_raise,
max_raise,
@ -208,8 +221,7 @@ impl<'a> ContributionsSealed<'a> {
return Err(StdError::generic_err("cannot exceed length 256"));
}
let result = contributions.iter().find(|c| c.token_index == token_index);
if result != None {
if let Some(_) = contributions.iter().find(|c| c.token_index == token_index) {
return Err(StdError::generic_err(
"token_index already in contributions",
));
@ -278,31 +290,19 @@ impl<'a> SaleSealed<'a> {
data: &[u8],
expected_num_allocations: u8,
indices: &Vec<u8>,
) -> StdResult<Vec<(u8, AssetAllocation)>> {
) -> StdResult<Vec<(u8, Allocation)>> {
if data[33] != expected_num_allocations {
return Err(StdError::generic_err("encoded num_allocations != expected"));
}
let mut parsed: Vec<(u8, AssetAllocation)> = Vec::with_capacity(indices.len());
let mut parsed: Vec<(u8, Allocation)> = Vec::with_capacity(indices.len());
for &token_index in indices {
let i = SaleSealed::HEADER_LEN + (token_index as usize) * Allocation::NUM_BYTES;
// allocated
let (invalid, allocated) = data.get_u256(i);
if invalid > 0 {
return Err(StdError::generic_err("allocated too large"));
}
// excess_contribution
let (invalid, excess_contributed) = data.get_u256(i + 32);
if invalid > 0 {
return Err(StdError::generic_err("excess_contributed too large"));
}
parsed.push((
token_index,
AssetAllocation {
allocated: allocated.into(),
excess_contributed: excess_contributed.into(),
Allocation {
allocated: to_u256(data, i + 1),
excess_contributed: to_u256(data, i + 33),
},
));
}
@ -337,6 +337,14 @@ impl<'a> SaleAborted<'a> {
*/
}
fn to_const_bytes32(data: &[u8], index: usize) -> [u8; 32] {
data.get_bytes32(index).get_const_bytes(0)
}
fn to_u256(data: &[u8], index: usize) -> Uint256 {
Uint256::new(to_const_bytes32(data, index))
}
pub fn make_asset_info(api: &dyn Api, addr: &[u8]) -> StdResult<AssetInfo> {
match addr[0] {
1u8 => {
@ -361,3 +369,13 @@ pub fn make_asset_info(api: &dyn Api, addr: &[u8]) -> StdResult<AssetInfo> {
}
}
}
pub fn to_uint128(value: Uint256) -> Option<Uint128> {
if value > Uint256::from(u128::MAX) {
return None;
}
let bytes = value.to_be_bytes();
let (_, value) = bytes.as_slice().get_u256(0);
Some(value.into())
}

View File

@ -3,6 +3,9 @@ use thiserror::Error;
#[derive(Error, Debug)]
pub enum CommonError {
#[error("AmountExceedsUint128Max")]
AmountExceedsUint128Max,
#[error("InvalidVaaAction")]
InvalidVaaAction,
}

View File

@ -15,8 +15,6 @@ import {
Wallet,
} from "@terra-money/terra.js";
import { readFileSync, writeFileSync, existsSync } from "fs";
import path from "path";
import { CustomError } from "ts-custom-error";
const DEFAULT_GAS_CURRENCY = "uusd";
const DEFAULT_GAS_PRICE = 0.15;
@ -49,10 +47,7 @@ export function newLocalClient(): Client {
return newClient("localterra", undefined);
}
export function newClient(
network: string,
mnemonic: string | undefined
): Client {
export function newClient(network: string, mnemonic: string | undefined): Client {
if (network == "mainnet" || network == "testnet") {
if (mnemonic === undefined) {
throw Error("mnemonic undefined");
@ -90,16 +85,6 @@ export async function sleep(timeout: number) {
await new Promise((resolve) => setTimeout(resolve, timeout));
}
export class TransactionError extends CustomError {
public constructor(
public code: number,
public codespace: string | undefined,
public rawLog: string
) {
super("transaction failed");
}
}
export async function createTransaction(wallet: Wallet, msg: Msg) {
let gas_currency = process.env.GAS_CURRENCY! || DEFAULT_GAS_CURRENCY;
let gas_price = process.env.GAS_PRICE! || DEFAULT_GAS_PRICE;
@ -115,24 +100,12 @@ export async function broadcastTransaction(terra: LCDClient, signedTx: Tx) {
return result;
}
export async function performTransaction(
terra: LCDClient,
wallet: Wallet,
msg: Msg
) {
export async function performTransaction(terra: LCDClient, wallet: Wallet, msg: Msg) {
const signedTx = await createTransaction(wallet, msg);
const result = await broadcastTransaction(terra, signedTx);
//if (isTxError(result)) {
// throw new TransactionError(parseInt(result.code), result.codespace, result.raw_log);
//}
return result;
return broadcastTransaction(terra, signedTx);
}
export async function uploadContract(
terra: LCDClient,
wallet: Wallet,
filepath: string
) {
export async function uploadContract(terra: LCDClient, wallet: Wallet, filepath: string) {
const contract = readFileSync(filepath, "base64");
const uploadMsg = new MsgStoreCode(wallet.key.accAddress, contract);
const receipt = await performTransaction(terra, wallet, uploadMsg);
@ -164,19 +137,14 @@ export async function instantiateContract(
}
export async function executeContract(
terra: LCDClient,
//terra: LCDClient,
wallet: Wallet,
contractAddress: string,
msg: object,
coins?: Coins.Input
) {
const executeMsg = new MsgExecuteContract(
wallet.key.accAddress,
contractAddress,
msg,
coins
);
return await performTransaction(terra, wallet, executeMsg);
const executeMsg = new MsgExecuteContract(wallet.key.accAddress, contractAddress, msg, coins);
return await performTransaction(wallet.lcd, wallet, executeMsg);
}
export async function queryContract(
@ -195,13 +163,7 @@ export async function deployContract(
initMsg: object
) {
const codeId = await uploadContract(terra, wallet, filepath);
return await instantiateContract(
terra,
wallet,
admin_address,
codeId,
initMsg
);
return await instantiateContract(terra, wallet, admin_address, codeId, initMsg);
}
export async function migrate(
@ -211,12 +173,7 @@ export async function migrate(
newCodeId: number,
msg: object
) {
const migrateMsg = new MsgMigrateContract(
wallet.key.accAddress,
contractAddress,
newCodeId,
msg
);
const migrateMsg = new MsgMigrateContract(wallet.key.accAddress, contractAddress, newCodeId, msg);
return await performTransaction(terra, wallet, migrateMsg);
}
@ -231,11 +188,7 @@ export async function update_contract_admin(
contract_address: string,
admin_address: string
) {
let msg = new MsgUpdateContractAdmin(
wallet.key.accAddress,
admin_address,
contract_address
);
let msg = new MsgUpdateContractAdmin(wallet.key.accAddress, admin_address, contract_address);
return await performTransaction(terra, wallet, msg);
}

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,11 @@
"bignumber.js": "^9.0.1",
"dotenv": "^8.2.0",
"elliptic": "^6.5.4",
"ethers": "^5.6.4",
"prettier": "^2.6.1",
"ts-custom-error": "^3.2.0",
"web3-eth-abi": "^1.7.3",
"web3-utils": "^1.7.3",
"yargs": "^17.4.1"
},
"devDependencies": {

View File

@ -6,24 +6,38 @@ This is a work-in-progress.
Tests will cover the following scenarios in `contributor.ts` (see [whitepaper](../../../WHITEPAPER.md) for details on how the Contributor works).
### Preparation
- [x] Mint CW20 Mock Token
- [x] Query Balance of Mock Token
### Deployment
- [x] Deploy Contract
- [ ] Expect Error when Non-Owner Attempts to Upgrade Contract
- [ ] Non-Owner Cannot Upgrade Contract
- [ ] Upgrade Contract
### Conduct Successful Sale
- [ ] Orchestrator Initializes Sale
- [ ] User Contributes to Sale
- [ ] Orchestrator Attests Contributions
- [ ] Orchestrator Seals Sale
- [ ] User Claims Allocations
- [x] 1. Orchestrator Initializes Sale
- [x] 2. Orchestrator Cannot Intialize Sale Again
- [x] 3. User Contributes to Sale (Native)
- [x] 4. User Contributes to Sale (CW20)
- [x] 5. User Cannot Contribute for Non-existent Token Index
- [x] 6. Orchestrator Cannot Attest Contributions Too Early
- [x] 7. User Cannot Contribute After Sale Ended
- [x] 8. Orchestrator Attests Contributions
- [ ] 9. Orchestrator Seals Sale
- [ ] 10. Orchestrator Cannot Seal Sale Again
- [ ] 11. User Claims Allocations
- [ ] 12. User Cannot Claim Allocations Again
### Conduct Aborted Sale
- [ ] Orchestrator Initializes Sale
- [ ] User Contributes to Sale
- [ ] Orchestrator Attests Contributions
- [ ] Orchestrator Aborts Sale
- [ ] User Claims Refunds
- [x] 1. Orchestrator Initializes Sale
- [x] 2. User Contributes to Sale (Native)
- [x] 3. User Contributes to Sale (CW20)
- [ ] 4. Orchestrator Aborts Sale
- [ ] 5. Orchestrator Cannot Abort Sale Again
- [ ] 6. User Claims Refunds
- [ ] 7. User Cannot Claims Refunds Again

File diff suppressed because it is too large Load Diff

298
terra/scripts/tests/lib.ts Normal file
View File

@ -0,0 +1,298 @@
import { Int, LCDClient, LocalTerra, Wallet } from "@terra-money/terra.js";
import { BigNumber, BigNumberish } from "ethers";
import { soliditySha3 } from "web3-utils";
import { queryContract } from "../helpers";
const elliptic = require("elliptic");
// sale struct info
const NUM_BYTES_ACCEPTED_TOKEN = 50;
const NUM_BYTES_ALLOCATION = 65;
// conductor info
export const CONDUCTOR_CHAIN = 2;
export const CONDUCTOR_ADDRESS = "000000000000000000000000f19a2a01b70519f67adb309a994ec8c69a967e8b";
export interface Actors {
owner: Wallet;
seller: Wallet;
buyers: Wallet[];
}
export function makeClientAndWallets(): [LCDClient, Actors] {
const terra = new LocalTerra();
const wallets = terra.wallets;
return [
terra,
{
owner: wallets.test1,
seller: wallets.test2,
buyers: [wallets.test3, wallets.test4],
},
];
}
export function signAndEncodeConductorVaa(
timestamp: number,
nonce: number,
sequence: number,
data: Buffer
): Buffer {
return signAndEncodeVaaBeta(
//return signAndEncodeVaaLegacy(
timestamp,
nonce,
CONDUCTOR_CHAIN,
CONDUCTOR_ADDRESS,
sequence,
data
);
}
export function signAndEncodeVaaBeta(
timestamp: number,
nonce: number,
emitterChainId: number,
emitterAddress: string,
sequence: number,
data: Buffer
): Buffer {
if (Buffer.from(emitterAddress, "hex").length != 32) {
throw Error("emitterAddress != 32 bytes");
}
// wormhole initialized with only one guardian in devnet
const signers = ["cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"];
const sigStart = 6;
const numSigners = signers.length;
const sigLength = 66;
const bodyStart = sigStart + sigLength * numSigners;
const bodyHeaderLength = 51;
const vm = Buffer.alloc(bodyStart + bodyHeaderLength + data.length);
// header
const guardianSetIndex = 0;
vm.writeUInt8(1, 0);
vm.writeUInt32BE(guardianSetIndex, 1);
vm.writeUInt8(numSigners, 5);
// encode body with arbitrary consistency level
const consistencyLevel = 1;
vm.writeUInt32BE(timestamp, bodyStart);
vm.writeUInt32BE(nonce, bodyStart + 4);
vm.writeUInt16BE(emitterChainId, bodyStart + 8);
vm.write(emitterAddress, bodyStart + 10, "hex");
vm.writeBigUInt64BE(BigInt(sequence), bodyStart + 42);
vm.writeUInt8(consistencyLevel, bodyStart + 50);
vm.write(data.toString("hex"), bodyStart + bodyHeaderLength, "hex");
// signatures
const body = vm.subarray(bodyStart).toString("hex");
const hash = soliditySha3(soliditySha3("0x" + body)!)!.substring(2);
for (let i = 0; i < numSigners; ++i) {
const ec = new elliptic.ec("secp256k1");
const key = ec.keyFromPrivate(signers[i]);
const signature = key.sign(hash, { canonical: true });
const start = sigStart + i * sigLength;
vm.writeUInt8(i, start);
vm.write(signature.r.toString(16).padStart(64, "0"), start + 1, "hex");
vm.write(signature.s.toString(16).padStart(64, "0"), start + 33, "hex");
vm.writeUInt8(signature.recoveryParam, start + 65);
//console.log(" beta signature", vm.subarray(start, start + 66).toString("hex"));
}
return vm;
}
export interface AcceptedToken {
address: string; // 32 bytes
chain: number; // uint16
conversionRate: string; // uint128
}
export function encodeAcceptedTokens(acceptedTokens: AcceptedToken[]): Buffer {
const n = acceptedTokens.length;
const encoded = Buffer.alloc(NUM_BYTES_ACCEPTED_TOKEN * n);
for (let i = 0; i < n; ++i) {
const token = acceptedTokens[i];
const start = i * NUM_BYTES_ACCEPTED_TOKEN;
encoded.write(token.address, start, "hex");
encoded.writeUint16BE(token.chain, start + 32);
encoded.write(toBigNumberHex(token.conversionRate, 16), start + 34, "hex");
}
return encoded;
}
export function encodeSaleInit(
saleId: number,
tokenAddress: string, // 32 bytes
tokenChain: number,
tokenAmount: string, // uint256
minRaise: string, // uint256
maxRaise: string, // uint256
saleStart: number,
saleEnd: number,
acceptedTokens: AcceptedToken[], // 50 * n_tokens
recipient: string, // 32 bytes
refundRecipient: string // 32 bytes
): Buffer {
const numTokens = acceptedTokens.length;
const encoded = Buffer.alloc(292 + numTokens * NUM_BYTES_ACCEPTED_TOKEN);
encoded.writeUInt8(1, 0); // initSale payload = 1
encoded.write(toBigNumberHex(saleId, 32), 1, "hex");
encoded.write(tokenAddress, 33, "hex");
encoded.writeUint16BE(tokenChain, 65);
encoded.write(toBigNumberHex(tokenAmount, 32), 67, "hex");
encoded.write(toBigNumberHex(minRaise, 32), 99, "hex");
encoded.write(toBigNumberHex(maxRaise, 32), 131, "hex");
encoded.write(toBigNumberHex(saleStart, 32), 163, "hex");
encoded.write(toBigNumberHex(saleEnd, 32), 195, "hex");
encoded.writeUInt8(numTokens, 227);
encoded.write(encodeAcceptedTokens(acceptedTokens).toString("hex"), 228, "hex");
const recipientIndex = 228 + numTokens * NUM_BYTES_ACCEPTED_TOKEN;
encoded.write(recipient, recipientIndex, "hex");
encoded.write(refundRecipient, recipientIndex + 32, "hex");
return encoded;
}
export interface Allocation {
allocation: BigNumber; // uint256
excessContribution: BigNumber; // uint256
}
export function encodeAllocations(allocations: Allocation[]): Buffer {
const n = allocations.length;
const encoded = Buffer.alloc(NUM_BYTES_ALLOCATION * n);
for (let i = 0; i < n; ++i) {
const item = allocations[i];
const start = i * NUM_BYTES_ALLOCATION;
encoded.writeUint8(i, start);
encoded.write(toBigNumberHex(item.allocation, 32), start + 1, "hex");
encoded.write(toBigNumberHex(item.excessContribution, 32), start + 33, "hex");
}
return encoded;
}
export function encodeSaleSealed(
saleId: number,
allocations: Allocation[] // 65 * n_allocations
): Buffer {
const headerLen = 33;
const numAllocations = allocations.length;
const encoded = Buffer.alloc(headerLen + numAllocations * NUM_BYTES_ALLOCATION);
encoded.writeUInt8(3, 0); // saleSealed payload = 3
encoded.write(toBigNumberHex(saleId, 32), 1, "hex");
encoded.write(encodeAllocations(allocations).toString("hex"), headerLen, "hex");
return encoded;
}
export function encodeSaleAborted(saleId: number): Buffer {
const encoded = Buffer.alloc(33);
encoded.writeUInt8(4, 0); // saleSealed payload = 4
encoded.write(toBigNumberHex(saleId, 32), 1, "hex");
return encoded;
}
function toBigNumberHex(value: BigNumberish, numBytes: number): string {
return BigNumber.from(value)
.toHexString()
.substring(2)
.padStart(numBytes * 2, "0");
}
// misc
export async function getBlockTime(terra: LCDClient): Promise<number> {
const info = await terra.tendermint.blockInfo();
const time = new Date(info.block.header.time);
return Math.floor(time.getTime() / 1000);
}
export function getErrorMessage(error: any): string {
return error.response.data.message;
}
// contract queries
export async function getBalance(terra: LCDClient, asset: string, account: string): Promise<Int> {
if (asset.startsWith("u")) {
const [balance] = await terra.bank.balance(account);
const coin = balance.get(asset);
if (coin !== undefined) {
return new Int(coin.amount);
}
return new Int(0);
}
const msg: any = {
balance: {
address: account,
},
};
const result = await queryContract(terra, asset, msg);
return new Int(result.balance);
}
export async function getBuyerStatus(
terra: LCDClient,
contributor: string,
saleId: Buffer,
tokenIndex: number,
buyer: string
): Promise<any> {
const msg: any = {
buyer_status: {
sale_id: saleId.toString("base64"),
token_index: tokenIndex,
buyer,
},
};
const result = await queryContract(terra, contributor, msg);
// verify header
for (let i = 0; i < 32; ++i) {
if (result.id[i] != saleId[i]) {
throw Error("id != expected");
}
}
if (result.token_index != tokenIndex) {
throw Error("token_index != expected");
}
if (result.buyer != buyer) {
throw Error("buyer != expected");
}
return result.status;
}
export async function getTotalContribution(
terra: LCDClient,
contributor: string,
saleId: Buffer,
tokenIndex: number
): Promise<Int> {
const msg: any = {
total_contribution: {
sale_id: saleId.toString("base64"),
token_index: tokenIndex,
},
};
const result = await queryContract(terra, contributor, msg);
// verify header
for (let i = 0; i < 32; ++i) {
if (result.id[i] != saleId[i]) {
throw Error("id != expected");
}
}
if (result.token_index != tokenIndex) {
throw Error("token_index != expected");
}
return new Int(result.amount);
}