cosmwasm: Add core accounting package

The accounting package implements the reusable, chain agnostic part of
tokenbridge accounting.

Part of #1880.
This commit is contained in:
Chirantan Ekbote 2022-11-15 15:44:13 +09:00 committed by Chirantan Ekbote
parent 4188373a69
commit c5925f1467
10 changed files with 1925 additions and 1 deletions

18
cosmwasm/Cargo.lock generated
View File

@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "accounting"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"cosmwasm-schema",
"cosmwasm-std",
"cw-storage-plus",
"hex",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "addr2line"
version = "0.17.0"
@ -33,6 +48,9 @@ name = "anyhow"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
dependencies = [
"backtrace",
]
[[package]]
name = "autocfg"

View File

@ -6,6 +6,7 @@ members = [
"contracts/token-bridge",
"contracts/shutdown-token-bridge",
"contracts/mock-bridge-integration",
"packages/accounting",
"packages/wormhole-bindings",
]
@ -21,8 +22,8 @@ incremental = false
overflow-checks = true
[patch.crates-io]
accounting = { path = "packages/accounting" }
cw20-wrapped-2 = { path = "contracts/cw20-wrapped" }
token-bridge-terra-2 = { path = "contracts/token-bridge" }
wormhole-bindings = { path = "packages/wormhole-bindings" }
wormhole-bridge-terra-2 = { path = "contracts/wormhole" }

View File

@ -0,0 +1,28 @@
[package]
name = "accounting"
version = "0.1.0"
authors = ["Wormhole Project Contributors"]
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# for more explicit tests, cargo test --features=backtraces
backtraces = ["cosmwasm-std/backtraces"]
# use library feature to disable all instantiate/execute/query exports
library = []
[dependencies]
anyhow = "1"
base64 = "0.13"
cosmwasm-schema = "1"
cosmwasm-std = "1"
cw-storage-plus = "0.13.2"
hex = "0.4.3"
schemars = "0.8.8"
serde = { version = "1.0.137", default-features = false }
thiserror = "1"
[dev-dependencies]
anyhow = { version = "1", features = ["backtrace"] }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
mod contract;
pub mod msg;
pub mod state;
pub use contract::*;

View File

@ -0,0 +1,10 @@
use cosmwasm_schema::cw_serde;
use crate::state::{Account, Modification, Transfer};
#[cw_serde]
pub struct Instantiate {
pub accounts: Vec<Account>,
pub transfers: Vec<Transfer>,
pub modifications: Vec<Modification>,
}

View File

@ -0,0 +1,55 @@
use std::fmt;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Uint256;
use cw_storage_plus::Map;
pub mod account;
mod addr;
pub mod transfer;
pub use account::Account;
pub use addr::TokenAddress;
pub use transfer::Transfer;
pub const ACCOUNTS: Map<account::Key, account::Balance> = Map::new("accounting/accounts");
pub const TRANSFERS: Map<transfer::Key, transfer::Data> = Map::new("accounting/transfers");
#[cw_serde]
#[derive(Eq, PartialOrd, Ord)]
pub enum Kind {
Add,
Sub,
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Kind::Add => f.write_str("ADD"),
Kind::Sub => f.write_str("SUB"),
}
}
}
#[cw_serde]
#[derive(Eq, PartialOrd, Ord)]
pub struct Modification {
// The sequence number of this modification. Each modification must be
// uniquely identifiable just by its sequnce number.
pub sequence: u64,
// The chain id of the account to be modified.
pub chain_id: u16,
// The chain id of the native chain for the token.
pub token_chain: u16,
// The address of the token on its native chain.
pub token_address: TokenAddress,
// The kind of modification to be made.
pub kind: Kind,
// The amount to be modified.
pub amount: Uint256,
// A human-readable reason for the modification.
pub reason: String,
}
pub const MODIFICATIONS: Map<u64, Modification> = Map::new("accounting/modifications");

View File

