Skip to content

Commit

Permalink
solana: cap decimal trimming by destination decimals
Browse files Browse the repository at this point in the history
  • Loading branch information
kcsongor committed Feb 26, 2024
1 parent 6d23a29 commit 4a2bb5d
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 32 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ Wormhole’s Native Token Transfers (NTT) is an open, flexible, and composable f

- NttManager: The NttManager contract is responsible for managing the token and the transceivers. It also handles the rate limiting and the message attestation logic. Note that each NTTManager corresponds to a single token. However, a single NTTManager can manager can control multiple transceivers.

### Amount trimming

In the payload, amounts are encoded as unsigned 64 bit integers, and capped at 8 decimals.
This means that if on the sending chain, the token has more than 8 decimals, then the amount is trimmed.
The amount that's removed during trimming is referred to as "dust". The contracts make sure to never destroy dust.
The NTT manager contracts additionally keep track of the token decimals of the other connected chains. When sending to a chain whose token decimals are less than 8, the amount is instead truncated to those decimals, in order to ensure that the recipient contract can handle the amount without destroying dust.

The payload includes the trimmed amount, together with the decimals that trimmed amount is expressed in. This number is the minimum of (8, source token decimals, destination token decimals).

### NTT Message Lifecycle

### EVM
Expand Down
33 changes: 26 additions & 7 deletions solana/modules/ntt-messages/src/trimmed_amount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ impl TrimmedAmount {
}
}

