Skip to content

Commit abe704d

Browse files
committed
solana: add recovery flow
1 parent 4b55b84 commit abe704d

File tree

7 files changed

+292
-59
lines changed

7 files changed

+292
-59
lines changed

solana/programs/example-native-token-transfers/Cargo.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ crate-type = ["cdylib", "lib"]
99
name = "example_native_token_transfers"
1010

1111
[features]
12-
default = ["mainnet"]
12+
default = ["owner-recovery", "mainnet"]
1313
no-entrypoint = []
1414
no-idl = []
1515
no-log-ix-name = []
1616
cpi = ["no-entrypoint"]
1717
idl-build = [
1818
"anchor-lang/idl-build",
19-
"anchor-spl/idl-build"
19+
"anchor-spl/idl-build",
2020
]
21+
# whether the owner can recover transactions
22+
owner-recovery = []
2123
# cargo-test-sbf will pass this along
2224
test-sbf = []
2325
# networks

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

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ pub enum NTTError {
5151
OverflowScaledAmount,
5252
#[msg("BitmapIndexOutOfBounds")]
5353
BitmapIndexOutOfBounds,
54+
#[msg("FeatureNotEnabled")]
55+
FeatureNotEnabled,
5456
}
5557

5658
impl From<ScalingError> for NTTError {
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
pub mod admin;
22
pub mod initialize;
3+
pub mod recover;
34
pub mod redeem;
45
pub mod release_inbound;
56
pub mod transfer;
67

78
pub use admin::*;
89
pub use initialize::*;
10+
pub use recover::*;
911
pub use redeem::*;
1012
pub use release_inbound::*;
1113
pub use transfer::*;

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub struct ReleaseInboundArgs {
5252

5353
#[derive(Accounts)]
5454
pub struct ReleaseInboundMint<'info> {
55-
common: ReleaseInbound<'info>,
55+
pub common: ReleaseInbound<'info>,
5656
}
5757

5858
/// Release an inbound transfer and mint the tokens to the recipient.
@@ -103,7 +103,7 @@ pub fn release_inbound_mint(
103103

104104
#[derive(Accounts)]
105105
pub struct ReleaseInboundUnlock<'info> {
106-
common: ReleaseInbound<'info>,
106+
pub common: ReleaseInbound<'info>,
107107

108108
/// CHECK: the token program checks if this indeed the right authority for the mint
109109
#[account(

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

+26
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![feature(type_changing_struct_update)]
2+
13
use anchor_lang::prelude::*;
24

35
// TODO: is there a more elegant way of checking that these 3 features are mutually exclusive?
@@ -73,6 +75,16 @@ pub mod example_native_token_transfers {
7375
instructions::initialize(ctx, args)
7476
}
7577

78+
/// Initialize the recovery account.
79+
/// The recovery flow
80+
pub fn initialize_recovery_account(ctx: Context<InitializeRecoveryAccount>) -> Result<()> {
81+
return instructions::initialize_recovery_account(ctx);
82+
}
83+
84+
pub fn update_recovery_address(ctx: Context<UpdateRecoveryAddress>) -> Result<()> {
85+
instructions::update_recovery_address(ctx)
86+
}
87+
7688
pub fn version(_ctx: Context<Version>) -> Result<String> {
7789
Ok(VERSION.to_string())
7890
}
@@ -106,6 +118,20 @@ pub mod example_native_token_transfers {
106118
instructions::release_inbound_unlock(ctx, args)
107119
}
108120

121+
pub fn recover_unlock<'info>(
122+
ctx: Context<'_, '_, '_, 'info, RecoverUnlock<'info>>,
123+
args: ReleaseInboundArgs,
124+
) -> Result<()> {
125+
instructions::recover_unlock(ctx, args)
126+
}
127+
128+
pub fn recover_mint<'info>(
129+
ctx: Context<'_, '_, '_, 'info, RecoverMint<'info>>,
130+
args: ReleaseInboundArgs,
131+
) -> Result<()> {
132+
instructions::recover_mint(ctx, args)
133+
}
134+
109135
pub fn transfer_ownership(ctx: Context<TransferOwnership>) -> Result<()> {
110136
instructions::transfer_ownership(ctx)
111137
}

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

+111-24
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe("example-native-token-transfers", () => {
5050
});
5151
const user = anchor.web3.Keypair.generate();
5252
let tokenAccount: anchor.web3.PublicKey;
53+
const recoveryTokenAccount = anchor.web3.Keypair.generate();
5354

5455
const mint = anchor.web3.Keypair.generate();
5556