@ -0,0 +1,324 @@
use std::ops::{Deref, DerefMut};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{StdResult, Uint256};
use cw_storage_plus::{Key as CwKey, KeyDeserialize, PrimaryKey};
use crate::state::TokenAddress;
#[cw_serde]
pub struct Account {
pub key: Key,
// The current balance of the account.
pub balance: Balance,
}
impl Account {
pub fn lock_or_burn(&mut self, amt: Uint256) -> StdResult<()> {
if self.key.chain_id == self.key.token_chain {
self.balance.0 = self.balance.checked_add(amt)?;
} else {
self.balance.0 = self.balance.checked_sub(amt)?;
}
Ok(())
}
pub fn unlock_or_mint(&mut self, amt: Uint256) -> StdResult<()> {
if self.key.chain_id == self.key.token_chain {
self.balance.0 = self.balance.checked_sub(amt)?;
} else {
self.balance.0 = self.balance.checked_add(amt)?;
}
Ok(())
}
}
#[cw_serde]
#[derive(Eq, PartialOrd, Ord)]
pub struct Key {
// The chain id of the chain to which this account belongs.
chain_id: u16,
// The chain id of the native chain for the token associated with this account.
token_chain: u16,
// The address of the token associated with this account on its native chain.
token_address: TokenAddress,
}
impl Key {
pub fn new(chain_id: u16, token_chain: u16, token_address: TokenAddress) -> Self {
Self {
chain_id,
token_chain,
token_address,
}
}
pub fn chain_id(&self) -> u16 {
self.chain_id
}
pub fn token_chain(&self) -> u16 {
self.token_chain
}
pub fn token_address(&self) -> &TokenAddress {
&self.token_address
}
}
impl KeyDeserialize for Key {
type Output = Self;
fn from_vec(v: Vec<u8>) -> StdResult<Self::Output> {
<(u16, u16, TokenAddress)>::from_vec(v).map(|(chain_id, token_chain, token_address)| Key {
chain_id,
token_chain,
token_address,
})
}
}
impl<'a> PrimaryKey<'a> for Key {
type Prefix = (u16, u16);
type SubPrefix = u16;
type Suffix = TokenAddress;
type SuperSuffix = (u16, TokenAddress);
fn key(&self) -> Vec<CwKey> {
self.chain_id
.key()
.into_iter()
.chain(self.token_chain.key())
.chain(self.token_address.key())
.collect()
}
}
#[cw_serde]
pub struct Balance(Uint256);
impl Balance {
pub const fn new(v: Uint256) -> Balance {
Balance(v)
}
pub const fn zero() -> Balance {
Balance(Uint256::zero())
}
}
impl Deref for Balance {
type Target = Uint256;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Balance {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl AsRef<Uint256> for Balance {
fn as_ref(&self) -> &Uint256 {
&self.0
}
}
impl AsMut<Uint256> for Balance {
fn as_mut(&mut self) -> &mut Uint256 {
&mut self.0
}
}
impl From<Uint256> for Balance {
fn from(v: Uint256) -> Self {
Balance(v)
}
}
impl From<Balance> for Uint256 {
fn from(b: Balance) -> Self {
b.0
}
}
#[cfg(test)]
mod test {
use cosmwasm_std::StdError;
use super::*;
#[test]
fn native_lock() {
let mut acc = Account {
key: Key {
chain_id: 0xbae2,
token_chain: 0xbae2,
token_address: TokenAddress::new([
0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90,
0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63,
0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61,
]),
},
balance: Balance(500u128.into()),
};
acc.lock_or_burn(200u128.into()).unwrap();
assert_eq!(acc.balance.0, Uint256::from(700u128));
}
#[test]
fn native_lock_overflow() {
let mut acc = Account {
key: Key {
chain_id: 0xbae2,
token_chain: 0xbae2,
token_address: TokenAddress::new([
0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90,
0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63,
0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61,
]),
},
balance: Balance(Uint256::MAX),
};
let e = acc.lock_or_burn(200u128.into()).unwrap_err();
assert!(matches!(e, StdError::Overflow { .. }))
}
#[test]
fn native_unlock() {
let mut acc = Account {
key: Key {
chain_id: 0xbae2,
token_chain: 0xbae2,
token_address: TokenAddress::new([
0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90,
0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63,
0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61,
]),
},
balance: Balance(500u128.into()),
};
acc.unlock_or_mint(200u128.into()).unwrap();
assert_eq!(acc.balance.0, Uint256::from(300u128));
}
#[test]
fn native_unlock_underflow() {
let mut acc = Account {
key: Key {
chain_id: 0xbae2,
token_chain: 0xbae2,
token_address: TokenAddress::new([
0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90,
0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63,
0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61,
]),
},
balance: Balance(Uint256::zero()),
};
let e = acc.unlock_or_mint(200u128.into()).unwrap_err();
assert!(matches!(e, StdError::Overflow { .. }))
}
#[test]
fn wrapped_burn() {
let mut acc = Account {
key: Key {
chain_id: 0xcae8,
token_chain: 0xbae2,
token_address: TokenAddress::new([
0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90,
0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63,
0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61,
]),
},
balance: Balance(500u128.into()),
};
acc.lock_or_burn(200u128.into()).unwrap();
assert_eq!(acc.balance.0, Uint256::from(300u128));
}
#[test]
fn wrapped_burn_underflow() {
let mut acc = Account {
key: Key {
chain_id: 0xcae8,
token_chain: 0xbae2,
token_address: TokenAddress::new([
0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90,
0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63,
0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61,
]),
},
balance: Balance(Uint256::zero()),
};
let e = acc.lock_or_burn(200u128.into()).unwrap_err();
assert!(matches!(e, StdError::Overflow { .. }))
}
#[test]
fn wrapped_mint() {
let mut acc = Account {
key: Key {
chain_id: 0xcae8,
token_chain: 0xbae2,
token_address: TokenAddress::new([
0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90,
0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63,
0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61,
]),
},
balance: Balance(500u128.into()),
};
acc.unlock_or_mint(200u128.into()).unwrap();
assert_eq!(acc.balance.0, Uint256::from(700u128));
}
#[test]
fn wrapped_mint_overflow() {
let mut acc = Account {
key: Key {
chain_id: 0xcae8,
token_chain: 0xbae2,
token_address: TokenAddress::new([
0x62, 0x4e, 0x8d, 0xc6, 0xe0, 0xfe, 0x16, 0xe2, 0x59, 0x6e, 0xcf, 0x9f, 0x90,
0x0e, 0xd9, 0x5f, 0x4e, 0x6d, 0x26, 0xea, 0xf1, 0x9e, 0xe3, 0xe2, 0x88, 0x63,
0x60, 0xff, 0xc4, 0x1b, 0xfb, 0x61,
]),
},
balance: Balance(Uint256::MAX),
};
let e = acc.unlock_or_mint(200u128.into()).unwrap_err();
assert!(matches!(e, StdError::Overflow { .. }))
}
}

View File

@ -0,0 +1,165 @@
use std::{
fmt,
ops::{Deref, DerefMut},
str::FromStr,
};
use anyhow::{anyhow, Context};
use cosmwasm_std::{StdError, StdResult};
use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
use schemars::JsonSchema;
use serde::{de, Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
#[repr(transparent)]
pub struct TokenAddress(#[schemars(with = "String")] [u8; 32]);
impl TokenAddress {
pub const fn new(addr: [u8; 32]) -> TokenAddress {
TokenAddress(addr)
}
}
impl From<[u8; 32]> for TokenAddress {
fn from(addr: [u8; 32]) -> Self {
TokenAddress(addr)
}
}
impl From<TokenAddress> for [u8; 32] {
fn from(addr: TokenAddress) -> Self {
addr.0
}
}
impl TryFrom<Vec<u8>> for TokenAddress {
type Error = anyhow::Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
<[u8; 32]>::try_from(value)
.map(Self)
.map_err(|v: Vec<u8>| anyhow!("invalid length; want 32, got {}", v.len()))
}
}
impl Deref for TokenAddress {
type Target = [u8; 32];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for TokenAddress {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl AsRef<[u8; 32]> for TokenAddress {
fn as_ref(&self) -> &[u8; 32] {
&self.0
}
}
impl AsMut<[u8; 32]> for TokenAddress {
fn as_mut(&mut self) -> &mut [u8; 32] {
&mut self.0
}
}
impl AsRef<[u8]> for TokenAddress {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl AsMut<[u8]> for TokenAddress {
fn as_mut(&mut self) -> &mut [u8] {
&mut self.0
}
}
impl fmt::Display for TokenAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&hex::encode(self))
}
}
impl FromStr for TokenAddress {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
hex::decode(s)
.context("failed to decode hex")
.and_then(Self::try_from)
}
}
impl Serialize for TokenAddress {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&base64::encode(self.0))
}
}
impl<'de> Deserialize<'de> for TokenAddress {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(Base64Visitor)
}
}
struct Base64Visitor;
impl<'de> de::Visitor<'de> for Base64Visitor {
type Value = TokenAddress;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a valid base64 encoded string of a 32-byte array")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
base64::decode(v)
.map_err(|_| E::invalid_value(de::Unexpected::Str(v), &self))
.and_then(|b| {
b.try_into()
.map_err(|b: Vec<u8>| E::invalid_length(b.len(), &self))
})
.map(TokenAddress)
}
}
impl KeyDeserialize for TokenAddress {
type Output = Self;
fn from_vec(v: Vec<u8>) -> StdResult<Self::Output> {
v.try_into()
.map(TokenAddress)
.map_err(|v| StdError::InvalidDataSize {
expected: 32,
actual: v.len() as u64,
})
}
}
impl<'a> PrimaryKey<'a> for TokenAddress {
type Prefix = ();
type SubPrefix = ();
type Suffix = Self;
type SuperSuffix = Self;
fn key(&self) -> Vec<Key> {
vec![Key::Ref(&**self)]
}
}
impl<'a> Prefixer<'a> for TokenAddress {
fn prefix(&self) -> Vec<Key> {
vec![Key::Ref(&**self)]
}
}

View File

@ -0,0 +1,157 @@
use std::{fmt, str::FromStr};
use anyhow::{ensure, Context};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{StdResult, Uint256};
use cw_storage_plus::{Key as CwKey, KeyDeserialize, PrimaryKey};
use crate::state::TokenAddress;
#[cw_serde]
pub struct Transfer {
pub key: Key,
pub data: Data,
}
#[cw_serde]
#[derive(Eq, PartialOrd, Ord, Default, Hash)]
pub struct Key {
// The chain id of the chain on which this transfer originated.
emitter_chain: u16,
// The address on the emitter chain that created this transfer.
emitter_address: TokenAddress,
// The sequence number of the transfer.
sequence: u64,
}
impl Key {
pub fn new(emitter_chain: u16, emitter_address: TokenAddress, sequence: u64) -> Self {
Self {
emitter_chain,
emitter_address,
sequence,
}
}
pub fn emitter_chain(&self) -> u16 {
self.emitter_chain
}
pub fn emitter_address(&self) -> &TokenAddress {
&self.emitter_address
}
pub fn sequence(&self) -> u64 {
self.sequence
}
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:05}/{}/{:016}",
self.emitter_chain, self.emitter_address, self.sequence
)
}
}
impl FromStr for Key {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut components = s.split('/');
let emitter_chain = components
.next()
.map(str::parse)
.transpose()
.context("failed to parse emitter chain")?
.context("emitter chain missing")?;
let emitter_address = components
.next()
.map(str::parse)
.transpose()
.context("failed to parse emitter address")?
.context("emitter address missing")?;
let sequence = components
.next()
.map(str::parse)
.transpose()
.context("failed to parse sequence")?
.context("sequence missing")?;
ensure!(
components.next().is_none(),
"unexpected trailing input data"
);
Ok(Key {
emitter_chain,
emitter_address,
sequence,
})
}
}
impl KeyDeserialize for Key {
type Output = Self;
fn from_vec(v: Vec<u8>) -> StdResult<Self::Output> {
<(u16, TokenAddress, u64)>::from_vec(v).map(|(emitter_chain, emitter_address, sequence)| {
Key {
emitter_chain,
emitter_address,
sequence,
}
})
}
}
impl<'a> PrimaryKey<'a> for Key {
type Prefix = (u16, TokenAddress);
type SubPrefix = u16;
type Suffix = u64;
type SuperSuffix = (TokenAddress, u64);
fn key(&self) -> Vec<CwKey> {
self.emitter_chain
.key()
.into_iter()
.chain(self.emitter_address.key())
.chain(self.sequence.key())
.collect()
}
}
#[cw_serde]
pub struct Data {
// The amount to be transferred.
pub amount: Uint256,
// The id of the native chain of the token.
pub token_chain: u16,
// The address of the token on its native chain.
pub token_address: TokenAddress,
// The chain id where the tokens are being sent.
pub recipient_chain: u16,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn key_display() {
let addr = TokenAddress::new([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 225, 139, 34, 20, 175, 249, 112, 0, 217, 116,
207, 100, 126, 124, 52, 126, 143, 165, 133,
]);
let k = Key::new(2, addr, 254278);
let expected = "00002/0000000000000000000000003ee18b2214aff97000d974cf647e7c347e8fa585/0000000000254278";
assert_eq!(expected, k.to_string());
}
}