cosmwasm: accounting: Add support for chain registration

Add support for handling chain registration VAAs for the tokenbridge
contract.  This will let us deploy accounting without also having to
deploy the tokenbridge.
This commit is contained in:
Chirantan Ekbote 2022-12-15 19:24:26 +09:00 committed by Chirantan Ekbote
parent 0dbeeec3d4
commit 9a559f3fbd
9 changed files with 405 additions and 47 deletions

View File

@ -166,7 +166,7 @@
"additionalProperties": false
},
{
"description": "Submit one or more signed token transfer VAAs to update the on-chain state. If committing any of the transfers returns an error, the entire transaction is aborted and none of the transfers are committed.",
"description": "Submit one or more signed VAAs to update the on-chain state. If processing any of the VAAs returns an error, the entire transaction is aborted and none of the VAAs are committed.",
"type": "object",
"required": [
"submit_v_a_as"
@ -437,6 +437,29 @@
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"chain_registration"
],
"properties": {
"chain_registration": {
"type": "object",
"required": [
"chain"
],
"properties": {
"chain": {
"type": "integer",
"format": "uint16",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
@ -918,6 +941,26 @@
}
}
},
"chain_registration": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ChainRegistrationResponse",
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"$ref": "#/definitions/Binary"
}
},
"additionalProperties": false,
"definitions": {
"Binary": {
"description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>",
"type": "string"
}
}
},
"modification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Modification",

View File