pub fn trim(amount: u64, from_decimals: u8) -> TrimmedAmount {
let to_decimals = TRIMMED_DECIMALS.min(from_decimals);
pub fn trim(amount: u64, from_decimals: u8, to_decimals: u8) -> TrimmedAmount {
let to_decimals = TRIMMED_DECIMALS.min(from_decimals).min(to_decimals);
Self {
amount: Self::scale(amount, from_decimals, to_decimals),
decimals: to_decimals,
Expand All @@ -73,8 +73,14 @@ impl TrimmedAmount {
Self::scale(self.amount, self.decimals, to_decimals)
}

pub fn remove_dust(amount: u64, from_decimals: u8) -> u64 {
Self::trim(amount, from_decimals).untrim(from_decimals)
/// Removes dust from an amount, returning the the amount with the removed
/// dust (expressed in the original decimals) and the trimmed amount.
/// The two amounts returned are equivalent, but (potentially) expressed in
/// different decimals.
pub fn remove_dust(amount: &mut u64, from_decimals: u8, to_decimals: u8) -> TrimmedAmount {
let trimmed = Self::trim(*amount, from_decimals, to_decimals);
*amount = trimmed.untrim(from_decimals);
trimmed
}

pub fn amount(&self) -> u64 {
Expand Down Expand Up @@ -121,20 +127,33 @@ mod test {
#[test]
fn test_trim() {
assert_eq!(
TrimmedAmount::trim(100_000_000_000_000_000, 18).amount(),
TrimmedAmount::trim(100_000_000_000_000_000, 18, 13).amount(),
10_000_000
);

assert_eq!(
TrimmedAmount::trim(100_000_000_000_000_000, 7).amount(),
TrimmedAmount::trim(100_000_000_000_000_000, 7, 11).amount(),
100_000_000_000_000_000
);

assert_eq!(
TrimmedAmount::trim(100_555_555_555_555_555, 18).untrim(18),
TrimmedAmount::trim(100_555_555_555_555_555, 18, 9).untrim(18),
100_555_550_000_000_000
);

assert_eq!(
TrimmedAmount::trim(100_555_555_555_555_555, 18, 1).untrim(18),
100_000_000_000_000_000
);

assert_eq!(
TrimmedAmount::trim(158434, 6, 3),
TrimmedAmount {
amount: 158,
decimals: 3
}
);

assert_eq!(
TrimmedAmount {
amount: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,15 @@ pub struct SetPeerArgs {
pub chain_id: ChainId,
pub address: [u8; 32],
pub limit: u64,
/// The token decimals on the peer chain.
pub token_decimals: u8,
}

pub fn set_peer(ctx: Context<SetPeer>, args: SetPeerArgs) -> Result<()> {
ctx.accounts.peer.set_inner(NttManagerPeer {
bump: ctx.bumps.peer,
address: args.address,
token_decimals: args.token_decimals,
});

ctx.accounts.inbox_rate_limit.set_inner(InboxRateLimit {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![allow(clippy::too_many_arguments)]
use anchor_lang::prelude::*;
use anchor_spl::token_interface;
use ntt_messages::{chain_id::ChainId, trimmed_amount::TrimmedAmount};
Expand Down Expand Up @@ -110,14 +111,18 @@ pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<(

let accs = ctx.accounts;
let TransferArgs {
amount,
mut amount,
recipient_chain,
recipient_address,
should_queue,
} = args;

// TODO: should we revert if we have dust?
let amount = TrimmedAmount::remove_dust(amount, accs.common.mint.decimals);
let trimmed_amount = TrimmedAmount::remove_dust(
&mut amount,
accs.common.mint.decimals,
accs.peer.token_decimals,
);

token_interface::burn(
CpiContext::new_with_signer(
Expand All @@ -141,6 +146,7 @@ pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<(
&mut accs.common,
&mut accs.inbox_rate_limit,
amount,
trimmed_amount,
recipient_chain,
recipient_ntt_manager,
recipient_address,
Expand Down Expand Up @@ -189,14 +195,18 @@ pub fn transfer_lock(ctx: Context<TransferLock>, args: TransferArgs) -> Result<(

let accs = ctx.accounts;
let TransferArgs {
amount,
mut amount,
recipient_chain,
recipient_address,
should_queue,
} = args;

// TODO: should we revert if we have dust?
let amount = TrimmedAmount::remove_dust(amount, accs.common.mint.decimals);
let trimmed_amount = TrimmedAmount::remove_dust(
&mut amount,
accs.common.mint.decimals,
accs.peer.token_decimals,
);

token_interface::transfer_checked(
CpiContext::new_with_signer(
Expand All @@ -222,6 +232,7 @@ pub fn transfer_lock(ctx: Context<TransferLock>, args: TransferArgs) -> Result<(
&mut accs.common,
&mut accs.inbox_rate_limit,
amount,
trimmed_amount,
recipient_chain,
recipient_ntt_manager,
recipient_address,
Expand All @@ -233,6 +244,7 @@ fn insert_into_outbox(
common: &mut Transfer<'_>,
inbox_rate_limit: &mut InboxRateLimit,
amount: u64,
trimmed_amount: TrimmedAmount,
recipient_chain: ChainId,
recipient_ntt_manager: [u8; 32],
recipient_address: [u8; 32],
Expand All @@ -258,7 +270,7 @@ fn insert_into_outbox(

common.outbox_item.set_inner(OutboxItem {
sequence,
amount: TrimmedAmount::trim(amount, common.mint.decimals),
amount: trimmed_amount,
sender: common.sender.key(),
recipient_chain,
recipient_ntt_manager,
Expand Down
2 changes: 1 addition & 1 deletion solana/programs/example-native-token-transfers/src/peer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use anchor_lang::prelude::*;
/// A peer on another chain. Stored in a PDA seeded by the chain id.
pub struct NttManagerPeer {
pub bump: u8,
// TODO: variable address length?
pub address: [u8; 32],
pub token_decimals: u8,
}

impl NttManagerPeer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ pub async fn setup_ntt(ctx: &mut ProgramTestContext, test_data: &TestData, mode:
chain_id: ChainId { id: OTHER_CHAIN },
address: OTHER_MANAGER,
limit: INBOUND_LIMIT,
token_decimals: 7,
},
)
.submit_with_signers(&[&test_data.program_owner], ctx)
Expand Down
20 changes: 10 additions & 10 deletions solana/programs/example-native-token-transfers/tests/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async fn test_transfer(ctx: &mut ProgramTestContext, test_data: &TestData, mode:

let sequence: Sequence = ctx.get_account_data_anchor(test_data.ntt.sequence()).await;

let (accs, args) = init_accs_args(ctx, test_data, outbox_item.pubkey(), 100, false);
let (accs, args) = init_accs_args(ctx, test_data, outbox_item.pubkey(), 154, false);

approve_token_authority(
&test_data.ntt,
Expand All @@ -127,8 +127,8 @@ async fn test_transfer(ctx: &mut ProgramTestContext, test_data: &TestData, mode:
OutboxItem {
sequence: sequence.sequence,
amount: TrimmedAmount {
amount: 10,
decimals: 8
amount: 1,
decimals: 7
},
sender: test_data.user.pubkey(),
recipient_chain: ChainId { id: 2 },
Expand Down Expand Up @@ -187,8 +187,8 @@ async fn test_transfer(ctx: &mut ProgramTestContext, test_data: &TestData, mode:
sender: test_data.user.pubkey().to_bytes(),
payload: NativeTokenTransfer {
amount: TrimmedAmount {
amount: 10,
decimals: 8
amount: 1,
decimals: 7
},
source_token: test_data.mint.to_bytes(),
to: [1u8; 32],
Expand Down Expand Up @@ -253,7 +253,7 @@ async fn locking_mode_locks_tokens() {

let outbox_item = Keypair::new();

let (accs, args) = init_accs_args(&mut ctx, &test_data, outbox_item.pubkey(), 105, false);
let (accs, args) = init_accs_args(&mut ctx, &test_data, outbox_item.pubkey(), 1050, false);

let token_account_before: TokenAccount = ctx
.get_account_data_anchor(test_data.user_token_account)
Expand Down Expand Up @@ -289,16 +289,16 @@ async fn locking_mode_locks_tokens() {

let mint_after: Mint = ctx.get_account_data_anchor(test_data.mint).await;

// NOTE: we transfer 105, but only 100 gets locked (token is 9 decimals, and
// gets trimmed to 8)
// NOTE: we transfer 1050, but only 1000 gets locked (token is 9 decimals, and
// gets trimmed to 7 because of the target chain's decimals)

assert_eq!(
token_account_before.amount - 100,
token_account_before.amount - 1000,
token_account_after.amount
);

assert_eq!(
custody_account_before.amount + 100,
custody_account_before.amount + 1000,
custody_account_after.amount
);

Expand Down
10 changes: 3 additions & 7 deletions solana/tests/example-native-token-transfer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as anchor from '@coral-xyz/anchor'
import { BN, type Program } from '@coral-xyz/anchor'
import { BN } from '@coral-xyz/anchor'
import * as spl from '@solana/spl-token'
import { type ExampleNativeTokenTransfers } from '../ts/sdk'
import { PostedMessageData } from '@certusone/wormhole-sdk/lib/cjs/solana/wormhole'
import { expect } from 'chai'
import { toChainId } from '@certusone/wormhole-sdk'
Expand All @@ -13,13 +12,9 @@ import { type TransceiverMessage, NttManagerMessage, NativeTokenTransfer, Trimme
export const GUARDIAN_KEY = 'cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0'

describe('example-native-token-transfers', () => {
// Configure the client to use the local cluster.
//anchor.setProvider(anchor.AnchorProvider.env())

const payerSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync(`${__dirname}/../keys/test.json`, { encoding: "utf-8" })));
const payer = anchor.web3.Keypair.fromSecretKey(payerSecretKey);

//const program = anchor.workspace.ExampleNativeTokenTransfers as Program<ExampleNativeTokenTransfers>
const owner = anchor.web3.Keypair.generate()
const connection = new anchor.web3.Connection('http://localhost:8899', 'confirmed');
const ntt = new NTT(connection, {
Expand Down Expand Up @@ -84,7 +79,8 @@ describe('example-native-token-transfers', () => {
owner: payer,
chain: 'ethereum',
address: Buffer.from('ntt_manager'.padStart(32, '\0')),
limit: new BN(1000000)
limit: new BN(1000000),
tokenDecimals: 18
})

});
Expand Down
6 changes: 4 additions & 2 deletions solana/ts/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,13 +534,15 @@ export class NTT {
owner: Keypair
chain: ChainName
address: ArrayLike<number>
limit: BN
limit: BN,
tokenDecimals: number
config?: Config
}) {
const ix = await this.program.methods.setPeer({
chainId: { id: toChainId(args.chain) },
address: Array.from(args.address),
limit: args.limit
limit: args.limit,
tokenDecimals: args.tokenDecimals
})
.accounts({
payer: args.payer.publicKey,
Expand Down

0 comments on commit 4a2bb5d

Please sign in to comment.