This document describes the motivation, goals, and technical design of the "Global Accountant".
The Global Accountant is a safety feature for registered token bridges and Native Token Transfer (NTT) deployments. It is implemented as an Integrity Checker on Wormchain.
Tightly limit the impact an exploit in a connected chain can have on registered token bridges and NTT deployments. Ensure the number of wrapped tokens capable of being moved out of a chain never exceeds the number sent to that chain, even if that chain was compromised.
Token Bridges built on Wormhole (e.g. Portal) allow transferring tokens between any of the various blockchains connected to the bridge.
A core feature of these systems is that xAssets are fungible across all chains. ETH sent from Ethereum to Solana to Polygon is the same as the ETH sent from Ethereum to Aptos to Polygon. Transfers work by locking native assets (or burning wrapped assets) on the source chain and emitting a Wormhole message that allows the minting of wrapped assets (or unlocking of native assets) on the destination chain.
It is unfortunately not practical to synchronize the state of token bridges and NTT protocols across all chains: If a transfer of Portal-wrapped ETH happens from Solana to Polygon, the token bridge contract on Ethereum is not aware of that transfer and therefore doesn't know where the wrapped assets are currently located.
If any connected chain gets compromised, e.g. through a bug in the RPC nodes, this could cause arbitrary minting of "unbacked" wrapped assets, i.e. wrapped assets that are not backed by native assets. Those unbacked wrapped assets could then be transferred to the native chain. Because wrapped assets are fungible, the smart contract on the native chain cannot distinguish those unbacked wrapped assets and would proceed with unlocking the native assets, effectively draining the bridge.
The Governor limits the maximal impact of such an attack on the token bridges, but cannot fully prevent it.
The core of the issue is the differing trust assumptions and security program maturity of various blockchains. By design, Token Bridges are subject to the weakest link. A holder of a wrapped asset should only be subject to security risks from the asset's native chain as well as the chain where the wrapped asset lives. They should not be subject to security risks from other chains connected to the bridge.
Native Token Transfer (NTT) deployments allow certain assets to be transferred across a subset of chains, according to the deployment details. Additional details can be found in that project's README
The Global Accountant consists of two parallel accounting systems:
Both systems act as Integrity Checkers on wormchain. Guardians submit pre-observations to the appropriate accountant and only finalize their observations if the accountant gives the go-ahead.
Each accountant keeps track of the tokens locked and minted on each connected blockchain for their respective message type.
Once a transfer message has reached a quorum of pre-observations (i.e. 13 guardians have submitted a pre-observation), the appropriate accountant attempts to apply the transfer to the account balances for each chain. If the transfer doesn't result in a negative account balance, it is allowed to proceed.
The Global Accountant supports both Token Bridge and NTT protocols through separate but parallel accounting systems:
accountantContractaccountantKeyPath and accountantKeyPassPhrase for authenticationaccountantNttContractaccountantNttKeyPath and accountantNttKeyPassPhrase for authenticationThe accountant includes message detection logic to route transfers to the appropriate accounting system:
Both accountants are cosmwasm-based smart contracts rather than built-in native cosmos-sdk modules. They expose query methods from the native wormhole module to cosmwasm. The x/wormhole native module exposes the following query methods:
#[cw_serde]
#[derive(QueryResponses)]
pub enum WormholeQuery {
/// Verifies that `vaa` has been signed by a quorum of guardians from valid guardian set.
#[returns(Empty)]
VerifyVaa {
vaa: Binary,
},
/// Verifies that `data` has been signed by a guardian from `guardian_set_index` using a correct `prefix`.
#[returns(Empty)]
VerifyMessageSignature {
prefix: Binary,
data: Binary,
guardian_set_index: u32,
signature: Signature,
},
/// Returns the number of signatures necessary for quorum for the given guardian set index.
#[returns(u32)]
CalculateQuorum { guardian_set_index: u32 },
}
When a guardian observes a transfer message, it first determines whether it's a Token Bridge or NTT transfer, then submits the observation to the appropriate accountant:
Each accountant maintains separate:
baseWorker / nttWorker)baseWatcher / nttWatcher)The contract processing logic is the same for both accountants:
Pending status and mark the transfer as having a signature from the guardian.(emitter_chain, emitter_address, sequence) tuple has already been committed, then check that the digest of the transfer matches the digest of the committed transfer. If they match return a Committed status; otherwise return an Error status and emit an Error event.Error status and emit an Error event. Otherwise return a Committed status and emit a Committed event.If the guardian receives a Committed status for a transfer then it can immediately proceed to signing the VAA. If it receives an Error status, then it should halt further processing of the transfer. If it receives a Pending status, then it should add the observation to a local database of pending transfers and wait for an event from the accountant.
Each guardian sets up watchers for both accountants, watching for events emitted signalling determinations on transfers. Events can come from either the Base Accountant or NTT Accountant depending on the transfer type.
There are 2 types of events that the guardian must handle from each accountant:
It may be possible that some transfers don't receive enough observations to reach quorum. This can happen if a number of guardians miss the transaction on the origin chain. Since the accountant runs before the peer to peer gossip layer, it cannot take advantage of the existing automatic retry mechanisms that the guardian network uses. To deal with this, both accountant contracts provide a MissingObservations query that returns a list of pending transfers for which the accountant does not have an observation from a particular guardian.
Guardians are expected to periodically run this query against both accountants and re-observe transactions for which the accountants are missing observations from them.
The dual accountant system supports flexible deployment configurations:
accountantContract configured, accountantNttContract empty)accountantNttContract configured, accountantContract empty)This allows for gradual rollout and protocol-specific deployments.
Upgrading either contract is performed with a migrate contract governance action from the guardian network. Once quorum+1 guardians (13) have signed the migrate contract governance VAA, the wormchain client migrate command can be ran by anyone with an authorized wallet to submit the valid governance vaa and updated wasm contract to wormchain.
Both the Base and NTT Accountants use the same account management principles. Each accountant keeps track of token balances for each blockchain ensuring they never become negative. When committing a transfer, the accountant parses the respective payload format (Token Bridge or NTT) to get the emitter chain, recipient chain, token chain, and token address. These values are used to look up the relevant accounts and adjust the balances as needed.
Both accountants maintain the following accounts:
Transferring a native token across either protocol (locking) increases the balance on both the custody account and wrapped account on the destination chain. Transferring a wrapped token back across either protocol (burning) and subsequently unlocking its native asset decreases the balance on both the wrapped and custody accounts. Any transfer causing the balance of an account to become negative is invalid and gets rejected.
Each account is identified via a (chain_id, token_chain, token_address) tuple. Custody accounts are those where chain_id == token_chain, while the rest are wrapped accounts. See the governance section below for further clarification.
Account balances recorded by either accountant may end up inaccurate due to bugs in the program or exploits of the respective protocols. In these cases, the guardians may issue governance actions to manually modify the balances of the accounts in either the Base or NTT Accountant. These governance actions must be signed by a quorum of guardians.
Example 1: A user sends SOL from Solana to Ethereum. In this transaction the (Solana, Solana, SOL) account is debited and the (Ethereum, Solana, SOL) account is credited, increasing the balances of both accounts.
Example 2: A user sends wrapped SOL from Ethereum to Solana. In this transaction the (Ethereum, Solana, SOL) account is debited and the (Solana, Solana, SOL) account is credited, decreasing the balances of both accounts.
Example 3: An attacker exploits a bug in the Solana contract to print fake wrapped ETH and transfers it over the bridge. This could drain real ETH from the Ethereum contract. Thanks to the accounting contract, the total amount drained is limited by the total number of wrapped ETH issued by the solana token bridge contract. Since the token bridge is still liable for all the legitimately issued wrapped ETH the account balances no longer reflect reality.
Wormhole decides to cover the cost of the hack and transfers the ETH equivalent to the stolen amount to the token bridge contract on Ethereum. To update the accounts, the guardians must issue 2 governance actions: one to increase the balance of the (Ethereum, Ethereum, ETH) custody account and one to increase the balance of the (Solana, Ethereum, ETH) wrapped account.
Example 4: A user sends wrapped SOL from Ethereum to Polygon. Even though SOL is not native to either chain, the transaction is still recorded the same way: the (Ethereum, Solana, SOL) wrapped account is debited and the (Polygon, Solana, SOL) wrapped account is credited. The total liability of the token bridge has not changed and instead we have just moved some liability from Ethereum to Polygon.
Since users can send any arbitrary token over the token bridge, the accountant should automatically create accounts for any new token. To determine whether the newly created account should be a custody account or a wrapped account, these steps are followed:
emitter_chain == token_chain and a custody account doesn't already exist, create one.emitter_chain != token_chain and a wrapped account doesn't already exist, reject the transaction. The wrapped account should have been created when the tokens were originally transferred to emitter_chain.token_chain != recipient_chain and a wrapped account doesn't already exist, create one.token_chain == recipient_chain and a custody account doesn't already exist, reject the transaction. The custody account should have been created when the tokens were first transferred from recipient_chain.Both accountants support per-emitter enforcement configuration:
This allows for:
In order for each accountant to have accurate values for the balances of the various accounts, it needs to replay all transactions since the initial launch of the respective protocol. Similarly, it may be necessary to backfill missing transfers if either accountant is disabled for any period of time.
To handle both issues, each accountant has the ability to directly process transfer VAAs for their respective protocols. As long as the VAAs are signed by a quorum of guardians from the latest guardian set, the accountant uses them to update its on-chain state of token balances.
Submitting VAAs does not disable any core checks: a VAA causing the balance of an account to become negative will still be rejected even if it has a quorum of signatures. This can be used to catch problematic transfers after the fact.
Legitimate transactions should never be rejected so a rejection implies either a bug in the accountant or an actual attack on the bridge. In both cases manual intervention is required.
Whenever a guardian observes an error (either via an observation response or via an error event), it will increase a Prometheus error counter. Guardians will be encouraged to set up alerts whenever these metrics change so that they can be notified when an error occurs from either accountant.
Since both Global Accountants are implemented as Integrity Checkers, they inherit the threat model of Integrity Checkers. Therefore, they are only able to block messages and not create or modify messages.
The dual accountant architecture provides protocol isolation benefits:
Once transfers are gated on approval from the respective accountants, the uptime of the guardian network for those transfers becomes dependent on the uptime of wormchain itself. Any downtime for wormchain would halt transfers until the network is back online (or 2/3+ of the guardians choose to disable the relevant accountant).
In practice, cosmos chains are stable and wormchain is not particularly complex. However, once the accountants are live, attacking wormchain could shut down transfers for both protocols.
Due to the nature of the accountants, a guardian that chooses to enable them in enforcing-mode will refuse to sign a transfer until it sees that the transfer is committed by the appropriate accountant. If a super-minority of guardians choose to not enable an accountant at all (i.e. stop sending their observations to the contract) then we will end up with a stalemate: the guardians with the feature disabled will be unable to reach quorum because they don't have enough signatures while the guardians in enforcing-mode will never observe the transfer being committed by the accountant (and so will never sign the observation). This is very different from the governor feature, where only a super-minority of guardians have to enable it to be effective.
Note: this applies only to the enforcing mode. If the Guardians enable logging mode on the accountant(s) instead, this issue will not occur.
The dual accountant system introduces configuration complexity: