Skip to content

Commit 4a2bb5d

Browse files
committed
solana: cap decimal trimming by destination decimals
1 parent 6d23a29 commit 4a2bb5d

File tree

9 files changed

+74
-32
lines changed

9 files changed

+74
-32
lines changed

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ Wormhole’s Native Token Transfers (NTT) is an open, flexible, and composable f
99

1010
- 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.
1111

12+
### Amount trimming
13+
14+
In the payload, amounts are encoded as unsigned 64 bit integers, and capped at 8 decimals.
15+
This means that if on the sending chain, the token has more than 8 decimals, then the amount is trimmed.
16+
The amount that's removed during trimming is referred to as "dust". The contracts make sure to never destroy dust.
17+
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.
18+
19+
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).
20+
1221
### NTT Message Lifecycle
1322

1423
### EVM

solana/modules/ntt-messages/src/trimmed_amount.rs

+26-7
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ impl TrimmedAmount {
6161
}
6262
}
6363

64-
pub fn trim(amount: u64, from_decimals: u8) -> TrimmedAmount {
65-
let to_decimals = TRIMMED_DECIMALS.min(from_decimals);
64+
pub fn trim(amount: u64, from_decimals: u8, to_decimals: u8) -> TrimmedAmount {
65+
let to_decimals = TRIMMED_DECIMALS.min(from_decimals).min(to_decimals);
6666
Self {
6767
amount: Self::scale(amount, from_decimals, to_decimals),
6868
decimals: to_decimals,
@@ -73,8 +73,14 @@ impl TrimmedAmount {
7373
Self::scale(self.amount, self.decimals, to_decimals)
7474
}
7575

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

8086
pub fn amount(&self) -> u64 {
@@ -121,20 +127,33 @@ mod test {
121127
#[test]
122128
fn test_trim() {
123129
assert_eq!(
124-
TrimmedAmount::trim(100_000_000_000_000_000, 18).amount(),
130+
TrimmedAmount::trim(100_000_000_000_000_000, 18, 13).amount(),
125131
10_000_000
126132
);
127133

128134
assert_eq!(
129-
TrimmedAmount::trim(100_000_000_000_000_000, 7).amount(),
135+
TrimmedAmount::trim(100_000_000_000_000_000, 7, 11).amount(),
130136
100_000_000_000_000_000
131137
);
132138

133139
assert_eq!(
134-
TrimmedAmount::trim(100_555_555_555_555_555, 18).untrim(18),
140+
TrimmedAmount::trim(100_555_555_555_555_555, 18, 9).untrim(18),
135141
100_555_550_000_000_000
136142
);
137143

144+
assert_eq!(
145+
TrimmedAmount::trim(100_555_555_555_555_555, 18, 1).untrim(18),
146+
100_000_000_000_000_000
147+
);
148+
149+
assert_eq!(
150+
TrimmedAmount::trim(158434, 6, 3),
151+
TrimmedAmount {
152+
amount: 158,
153+
decimals: 3
154+
}
155+
);
156+
138157
assert_eq!(
139158
TrimmedAmount {
140159
amount: 1,

solana/programs/example-native-token-transfers/src/instructions/admin.rs

+3
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,15 @@ pub struct SetPeerArgs {
170170
pub chain_id: ChainId,
171171
pub address: [u8; 32],
172172
pub limit: u64,
173+
/// The token decimals on the peer chain.
174+
pub token_decimals: u8,
173175
}
174176

175177
pub fn set_peer(ctx: Context<SetPeer>, args: SetPeerArgs) -> Result<()> {
176178
ctx.accounts.peer.set_inner(NttManagerPeer {
177179
bump: ctx.bumps.peer,
178180
address: args.address,
181+
token_decimals: args.token_decimals,
179182
});
180183

181184
ctx.accounts.inbox_rate_limit.set_inner(InboxRateLimit {

solana/programs/example-native-token-transfers/src/instructions/transfer.rs

+17-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![allow(clippy::too_many_arguments)]
12
use anchor_lang::prelude::*;
23
use anchor_spl::token_interface;
34
use ntt_messages::{chain_id::ChainId, trimmed_amount::TrimmedAmount};
@@ -110,14 +111,18 @@ pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<(
110111

111112
let accs = ctx.accounts;
112113
let TransferArgs {
113-
amount,
114+
mut amount,
114115
recipient_chain,
115116
recipient_address,
116117
should_queue,
117118
} = args;
118119

119120
// TODO: should we revert if we have dust?
120-
let amount = TrimmedAmount::remove_dust(amount, accs.common.mint.decimals);
121+
let trimmed_amount = TrimmedAmount::remove_dust(
122+
&mut amount,
123+
accs.common.mint.decimals,
124+
accs.peer.token_decimals,
125+
);
121126

122127
token_interface::burn(
123128
CpiContext::new_with_signer(
@@ -141,6 +146,7 @@ pub fn transfer_burn(ctx: Context<TransferBurn>, args: TransferArgs) -> Result<(
141146
&mut accs.common,
142147
&mut accs.inbox_rate_limit,
143148
amount,
149+
trimmed_amount,
144150
recipient_chain,
145151
recipient_ntt_manager,
146152
recipient_address,
@@ -189,14 +195,18 @@ pub fn transfer_lock(ctx: Context<TransferLock>, args: TransferArgs) -> Result<(
189195

190196
let accs = ctx.accounts;
191197
let TransferArgs {
192-
amount,
198+
mut amount,
193199
recipient_chain,
194200
recipient_address,
195201
should_queue,
196202
} = args;
197203

198204
// TODO: should we revert if we have dust?
199-
let amount = TrimmedAmount::remove_dust(amount, accs.common.mint.decimals);
205+
let trimmed_amount = TrimmedAmount::remove_dust(
206+
&mut amount,
207+
accs.common.mint.decimals,
208+
accs.peer.token_decimals,
209+
);
200210

201211
token_interface::transfer_checked(
202212
CpiContext::new_with_signer(
@@ -222,6 +232,7 @@ pub fn transfer_lock(ctx: Context<TransferLock>, args: TransferArgs) -> Result<(
222232
&mut accs.common,
223233
&mut accs.inbox_rate_limit,
224234
amount,
235+
trimmed_amount,
225236
recipient_chain,
226237
recipient_ntt_manager,
227238
recipient_address,
@@ -233,6 +244,7 @@ fn insert_into_outbox(
233244
common: &mut Transfer<'_>,
234245
inbox_rate_limit: &mut InboxRateLimit,
235246
amount: u64,
247+
trimmed_amount: TrimmedAmount,
236248
recipient_chain: ChainId,
237249
recipient_ntt_manager: [u8; 32],
238250
recipient_address: [u8; 32],
@@ -258,7 +270,7 @@ fn insert_into_outbox(
258270

259271
common.outbox_item.set_inner(OutboxItem {
260272
sequence,
261-
amount: TrimmedAmount::trim(amount, common.mint.decimals),
273+
amount: trimmed_amount,
262274
sender: common.sender.key(),
263275
recipient_chain,
264276
recipient_ntt_manager,

solana/programs/example-native-token-transfers/src/peer.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use anchor_lang::prelude::*;
55
/// A peer on another chain. Stored in a PDA seeded by the chain id.
66
pub struct NttManagerPeer {
77
pub bump: u8,
8-
// TODO: variable address length?
98
pub address: [u8; 32],
9+
pub token_decimals: u8,
1010
}
1111

1212
impl NttManagerPeer {

solana/programs/example-native-token-transfers/tests/common/setup.rs

+1
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ pub async fn setup_ntt(ctx: &mut ProgramTestContext, test_data: &TestData, mode:
205205
chain_id: ChainId { id: OTHER_CHAIN },
206206
address: OTHER_MANAGER,
207207
limit: INBOUND_LIMIT,
208+
token_decimals: 7,
208209
},
209210
)
210211
.submit_with_signers(&[&test_data.program_owner], ctx)

solana/programs/example-native-token-transfers/tests/transfer.rs

+10-10
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ async fn test_transfer(ctx: &mut ProgramTestContext, test_data: &TestData, mode:
104104

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

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

109109
approve_token_authority(
110110
&test_data.ntt,
@@ -127,8 +127,8 @@ async fn test_transfer(ctx: &mut ProgramTestContext, test_data: &TestData, mode:
127127
OutboxItem {
128128
sequence: sequence.sequence,
129129
amount: TrimmedAmount {
130-
amount: 10,
131-
decimals: 8
130+
amount: 1,
131+
decimals: 7
132132
},
133133
sender: test_data.user.pubkey(),
134134
recipient_chain: ChainId { id: 2 },
@@ -187,8 +187,8 @@ async fn test_transfer(ctx: &mut ProgramTestContext, test_data: &TestData, mode:
187187
sender: test_data.user.pubkey().to_bytes(),
188188
payload: NativeTokenTransfer {
189189
amount: TrimmedAmount {
190-
amount: 10,
191-
decimals: 8
190+
amount: 1,
191+
decimals: 7
192192
},
193193
source_token: test_data.mint.to_bytes(),
194194
to: [1u8; 32],
@@ -253,7 +253,7 @@ async fn locking_mode_locks_tokens() {
253253

254254
let outbox_item = Keypair::new();
255255

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

258258
let token_account_before: TokenAccount = ctx
259259
.get_account_data_anchor(test_data.user_token_account)
@@ -289,16 +289,16 @@ async fn locking_mode_locks_tokens() {
289289

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

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

295295
assert_eq!(
296-
token_account_before.amount - 100,
296+
token_account_before.amount - 1000,
297297
token_account_after.amount
298298
);
299299

300300
assert_eq!(
301-
custody_account_before.amount + 100,
301+
custody_account_before.amount + 1000,
302302
custody_account_after.amount
303303
);
304304

solana/tests/example-native-token-transfer.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as anchor from '@coral-xyz/anchor'
2-
import { BN, type Program } from '@coral-xyz/anchor'
2+
import { BN } from '@coral-xyz/anchor'
33
import * as spl from '@solana/spl-token'
4-
import { type ExampleNativeTokenTransfers } from '../ts/sdk'
54
import { PostedMessageData } from '@certusone/wormhole-sdk/lib/cjs/solana/wormhole'
65
import { expect } from 'chai'
76
import { toChainId } from '@certusone/wormhole-sdk'
@@ -13,13 +12,9 @@ import { type TransceiverMessage, NttManagerMessage, NativeTokenTransfer, Trimme
1312
export const GUARDIAN_KEY = 'cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0'
1413

1514
describe('example-native-token-transfers', () => {
16-
// Configure the client to use the local cluster.
17-
//anchor.setProvider(anchor.AnchorProvider.env())
18-
1915
const payerSecretKey = Uint8Array.from(JSON.parse(fs.readFileSync(`${__dirname}/../keys/test.json`, { encoding: "utf-8" })));
2016
const payer = anchor.web3.Keypair.fromSecretKey(payerSecretKey);
2117

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

9086
});

solana/ts/sdk/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -534,13 +534,15 @@ export class NTT {
534534
owner: Keypair
535535
chain: ChainName
536536
address: ArrayLike<number>
537-
limit: BN
537+
limit: BN,
538+
tokenDecimals: number
538539
config?: Config
539540
}) {
540541
const ix = await this.program.methods.setPeer({
541542
chainId: { id: toChainId(args.chain) },
542543
address: Array.from(args.address),
543-
limit: args.limit
544+
limit: args.limit,
545+
tokenDecimals: args.tokenDecimals
544546
})
545547
.accounts({
546548
payer: args.payer.publicKey,

0 commit comments

Comments
 (0)