@ -15,10 +15,10 @@ use cosmwasm_std::{
use cw2::set_contract_version;
use cw_storage_plus::Bound;
use tinyvec::{Array, TinyVec};
use tokenbridge::msg::ChainRegistrationResponse;
use wormhole::{
token::Message,
token::{Action, GovernancePacket, Message},
vaa::{Body, Header, Signature},
Chain,
};
use wormhole_bindings::WormholeQuery;
@ -27,10 +27,13 @@ use crate::{
error::{AnyError, ContractError},
msg::{
AllAccountsResponse, AllModificationsResponse, AllPendingTransfersResponse,
AllTransfersResponse, ExecuteMsg, Instantiate, InstantiateMsg, MigrateMsg, Observation,
QueryMsg, Upgrade,
AllTransfersResponse, ChainRegistrationResponse, ExecuteMsg, Instantiate, InstantiateMsg,
MigrateMsg, Observation, QueryMsg, Upgrade,
},
state::{
self, Data, PendingTransfer, CHAIN_REGISTRATIONS, GOVERNANCE_VAAS, PENDING_TRANSFERS,
TOKENBRIDGE_ADDR,
},
state::{self, Data, PendingTransfer, PENDING_TRANSFERS, TOKENBRIDGE_ADDR},
};
// version info for migration info
@ -221,21 +224,10 @@ fn handle_observation(
let emitter_chain = o.key.emitter_chain();
let tokenbridge_addr = TOKENBRIDGE_ADDR
.load(deps.storage)
.context("failed to load tokenbridge addr")?;
let ChainRegistrationResponse {
address: registered_emitter,
} = deps
.querier
.query_wasm_smart(
tokenbridge_addr,
&tokenbridge::msg::QueryMsg::ChainRegistration {
chain: emitter_chain,
},
)
.context("failed to query chain registration")?;
let registered_emitter = CHAIN_REGISTRATIONS
.may_load(deps.storage, emitter_chain)
.context("failed to load chain registration")?
.ok_or_else(|| ContractError::MissingChainRegistration(emitter_chain.into()))?;
ensure!(
*registered_emitter == **o.key.emitter_address(),
"unknown emitter address"
@ -343,7 +335,7 @@ fn submit_vaas(
.add_events(evts))
}
fn handle_vaa(mut deps: DepsMut<WormholeQuery>, vaa: Binary) -> anyhow::Result<Event> {
fn handle_vaa(deps: DepsMut<WormholeQuery>, vaa: Binary) -> anyhow::Result<Event> {
let (header, data) = serde_wormhole::from_slice_with_payload::<Header>(&vaa)
.context("failed to parse VAA header")?;
@ -359,10 +351,67 @@ fn handle_vaa(mut deps: DepsMut<WormholeQuery>, vaa: Binary) -> anyhow::Result<E
.into(),
)
.context(ContractError::VerifyQuorum)?;
let (body, _) = serde_wormhole::from_slice_with_payload::<Body<Message>>(data)
let (body, payload) = serde_wormhole::from_slice_with_payload::<Body<()>>(data)
.context("failed to parse VAA body")?;
if body.emitter_chain == Chain::Solana && body.emitter_address == wormhole::GOVERNANCE_EMITTER {
let govpacket =
serde_wormhole::from_slice(payload).context("failed to parse governance packet")?;
handle_governance_vaa(deps, body.with_payload(govpacket))
} else {
let (msg, _) = serde_wormhole::from_slice_with_payload(payload)
.context("failed to parse tokenbridge message")?;
handle_tokenbridge_vaa(deps, body.with_payload(msg))
}
}
fn handle_governance_vaa(
deps: DepsMut<WormholeQuery>,
body: Body<GovernancePacket>,
) -> anyhow::Result<Event> {
ensure!(
body.payload.chain == Chain::Any || body.payload.chain == Chain::Wormchain,
"this governance VAA is for another chain"
);
let digest = body
.digest()
.context("failed to calculate digest for governance VAA body")?;
let key = GOVERNANCE_VAAS.key(digest.secp256k_hash.to_vec());
if key.has(deps.storage) {
bail!(ContractError::DuplicateGovernanceVaa);
}
let evt = match body.payload.action {
Action::RegisterChain {
chain,
emitter_address,
} => {
CHAIN_REGISTRATIONS
.save(
deps.storage,
chain.into(),
&emitter_address.0.to_vec().into(),
)
.context("failed to save chain registration")?;
Event::new("RegisterChain")
.add_attribute("chain", chain.to_string())
.add_attribute("emitter_address", emitter_address.to_string())
}
_ => bail!("unsupported governance action"),
};
key.save(deps.storage, &())
.context("failed to save governance VAA digest")?;
Ok(evt)
}
fn handle_tokenbridge_vaa(
mut deps: DepsMut<WormholeQuery>,
body: Body<Message>,
) -> anyhow::Result<Event> {
let data = match body.payload {
Message::Transfer {
amount,
@ -434,6 +483,9 @@ pub fn query(deps: Deps<WormholeQuery>, _env: Env, msg: QueryMsg) -> StdResult<B
})
})
.and_then(|()| to_binary(&Empty {})),
QueryMsg::ChainRegistration { chain } => {
query_chain_registration(deps, chain).and_then(|resp| to_binary(&resp))
}
}
}
@ -542,3 +594,12 @@ fn query_all_modifications(
.map(|modifications| AllModificationsResponse { modifications })
}
}
fn query_chain_registration(
deps: Deps<WormholeQuery>,
chain: u16,
) -> StdResult<ChainRegistrationResponse> {
CHAIN_REGISTRATIONS
.load(deps.storage, chain)
.map(|address| ChainRegistrationResponse { address })
}

View File

@ -3,11 +3,16 @@ use std::ops::{Deref, DerefMut};
use anyhow::anyhow;
use cosmwasm_std::StdError;
use thiserror::Error;
use wormhole::Chain;
#[derive(Error, Debug)]
pub enum ContractError {
#[error("failed to verify quorum")]
VerifyQuorum,
#[error("no registered emitter for chain {0}")]
MissingChainRegistration(Chain),
#[error("governance VAA already executed")]
DuplicateGovernanceVaa,
}
// This is a workaround for the fact that `cw_multi_test::ContractWrapper` doesn't support contract

View File

@ -93,9 +93,8 @@ pub enum ExecuteMsg {
signatures: Vec<Signature>,
},
/// Submit one or more signed token transfer VAAs to update the on-chain state. If committing
/// any of the transfers returns an error, the entire transaction is aborted and none of the
/// transfers are committed.
/// Submit one or more signed VAAs to update the on-chain state. If processing any of the VAAs
/// returns an error, the entire transaction is aborted and none of the VAAs are committed.
SubmitVAAs {
/// One or more VAAs to be submitted. Each VAA should be encoded in the standard wormhole
/// wire format.
@ -139,6 +138,8 @@ pub enum QueryMsg {
},
#[returns(cosmwasm_std::Empty)]
ValidateTransfer { transfer: Transfer },
#[returns(ChainRegistrationResponse)]
ChainRegistration { chain: u16 },
}
#[cw_serde]
@ -160,3 +161,8 @@ pub struct AllPendingTransfersResponse {
pub struct AllModificationsResponse {
pub modifications: Vec<Modification>,
}
#[cw_serde]
pub struct ChainRegistrationResponse {
pub address: Binary,
}

View File

@ -1,6 +1,6 @@
use accounting::state::transfer;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Addr;
use cosmwasm_std::{Addr, Binary};
use cw_storage_plus::{Item, Map};
use thiserror::Error;
use tinyvec::TinyVec;
@ -10,6 +10,8 @@ use crate::msg::Observation;
pub const TOKENBRIDGE_ADDR: Item<Addr> = Item::new("tokenbride_addr");
pub const PENDING_TRANSFERS: Map<transfer::Key, TinyVec<[Data; 2]>> = Map::new("pending_transfers");
pub const CHAIN_REGISTRATIONS: Map<u16, Binary> = Map::new("chain_registrations");
pub const GOVERNANCE_VAAS: Map<Vec<u8>, ()> = Map::new("governance_vaas");
#[cw_serde]
pub struct PendingTransfer {

View File

@ -0,0 +1,195 @@
mod helpers;
use cosmwasm_std::{to_binary, Event};
use helpers::*;
use wormchain_accounting::msg::ChainRegistrationResponse;
use wormhole::{
token::{Action, GovernancePacket},
vaa::Body,
Address, Chain,
};
fn create_vaa_body() -> Body<GovernancePacket> {
Body {
timestamp: 1,
nonce: 1,
emitter_chain: Chain::Solana,
emitter_address: wormhole::GOVERNANCE_EMITTER,
sequence: 15920283,
consistency_level: 0,
payload: GovernancePacket {
chain: Chain::Any,
action: Action::RegisterChain {
chain: Chain::Solana,
emitter_address: Address([
0xc6, 0x9a, 0x1b, 0x1a, 0x65, 0xdd, 0x33, 0x6b, 0xf1, 0xdf, 0x6a, 0x77, 0xaf,
0xb5, 0x01, 0xfc, 0x25, 0xdb, 0x7f, 0xc0, 0x93, 0x8c, 0xb0, 0x85, 0x95, 0xa9,
0xef, 0x47, 0x32, 0x65, 0xcb, 0x4f,
]),
},
},
}
}
#[test]
fn any_target() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let body = create_vaa_body();
let (v, data) = sign_vaa_body(&wh, body);
let resp = contract
.submit_vaas(vec![data])
.expect("failed to submit chain registration");
let Action::RegisterChain { chain, emitter_address } = v.payload.action else { panic!() };
resp.assert_event(
&Event::new("wasm-RegisterChain")
.add_attribute("chain", chain.to_string())
.add_attribute("emitter_address", emitter_address.to_string()),
);
let ChainRegistrationResponse { address } =
contract.query_chain_registration(chain.into()).unwrap();
assert_eq!(&*address, &emitter_address.0);
}
#[test]
fn wormchain_target() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let mut body = create_vaa_body();
body.payload.chain = Chain::Wormchain;
let (v, data) = sign_vaa_body(&wh, body);
let resp = contract
.submit_vaas(vec![data])
.expect("failed to submit chain registration");
let Action::RegisterChain { chain, emitter_address } = v.payload.action else { panic!() };
resp.assert_event(
&Event::new("wasm-RegisterChain")
.add_attribute("chain", chain.to_string())
.add_attribute("emitter_address", emitter_address.to_string()),
);
let ChainRegistrationResponse { address } =
contract.query_chain_registration(chain.into()).unwrap();
assert_eq!(&*address, &emitter_address.0);
}
#[test]
fn wrong_target() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let mut body = create_vaa_body();
body.payload.chain = Chain::Oasis;
let (_, data) = sign_vaa_body(&wh, body);
contract
.submit_vaas(vec![data])
.expect_err("successfully executed chain registration VAA for different chain");
}
#[test]
fn non_governance_chain() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let mut body = create_vaa_body();
body.emitter_chain = Chain::Fantom;
let (_, data) = sign_vaa_body(&wh, body);
contract
.submit_vaas(vec![data])
.expect_err("successfully executed chain registration with non-governance chain");
}
#[test]
fn non_governance_emitter() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let mut body = create_vaa_body();
body.emitter_address = Address([0x88; 32]);
let (_, data) = sign_vaa_body(&wh, body);
contract
.submit_vaas(vec![data])
.expect_err("successfully executed chain registration with non-governance emitter");
}
#[test]
fn duplicate() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let (_, data) = sign_vaa_body(&wh, create_vaa_body());
contract
.submit_vaas(vec![data.clone()])
.expect("failed to submit chain registration");
contract
.submit_vaas(vec![data])
.expect_err("successfully submitted duplicate vaa");
}
#[test]
fn no_quorum() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
.unwrap() as usize;
let (mut v, _) = sign_vaa_body(&wh, create_vaa_body());
v.signatures.truncate(quorum - 1);
let data = serde_wormhole::to_vec(&v).map(From::from).unwrap();
contract
.submit_vaas(vec![data])
.expect_err("successfully executed chain registration without a quorum of signatures");
}
#[test]
fn bad_signature() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let (mut v, _) = sign_vaa_body(&wh, create_vaa_body());
// Flip a bit in the first signature so it becomes invalid.
v.signatures[0].signature[0] ^= 1;
let data = serde_wormhole::to_vec(&v).map(From::from).unwrap();
contract
.submit_vaas(vec![data])
.expect_err("successfully executed chain registration with bad signature");
}
#[test]
fn bad_serialization() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let (v, _) = sign_vaa_body(&wh, create_vaa_body());
// Rather than using the wormhole wire format use cosmwasm json.
let data = to_binary(&v).unwrap();
contract
.submit_vaas(vec![data])
.expect_err("successfully executed chain registration with bad serialization");
}
#[test]
fn non_chain_registration() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
let mut body = create_vaa_body();
body.payload.action = Action::ContractUpgrade {
new_contract: Address([0x2f; 32]),
};
let (_, data) = sign_vaa_body(&wh, body);
contract
.submit_vaas(vec![data])
.expect_err("successfully executed VAA with non-chain registration action");
}

