terra/doc: add some implementation notes (#3597)

This commit is contained in:
Csongor Kiss 2023-12-13 17:21:34 +00:00 committed by GitHub
parent 09d18801fd
commit bfd4ba40ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 129 additions and 0 deletions

View File

@ -63,6 +63,8 @@ pub enum TransferType<A> {
/// can (and should be) safely deleted after the upgrade happened successfully.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
// see [the token upgrades](../../../docs/token_upgrades.md) document for
// information on upgrading the wrapped token contract.
Ok(Response::default())
}
@ -681,6 +683,7 @@ fn handle_complete_transfer(
relayer_address: &HumanAddr,
) -> StdResult<Response> {
let transfer_info = TransferInfo::deserialize(data)?;
// see [the token id doc](../../../docs/token_id.md) for more info
if transfer_info.token_chain == CHAIN_ID && is_native_id(transfer_info.token_address.as_slice())
{
handle_complete_transfer_token_native(

8
terra/docs/README.md Normal file
View File

@ -0,0 +1,8 @@
# Terra (classic) smart contract implementation notes
This folder contains documents that describe various implementation aspects of the Terra smart contracts. Where appropriate, we also discuss historical context and the rationale behind certain design decisions.
- [Token ID encoding in the Token bridge](token_id.md) describes the token ID scheme used by the Token Bridge contract.
- [Upgrading the CW20 wrapped token contract](token_upgrades.md) describes the Token Bridge contract and its implementation.

58
terra/docs/token_id.md Normal file
View File

@ -0,0 +1,58 @@
# Token ID encoding in the Token bridge
## Background
Terra classic (being a Cosmos chain with a CosmWasm runtime) supports two types of tokens: "native Bank tokens" and "CW20 tokens".
The Bank module is a native Cosmos SDK module that supports the transfer of tokens between accounts. These tokens are identified by their denomination (e.g. `uluna` or `uusd`), and are analogous to the native tokens on other chains, such as eth on Ethereum or sol on Solana.
CW20 tokens on the other hand are smart contracts that implement the CW20 interface, which is analogous to the ERC20 interface on EVM chains. These tokens are identified by their contract address.
The Terra token bridge supports both types of tokens directly (i.e. without wrapping them in a synthetic token like the EVM token bridge wraps eth into the canonical Wrapped ETH ERC20 contract). This means that the token bridge needs to be able to distinguish between the two types of tokens.
Addresses (both account and CosmWasm contract) on Terra used to fit into 20 bytes.
This changed when the chain underwent a hard fork to upgrade the runtime to CosmWasm 1.1.0 in June 2023. New contract addresses and account addresses are now 32 bytes long.
The initial design was made with the assumption that addresses are 20 bytes long. We first discuss that original version, and the adjustments that were made to support 32 byte addresses.
## Token ID encoding before CosmWasm 1.1.0
In the [Wormhole Token Bridge wire format](../../whitepapers/0003_token_bridge.md), token addresses are encoded as 32 bytes. Since CW20 addresses were 20 bytes long, the first 12 bytes were set to zero. The decision was also made to limit the length of native denom strings to 20 bytes also. This meant that the first 12 bytes of both CW20 addresses and native denoms were always zero.
The way the token bridge would then distinguish between the two is by writing a `0x01` byte in the first byte position of native denoms. Then, if the first byte of the token address is `0x01`, the token is a native denom, and if it is `0x00`, the token is a CW20 token. If it is anything else, the token is invalid.
```rust
let marker_byte = transfer_info.token_address.as_slice()[0];
if transfer_info.token_chain == CHAIN_ID {
match marker_byte {
1 => handle_complete_transfer_token_native(...),
0 => handle_complete_transfer_token(...),
b => Err(StdError::generic_err(format!("Unknown marker byte: {b}"))),
}
} else {
handle_complete_transfer_token(...)
}
```
[wormhole/terra/contracts/token-bridge/src/contract.rs#L734-L770](https://github.com/wormhole-foundation/wormhole/blob/dee0d1532b4a4ab6657dbdd1f0b8d19eadd90ec9/terra/contracts/token-bridge/src/contract.rs#L734-L770)
## Token ID encoding after CosmWasm 1.1.0
After the hard fork, addresses can now be 32 bytes long. Theoretically this would mean that new (32 byte addressed) tokens bridged out then back could collide with that check above. However, on the way out the token's address was checked to fit into 20 bytes, so no 32 byte addressed CW20 could be bridged out. That is, prior to upgrading the contracts to CW 1.1.0, the token bridge would not allow bridging out 32 byte addressed CW20 tokens.
In order to support 32 byte addresses, we simply change the above check so instead of just checking that the first byte is `0x01` for native denoms, we check that the first byte is `0x01` and the next 11 bytes are `0x00`:
```rust
fn is_native_id(address: &[u8]) -> bool {
address[0] == 1 && address[1..12].iter().all(|&x| x == 0)
}
```
[wormhole/terra/contracts/token-bridge/src/contract.rs#L1434-L1436](https://github.com/wormhole-foundation/wormhole/blob/6e9127bd2a0a3d7f71ac6709a2893f6132bfe3ae/terra/contracts/token-bridge/src/contract.rs#L1434-L1436)
Now the check becomes:
```rustic
if transfer_info.token_chain == CHAIN_ID && is_native_id(transfer_info.token_address.as_slice())
{
handle_complete_transfer_token_native(...)
} else {
handle_complete_transfer_token(...)
}
```
[wormhole/terra/contracts/token-bridge/src/contract.rs#L684-L707](https://github.com/wormhole-foundation/wormhole/blob/6e9127bd2a0a3d7f71ac6709a2893f6132bfe3ae/terra/contracts/token-bridge/src/contract.rs#L684-L707)
This is backwards compatible with the old encoding, but also allows for 32 byte addressed CW20 tokens. There is a theoretical possibility that the CW20 address happens have the first 12 bytes in the form `0x01 0x00 0x00 ... 0x00`, but this is extremely unlikely (1 in 2^96, assuming that the bits of the address are uniformly distributed).

View File

@ -0,0 +1,60 @@
# Upgrading the CW20 wrapped token contract
## Background: CosmWasm upgrades
CosmWasm contracts are deployed by first uploading the WASM bytecode, then instantiating a contract from that bytecode. The bytecode itself gets assigned a "code ID", which is an incrementally allocated identifier to uploaded WASM bytecodes. A given code ID can be instantiated multiple times, each time running the initialiser (the `instantiate` entrypoint).
Upgrades to a contract can then be performed by the contract owner sending a `migrate` message to the contract with the new code ID. The contract's storage will remain intact, but the underlying code id (and thus the bytecode) is replaced with the new ID. The runtime also executes the `migrate` entrypoint of the *new* bytecode within the upgrade transaction atomically.
## Background: Token bridge CW20 wrapped tokens
When the token bridge is instantiated, the wrapped asset code ID is passed to the instantiation handler:
```rust
pub struct InstantiateMsg {
// governance contract details
pub gov_chain: u16,
pub gov_address: Binary,
pub wormhole_contract: HumanAddr,
pub wrapped_asset_code_id: u64, // <---- code id
}
```
[wormhole/terra/contracts/token-bridge/src/msg.rs#L9-L16](https://github.com/wormhole-foundation/wormhole/blob/dee0d1532b4a4ab6657dbdd1f0b8d19eadd90ec9/terra/contracts/token-bridge/src/msg.rs#L9-L16)
then during wrapped asset creation, the token bridge contract instantiates new instances of this contract by sending the `instantiate` message with the appropriate code id:
```rust
CosmosMsg::Wasm(WasmMsg::Instantiate {
admin: Some(env.contract.address.clone().into_string()),
code_id: cfg.wrapped_asset_code_id,
msg: to_binary(&WrappedInit {
...
})?,
...
})
```
[wormhole/terra/contracts/token-bridge/src/contract.rs#L458-L477](https://github.com/wormhole-foundation/wormhole/blob/dee0d1532b4a4ab6657dbdd1f0b8d19eadd90ec9/terra/contracts/token-bridge/src/contract.rs#L458-L477)
## Upgrading the wrapped CW20 contract
When upgrading the token contract, two steps need to be taken:
1. Update the code ID in the state so future wrapped assets are created from the new ID:
```rust
let mut c = config(deps.storage).load()?;
c.wrapped_asset_code_id = new_code_id;
config(deps.storage).save(&c)?;
```
[wormhole/terra/contracts/token-bridge/src/contract.rs#L79-L81](https://github.com/wormhole-foundation/wormhole/blob/dee0d1532b4a4ab6657dbdd1f0b8d19eadd90ec9/terra/contracts/token-bridge/src/contract.rs#L79-L81)
2. Migrate all existing contracts to the new code ID. A simple implementation of this function can be seen [here](https://github.com/wormhole-foundation/wormhole/blob/dee0d1532b4a4ab6657dbdd1f0b8d19eadd90ec9/terra/contracts/token-bridge/src/contract.rs#L123-L147). Unfortunately, while this simple approach works in local testing, it does not scale to mainnet, the contract sends too many migrate messages (one to each wrapped asset), and exceeds the gas counter.
Currently there is no implementation of a token contract migration function that works on mainnet. If the need arises, one would have to be designed. If the upgrade does not require the migration to happen atomically, then it could be handled with a new permissionless entrypoint that can handle a range of contracts, and it could be done in multiple transactions. If for some reason atomicity is required, then extra care must be taken to pause all other bridge operation and perform the upgrade over multiple transactions.
## Historical note:
In Aug 2022, an attempt to upgrade the the token contracts was made, but failed due to the gas error:
https://finder.terra.money/classic/tx/FE39E9549770F59E2AAA1C6B0B86DDF36A4C56CED0CFB0CA4C9D4CC9FBE1E5BA. A subsequent upgrade changed the code id of *new* wrapped contracts to 767, but did not perform the migration for old contracts. This means that currently (as of Dec 2023), some wrapped tokens are still on the old code id, and some (the ones deployed after Aug 2022) are on the new code. This discrepancy is fine in the current case, because it only affects the rendering of the token name (https://github.com/wormhole-foundation/wormhole/commit/c832b123fcfb017d55086cb4d71241370ed270c6).