@@ -113,6 +114,16 @@ describe("example-native-token-transfers", () => {
113114
spl.ASSOCIATED_TOKEN_PROGRAM_ID
114115
);
115116

117+
await spl.createAccount(
118+
connection,
119+
payer,
120+
mint.publicKey,
121+
payer.publicKey,
122+
recoveryTokenAccount,
123+
undefined,
124+
spl.TOKEN_2022_PROGRAM_ID
125+
);
126+
116127
await spl.mintTo(
117128
connection,
118129
payer,
@@ -154,6 +165,14 @@ describe("example-native-token-transfers", () => {
154165
});
155166

156167
describe("Locking", () => {
168+
const guardians = new MockGuardians(0, [GUARDIAN_KEY]);
169+
170+
const emitter = new MockEmitter(
171+
Buffer.from("transceiver".padStart(32, "\0")).toString("hex"),
172+
toChainId("ethereum"),
173+
Number(0) // sequence
174+
);
175+
157176
before(async () => {
158177
await spl.setAuthority(
159178
connection,
@@ -173,7 +192,7 @@ describe("example-native-token-transfers", () => {
173192
chain: "solana",
174193
mint: mint.publicKey,
175194
outboundLimit: new BN(1000000),
176-
mode: "locking",
195+
mode: "burning",
177196
});
178197

179198
await ntt.registerTransceiver({
@@ -233,39 +252,18 @@ describe("example-native-token-transfers", () => {
233252
messageData.message.payload
234253
);
235254

236-
// assert theat amount is what we expect
255+
// assert that amount is what we expect
237256
expect(
238257
transceiverMessage.nttManagerPayload.payload.trimmedAmount
239258
).to.deep.equal({ amount: 10000n, decimals: 8 });
240259
// get from balance
241260
const balance = await connection.getTokenAccountBalance(tokenAccount);
242261
expect(balance.value.amount).to.equal("9900000");
243262

244-
// grab logs
245-
// await connection.confirmTransaction(redeemTx, 'confirmed');
246-
// const tx = await anchor.getProvider().connection.getParsedTransaction(redeemTx, {
247-
// commitment: "confirmed",
248-
// });
249-
// console.log(tx);
250-
251-
// const log = tx.meta.logMessages[1];
252-
// const message = log.substring(log.indexOf(':') + 1);
253-
// console.log(message);
254-
255-
// TODO: assert other stuff in the message
256-
// console.log(nttManagerMessage);
257263
expect((await counterValue()).toString()).to.be.eq("1")
258264
});
259265

260266
it("Can receive tokens", async () => {
261-
const emitter = new MockEmitter(
262-
Buffer.from("transceiver".padStart(32, "\0")).toString("hex"),
263-
toChainId("ethereum"),
264-
Number(0) // sequence
265-
);
266-
267-
const guardians = new MockGuardians(0, [GUARDIAN_KEY]);
268-
269267
const sendingTransceiverMessage: WormholeTransceiverMessage<
270268
typeof nttMessageLayout
271269
> = {
@@ -280,7 +278,7 @@ describe("example-native-token-transfers", () => {
280278
sender: new UniversalAddress("FACE".padStart(64, "0")),
281279
payload: {
282280
trimmedAmount: {
283-
amount: 10000n,
281+
amount: 5000n,
284282
decimals: 8,
285283
},
286284
sourceToken: new UniversalAddress("FAFA".padStart(64, "0")),
@@ -315,6 +313,95 @@ describe("example-native-token-transfers", () => {
315313

316314
expect((await counterValue()).toString()).to.be.eq("2")
317315
});
316+
317+
describe("Recovery", () => {
318+
it("Can initialize recovery account", async () => {
319+
await ntt.initializeRecoveryAccount({
320+
payer,
321+
owner: payer,
322+
recoveryTokenAccount: tokenAccount,
323+
});
324+
325+
const recoveryAccount = await ntt.getRecoveryAccount();
326+
327+
expect(recoveryAccount?.toBase58()).to.equal(tokenAccount.toBase58());
328+
});
329+
330+
it("Can update recovery account", async () => {
331+
await ntt.updateRecoveryAddress({
332+
// payer,
333+
owner: payer,
334+
newRecoveryAccount: recoveryTokenAccount.publicKey,
335+
});
336+
337+
const recoveryAccount = await ntt.getRecoveryAccount();
338+
339+
expect(recoveryAccount?.toBase58()).to.equal(
340+
recoveryTokenAccount.publicKey.toBase58()
341+
);
342+
});
343+
344+
it("Owner can recover transfers", async () => {
345+
const sendingTransceiverMessage: WormholeTransceiverMessage<
346+
typeof nttMessageLayout
347+
> = {
348+
sourceNttManager: new UniversalAddress(
349+
encoding.bytes.encode("nttManager".padStart(32, "\0"))
350+
),
351+
recipientNttManager: new UniversalAddress(
352+
ntt.program.programId.toBytes()
353+
),
354+
nttManagerPayload: {
355+
id: encoding.bytes.encode("sequence2".padEnd(32, "0")),
356+
sender: new UniversalAddress("FACE".padStart(64, "0")),
357+
payload: {
358+
trimmedAmount: {
359+
amount: 5000n,
360+
decimals: 8,
361+
},
362+
sourceToken: new UniversalAddress("FAFA".padStart(64, "0")),
363+
recipientAddress: new UniversalAddress(user.publicKey.toBytes()),
364+
recipientChain: "Solana",
365+
},
366+
},
367+
transceiverPayload: { forSpecializedRelayer: false },
368+
} as const;
369+
370+
const serialized = serializePayload(
371+
"Ntt:WormholeTransfer",
372+
sendingTransceiverMessage
373+
);
374+
375+
const published = emitter.publishMessage(
376+
0, // nonce
377+
Buffer.from(serialized),
378+
0 // consistency level
379+
);
380+
381+
const vaaBuf = guardians.addSignatures(published, [0]);
382+
383+
await postVaa(connection, payer, vaaBuf, ntt.wormholeId);
384+
385+
const released = await ntt.redeem({
386+
payer,
387+
vaa: vaaBuf,
388+
recover: payer,
389+
});
390+
391+
expect(released).to.equal(true);
392+
393+
const account = await spl.getAccount(
394+
connection,
395+
recoveryTokenAccount.publicKey,
396+
undefined,
397+
spl.TOKEN_2022_PROGRAM_ID
398+
);
399+
400+
expect(account.amount).to.equal(BigInt(50000));
401+
402+
expect((await counterValue()).toString()).to.be.eq("3")
403+
});
404+
});
318405
});
319406

320407
// describe('Burning', () => {

0 commit comments

Comments
 (0)