View File

@ -8,14 +8,19 @@ use cosmwasm_std::{
use cw_multi_test::{
App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor, WasmKeeper,
};
use serde::Serialize;
use wormchain_accounting::{
msg::{
AllAccountsResponse, AllModificationsResponse, AllPendingTransfersResponse,
AllTransfersResponse, ExecuteMsg, Instantiate, InstantiateMsg, QueryMsg,
AllTransfersResponse, ChainRegistrationResponse, ExecuteMsg, Instantiate, InstantiateMsg,
QueryMsg,
},
state,
};
use wormhole::vaa::Signature;
use wormhole::{
vaa::{Body, Header, Signature},
Vaa,
};
use wormhole_bindings::{fake, WormholeQuery};
mod fake_tokenbridge;
@ -166,6 +171,12 @@ impl Contract {
&QueryMsg::AllModifications { start_after, limit },
)
}
pub fn query_chain_registration(&self, chain: u16) -> StdResult<ChainRegistrationResponse> {
self.app
.wrap()
.query_wasm_smart(self.addr(), &QueryMsg::ChainRegistration { chain })
}
}
const USER: &str = "USER";
@ -318,3 +329,19 @@ pub fn proper_instantiate(
(wh, Contract { addr, app })
}
pub fn sign_vaa_body<P: Serialize>(wh: &fake::WormholeKeeper, body: Body<P>) -> (Vaa<P>, Binary) {
let data = serde_wormhole::to_vec(&body).unwrap();
let signatures = wh.sign(&data);
let header = Header {
version: 1,
guardian_set_index: wh.guardian_set_index(),
signatures,
};
let v = (header, body).into();
let data = serde_wormhole::to_vec(&v).map(From::from).unwrap();
(v, data)
}

