168 lines
5.4 KiB
Plaintext
168 lines
5.4 KiB
Plaintext
// SPDX-License-Identifier: Apache 2
|
|
|
|
/// This module implements a container that stores the token transfer amount
|
|
/// encoded in a Token Bridge message. These amounts are capped at 8 decimals.
|
|
/// This means that any amount of a coin whose metadata defines its decimals
|
|
/// as some value greater than 8, the encoded amount will be normalized to
|
|
/// eight decimals (which will lead to some residual amount after the transfer).
|
|
/// For inbound transfers, this amount will be denormalized (scaled by the same
|
|
/// decimal difference).
|
|
module token_bridge::normalized_amount {
|
|
use sui::math::{Self};
|
|
use wormhole::bytes32::{Self};
|
|
use wormhole::cursor::{Cursor};
|
|
|
|
/// The amounts in the token bridge payload are truncated to 8 decimals
|
|
/// in each of the contracts when sending tokens out, so there's no
|
|
/// precision beyond 10^-8. We could preserve the original number of
|
|
/// decimals when creating wrapped assets, and "untruncate" the amounts
|
|
/// on the way out by scaling back appropriately. This is what most
|
|
/// other chains do, but untruncating from 8 decimals to 18 decimals
|
|
/// loses log2(10^10) ~ 33 bits of precision, which we cannot afford on
|
|
/// Aptos (and Solana), as the coin type only has 64bits to begin with.
|
|
/// Contrast with Ethereum, where amounts are 256 bits.
|
|
/// So we cap the maximum decimals at 8 when creating a wrapped token.
|
|
const MAX_DECIMALS: u8 = 8;
|
|
|
|
/// Container holding the value decoded from a Token Bridge transfer.
|
|
struct NormalizedAmount has store, copy, drop {
|
|
value: u64
|
|
}
|
|
|
|
public fun max_decimals(): u8 {
|
|
MAX_DECIMALS
|
|
}
|
|
|
|
/// Utility function to cap decimal amount to 8.
|
|
public fun cap_decimals(decimals: u8): u8 {
|
|
if (decimals > MAX_DECIMALS) {
|
|
MAX_DECIMALS
|
|
} else {
|
|
decimals
|
|
}
|
|
}
|
|
|
|
/// Create new `NormalizedAmount` of zero.
|
|
public fun default(): NormalizedAmount {
|
|
new(0)
|
|
}
|
|
|
|
/// Retrieve underlying value.
|
|
public fun value(self: &NormalizedAmount): u64 {
|
|
self.value
|
|
}
|
|
|
|
/// Retrieve underlying value as `u256`.
|
|
public fun to_u256(norm: NormalizedAmount): u256 {
|
|
(take_value(norm) as u256)
|
|
}
|
|
|
|
/// Create new `NormalizedAmount` using raw amount and specified decimals.
|
|
public fun from_raw(amount: u64, decimals: u8): NormalizedAmount {
|
|
if (amount == 0) {
|
|
default()
|
|
} else if (decimals > MAX_DECIMALS) {
|
|
new(amount / math::pow(10, decimals - MAX_DECIMALS))
|
|
} else {
|
|
new(amount)
|
|
}
|
|
}
|
|
|
|
/// Denormalize `NormalizedAmount` using specified decimals.
|
|
public fun to_raw(norm: NormalizedAmount, decimals: u8): u64 {
|
|
let value = take_value(norm);
|
|
|
|
if (value > 0 && decimals > MAX_DECIMALS) {
|
|
value * math::pow(10, decimals - MAX_DECIMALS)
|
|
} else {
|
|
value
|
|
}
|
|
}
|
|
|
|
/// Transform `NormalizedAmount` to serialized (big-endian) u256.
|
|
public fun to_bytes(norm: NormalizedAmount): vector<u8> {
|
|
bytes32::to_bytes(bytes32::from_u256_be(to_u256(norm)))
|
|
}
|
|
|
|
/// Read 32 bytes from `Cursor` and deserialize to u64, ensuring no
|
|
/// overflow.
|
|
public fun take_bytes(cur: &mut Cursor<u8>): NormalizedAmount {
|
|
// Amounts are encoded with 32 bytes.
|
|
new(bytes32::to_u64_be(bytes32::take_bytes(cur)))
|
|
}
|
|
|
|
fun new(value: u64): NormalizedAmount {
|
|
NormalizedAmount {
|
|
value
|
|
}
|
|
}
|
|
|
|
fun take_value(norm: NormalizedAmount): u64 {
|
|
let NormalizedAmount { value } = norm;
|
|
value
|
|
}
|
|
}
|
|
|
|
#[test_only]
|
|
module token_bridge::normalized_amount_test {
|
|
use wormhole::bytes::{Self};
|
|
use wormhole::cursor::{Self};
|
|
|
|
use token_bridge::normalized_amount::{Self};
|
|
|
|
#[test]
|
|
fun test_from_and_to_raw() {
|
|
// Use decimals > 8 to check truncation.
|
|
let decimals = 9;
|
|
let raw_amount = 12345678910111;
|
|
let normalized = normalized_amount::from_raw(raw_amount, decimals);
|
|
let denormalized = normalized_amount::to_raw(normalized, decimals);
|
|
assert!(denormalized == 10 * (raw_amount / 10), 0);
|
|
|
|
// Use decimals <= 8 to check raw amount recovery.
|
|
let decimals = 5;
|
|
let normalized = normalized_amount::from_raw(raw_amount, decimals);
|
|
let denormalized = normalized_amount::to_raw(normalized, decimals);
|
|
assert!(denormalized == raw_amount, 0);
|
|
}
|
|
|
|
#[test]
|
|
fun test_take_bytes() {
|
|
let cur =
|
|
cursor::new(
|
|
x"000000000000000000000000000000000000000000000000ffffffffffffffff"
|
|
);
|
|
|
|
let norm = normalized_amount::take_bytes(&mut cur);
|
|
assert!(
|
|
normalized_amount::value(&norm) == ((1u256 << 64) - 1 as u64),
|
|
0
|
|
);
|
|
|
|
// Clean up.
|
|
cursor::destroy_empty(cur);
|
|
}
|
|
|
|
#[test]
|
|
#[expected_failure(abort_code = wormhole::bytes32::E_U64_OVERFLOW)]
|
|
fun test_cannot_take_bytes_overflow() {
|
|
let encoded_overflow =
|
|
x"0000000000000000000000000000000000000000000000010000000000000000";
|
|
|
|
let amount = {
|
|
let cur = cursor::new(encoded_overflow);
|
|
let value = bytes::take_u256_be(&mut cur);
|
|
cursor::destroy_empty(cur);
|
|
value
|
|
};
|
|
assert!(amount == (1 << 64), 0);
|
|
|
|
let cur = cursor::new(encoded_overflow);
|
|
|
|
// You shall not pass!
|
|
normalized_amount::take_bytes(&mut cur);
|
|
|
|
abort 42
|
|
}
|
|
}
|