View File

@ -8,7 +8,11 @@ use cosmwasm_std::{to_binary, Binary, Event, Uint256};
use cw_multi_test::AppResponse;
use helpers::*;
use wormchain_accounting::msg::Observation;
use wormhole::{token::Message, Address, Amount};
use wormhole::{
token::{Action, GovernancePacket, Message},
vaa::Body,
Address, Amount, Chain,
};
use wormhole_bindings::fake;
fn set_up(count: usize) -> (Vec<Message>, Vec<Observation>) {
@ -37,12 +41,36 @@ fn set_up(count: usize) -> (Vec<Message>, Vec<Observation>) {
(txs, observations)
}
fn register_emitters(wh: &fake::WormholeKeeper, contract: &mut Contract, count: usize) {
for i in 0..count {
let body = Body {
timestamp: i as u32,
nonce: i as u32,
emitter_chain: Chain::Solana,
emitter_address: wormhole::GOVERNANCE_EMITTER,
sequence: i as u64,
consistency_level: 0,
payload: GovernancePacket {
chain: Chain::Any,
action: Action::RegisterChain {
chain: (i as u16).into(),
emitter_address: Address([i as u8; 32]),
},
},
};
let (_, data) = sign_vaa_body(wh, body);
contract.submit_vaas(vec![data]).unwrap();
}
}
#[test]
fn batch() {
const COUNT: usize = 5;
let (txs, observations) = set_up(COUNT);
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
register_emitters(&wh, &mut contract, COUNT);
let index = wh.guardian_set_index();
@ -132,6 +160,7 @@ fn duplicates() {
let (txs, observations) = set_up(COUNT);
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
register_emitters(&wh, &mut contract, COUNT);
let index = wh.guardian_set_index();
let obs = to_binary(&observations).unwrap();
@ -234,6 +263,7 @@ fn transfer_tokens(
#[test]
fn round_trip() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
register_emitters(&wh, &mut contract, 15);
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
@ -489,6 +519,7 @@ fn repeated() {
const ITERATIONS: usize = 10;
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
register_emitters(&wh, &mut contract, 3);
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
@ -553,6 +584,7 @@ fn wrapped_to_wrapped() {
Vec::new(),
Vec::new(),
);
register_emitters(&wh, &mut contract, 15);
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
@ -631,6 +663,7 @@ fn unknown_emitter() {
#[test]
fn different_observations() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
register_emitters(&wh, &mut contract, 3);
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)
@ -711,6 +744,8 @@ fn different_observations() {
#[test]
fn emit_event_with_quorum() {
let (wh, mut contract) = proper_instantiate(Vec::new(), Vec::new(), Vec::new());
register_emitters(&wh, &mut contract, 3);
let index = wh.guardian_set_index();
let quorum = wh
.calculate_quorum(index, contract.app().block_info().height)

View File

@ -42,22 +42,6 @@ fn create_vaa_body(i: usize) -> Body<Message> {
}
}
fn sign_vaa_body(wh: &WormholeKeeper, body: Body<Message>) -> (Vaa<Message>, Binary) {
let data = serde_wormhole::to_vec(&body).unwrap();
let signatures = wh.sign(&data);
let header = Header {
version: 1,
guardian_set_index: wh.guardian_set_index(),
signatures,
};
let v = (header, body).into();
let data = serde_wormhole::to_vec(&v).map(From::from).unwrap();
(v, data)
}
fn transfer_data_from_token_message(msg: Message) -> transfer::Data {
match msg {
Message::Transfer {