From 0b1a20ec940dba8c2c67b0b5655802c7dcd33ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Thu, 29 Aug 2024 00:02:06 +0800 Subject: [PATCH 01/20] Update the latest way to get the seed. replace "init" with "init_if_needed" for vault field in the struct InitializeVault<'info>. delete unnecessary parameters "accounts()" in the test typescript. --- .../courses/program-security/signer-auth.md | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/content/courses/program-security/signer-auth.md b/content/courses/program-security/signer-auth.md index ab0d6a7be..79b41d653 100644 --- a/content/courses/program-security/signer-auth.md +++ b/content/courses/program-security/signer-auth.md @@ -318,7 +318,7 @@ pub mod signer_authorization { pub fn insecure_withdraw(ctx: Context) -> Result<()> { let amount = ctx.accounts.token_account.amount; - let seeds = &[b"vault".as_ref(), &[*ctx.bumps.get("vault").unwrap()]]; + let seeds = &[b"vault".as_ref(), &[ctx.bumps.vault]]; let signer = [&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( @@ -339,7 +339,9 @@ pub mod signer_authorization { #[derive(Accounts)] pub struct InitializeVault<'info> { #[account( - init, + // We use init_if_needed here for the test. Otherwise, the test will result in an error "Already in use" after testing once. + // Use "init" if you want to ensure that the "initialize_vault" function runs only once. + init_if_needed, payer = authority, space = 8 + 32 + 32, seeds = [b"vault"], @@ -402,26 +404,23 @@ account, but we’ll use a different keypair to sign and send the transaction. ```typescript describe("signer-authorization", () => { - ... - it("Insecure withdraw", async () => { + ... + it("Insecure withdraw", async () => { const tx = await program.methods .insecureWithdraw() .accounts({ - vault: vaultPDA, - tokenAccount: tokenAccount.publicKey, withdrawDestination: withdrawDestinationFake, - authority: wallet.publicKey, }) - .transaction() + .transaction(); - await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]) + await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]); const balance = await connection.getTokenAccountBalance( - tokenAccount.publicKey - ) - expect(balance.value.uiAmount).to.eq(0) - }) -}) + tokenAccount.publicKey, + ); + expect(balance.value.uiAmount).to.eq(0); + }); +}); ``` Run `anchor test` to see that both transactions will complete successfully. @@ -461,7 +460,7 @@ pub mod signer_authorization { pub fn secure_withdraw(ctx: Context) -> Result<()> { let amount = ctx.accounts.token_account.amount; - let seeds = &[b"vault".as_ref(), &[*ctx.bumps.get("vault").unwrap()]]; + let seeds = &[b"vault".as_ref(), &[ctx.bumps.vault]]; let signer = [&seeds[..]]; let cpi_ctx = CpiContext::new_with_signer( @@ -514,10 +513,7 @@ describe("signer-authorization", () => { const tx = await program.methods .secureWithdraw() .accounts({ - vault: vaultPDA, - tokenAccount: tokenAccount.publicKey, withdrawDestination: withdrawDestinationFake, - authority: wallet.publicKey, }) .transaction() From 181790e5cc090a6ce071351f9a298c39f1fd93b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Thu, 29 Aug 2024 09:47:34 +0800 Subject: [PATCH 02/20] use BDD style for typescript --- content/courses/program-security/signer-auth.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/courses/program-security/signer-auth.md b/content/courses/program-security/signer-auth.md index 79b41d653..b942e6204 100644 --- a/content/courses/program-security/signer-auth.md +++ b/content/courses/program-security/signer-auth.md @@ -405,7 +405,7 @@ account, but we’ll use a different keypair to sign and send the transaction. ```typescript describe("signer-authorization", () => { ... - it("Insecure withdraw", async () => { + it("insecureWithdraw should be success", async () => { const tx = await program.methods .insecureWithdraw() .accounts({ @@ -508,7 +508,7 @@ transaction to fail the signer check and return an error. ```typescript describe("signer-authorization", () => { ... - it("Secure withdraw", async () => { + it("secureWithdraw should throw an exception", async () => { try { const tx = await program.methods .secureWithdraw() From 971ee06dbb0aec0b220ac9fb58553e7dcb2d8161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Thu, 29 Aug 2024 15:00:08 +0800 Subject: [PATCH 03/20] revoke using init_if_needed --- content/courses/program-security/signer-auth.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/content/courses/program-security/signer-auth.md b/content/courses/program-security/signer-auth.md index b942e6204..4d9674931 100644 --- a/content/courses/program-security/signer-auth.md +++ b/content/courses/program-security/signer-auth.md @@ -339,9 +339,7 @@ pub mod signer_authorization { #[derive(Accounts)] pub struct InitializeVault<'info> { #[account( - // We use init_if_needed here for the test. Otherwise, the test will result in an error "Already in use" after testing once. - // Use "init" if you want to ensure that the "initialize_vault" function runs only once. - init_if_needed, + init, payer = authority, space = 8 + 32 + 32, seeds = [b"vault"], From c4ef60c4a360983cd06f18d23ccbac8dc1986ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Thu, 29 Aug 2024 21:23:36 +0800 Subject: [PATCH 04/20] =?UTF-8?q?Use=20"@coral-xyz/anchor"=20instead=20of?= =?UTF-8?q?=20"@project-serum/anchor"=20for=20the=20test=20typescript.=20C?= =?UTF-8?q?onsistently=C2=A0use=20"rpc()"=20in=20the=20test=20typescript.?= =?UTF-8?q?=20Use=C2=A0InitSpace=C2=A0to=20calculate=20space=20needed=20fo?= =?UTF-8?q?r=20accounts.=20Update=20BDD=20style=20for=20the=20test=20types?= =?UTF-8?q?cript.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reinitialization-attacks.md | 84 ++++++++++--------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/content/courses/program-security/reinitialization-attacks.md b/content/courses/program-security/reinitialization-attacks.md index ae148a74a..dc649af00 100644 --- a/content/courses/program-security/reinitialization-attacks.md +++ b/content/courses/program-security/reinitialization-attacks.md @@ -55,7 +55,6 @@ existing `authority` stored on the account data. ```rust use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -79,7 +78,8 @@ pub struct Initialize<'info> { authority: Signer<'info>, } -#[derive(BorshSerialize, BorshDeserialize)] +#[account] +#[derive(InitSpace)] pub struct User { authority: Pubkey, } @@ -105,7 +105,6 @@ authority with their own public key. ```rust use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -135,7 +134,7 @@ pub struct Initialize<'info> { authority: Signer<'info>, } -#[derive(BorshSerialize, BorshDeserialize)] + pub struct User { is_initialized: bool, authority: Pubkey, @@ -177,7 +176,7 @@ pub mod initialization_recommended { #[derive(Accounts)] pub struct Initialize<'info> { - #[account(init, payer = authority, space = 8+32)] + #[account(init, payer = authority, space = DISCRIMINATOR_SIZE + User::INIT_SPACE)] user: Account<'info, User>, #[account(mut)] authority: Signer<'info>, @@ -185,6 +184,7 @@ pub struct Initialize<'info> { } #[account] +#[derive(InitSpace)] pub struct User { authority: Pubkey, } @@ -236,7 +236,6 @@ a second time to override the `authority` stored on an existing `user` account. ```rust use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -260,7 +259,8 @@ pub struct Unchecked<'info> { authority: Signer<'info>, } -#[derive(BorshSerialize, BorshDeserialize)] +#[account] +#[derive(InitSpace)] pub struct User { authority: Pubkey, } @@ -291,7 +291,6 @@ describe("initialization", () => { const wallet = anchor.workspace.Initialization.provider.wallet; const walletTwo = anchor.web3.Keypair.generate(); - const userInsecure = anchor.web3.Keypair.generate(); const userRecommended = anchor.web3.Keypair.generate(); @@ -312,35 +311,43 @@ describe("initialization", () => { userInsecure, ]); + const airdropSignature = await provider.connection.requestAirdrop( + walletTwo.publicKey, + 1 * anchor.web3.LAMPORTS_PER_SOL, + ); + + const latestBlockHash = await provider.connection.getLatestBlockhash(); + await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop( - walletTwo.publicKey, - 1 * anchor.web3.LAMPORTS_PER_SOL, - ), + { + blockhash: latestBlockHash.blockhash, + lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, + signature: airdropSignature, + }, "confirmed", ); }); - it("Insecure init", async () => { + it("insecureInitialization should be successful", async () => { await program.methods .insecureInitialization() .accounts({ user: userInsecure.publicKey, + authority: wallet.publicKey, }) + .signers([wallet.payer]) .rpc(); }); - it("Re-invoke insecure init with different auth", async () => { - const tx = await program.methods + it("insecureInitialization with a different authority should be successful again", async () => { + await program.methods .insecureInitialization() .accounts({ user: userInsecure.publicKey, authority: walletTwo.publicKey, }) - .transaction(); - await anchor.web3.sendAndConfirmTransaction(provider.connection, tx, [ - walletTwo, - ]); + .signers([walletTwo]) + .rpc(); }); }); ``` @@ -349,8 +356,8 @@ Run `anchor test` to see that both transactions will complete successfully. ```bash initialization - ✔ Insecure init (478ms) - ✔ Re-invoke insecure init with different auth (464ms) + ✔ insecureInitialization should be successful (417ms) + ✔ insecureInitialization with a different authority should be successful again (417ms) ``` #### 3. Add `recommended_initialization` instruction @@ -371,10 +378,11 @@ state. ```rust use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); +const DISCRIMINATOR_SIZE:usize = 8; + #[program] pub mod initialization { use super::*; @@ -387,11 +395,10 @@ pub mod initialization { #[derive(Accounts)] pub struct Checked<'info> { - #[account(init, payer = authority, space = 8+32)] + #[account(init, payer = authority, space = DISCRIMINATOR_SIZE + User::INIT_SPACE)] user: Account<'info, User>, #[account(mut)] - authority: Signer<'info>, - system_program: Program<'info, System>, + authority: Signer<'info> } ``` @@ -404,36 +411,33 @@ when we try to initialize the same account a second time. ```typescript describe("initialization", () => { ... - it("Recommended init", async () => { - await program.methods +it("recommendedInitialization should be successful", async () => { + const tx = await program.methods .recommendedInitialization() .accounts({ user: userRecommended.publicKey, + authority: wallet.publicKey, }) .signers([userRecommended]) - .rpc() - }) + .rpc(); + }); - it("Re-invoke recommended init with different auth, expect error", async () => { + it("recommendedInitialization with a different authority should throw an expection", async () => { try { - // Add your test here. const tx = await program.methods .recommendedInitialization() .accounts({ user: userRecommended.publicKey, authority: walletTwo.publicKey, }) - .transaction() - await anchor.web3.sendAndConfirmTransaction(provider.connection, tx, [ - walletTwo, - userRecommended, - ]) + .signers([userRecommended, walletTwo]) + .rpc(); } catch (err) { - expect(err) - console.log(err) + expect(err); + console.log(err); } - }) -}) + }); +}); ``` Run `anchor test` and to see that the second transaction which tries to From 170594dbf927c09160f90642579205ad3fe2f9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Fri, 30 Aug 2024 21:53:09 +0800 Subject: [PATCH 05/20] =?UTF-8?q?Use=C2=A0InitSpace=20to=20calculate=20spa?= =?UTF-8?q?ce=20needed=20for=20accounts.=20change=20"token=20account"=20in?= =?UTF-8?q?=20the=20"Starter"=20section=20to=20"associated=20token=20accou?= =?UTF-8?q?nt"=20change=20"token=20account"=20in=20the=20"Add=20`initializ?= =?UTF-8?q?e=5Fpool=5Fsecure"=20section=20to=20"associated=20token=20accou?= =?UTF-8?q?nt"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../courses/program-security/pda-sharing.md | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/content/courses/program-security/pda-sharing.md b/content/courses/program-security/pda-sharing.md index 3e9c77201..2cdef7410 100644 --- a/content/courses/program-security/pda-sharing.md +++ b/content/courses/program-security/pda-sharing.md @@ -63,7 +63,8 @@ pub struct WithdrawTokens<'info> { pool: Account<'info, TokenPool>, vault: Account<'info, TokenAccount>, withdraw_destination: Account<'info, TokenAccount>, - authority: AccountInfo<'info>, + /// CHECK: PDA + authority: UncheckedAccount<'info>, token_program: Program<'info, Token>, } @@ -124,7 +125,8 @@ pub struct WithdrawTokens<'info> { pool: Account<'info, TokenPool>, vault: Account<'info, TokenAccount>, withdraw_destination: Account<'info, TokenAccount>, - authority: AccountInfo<'info>, + /// CHECK: PDA + authority: UncheckedAccount<'info>, token_program: Program<'info, Token>, } @@ -236,8 +238,9 @@ The starter code includes a program with two instructions and the boilerplate setup for the test file. The `initialize_pool` instruction initializes a new `TokenPool` that stores a -`vault`, `mint`, `withdraw_destination`, and `bump`. The `vault` is a token -account where the authority is set as a PDA derived using the `mint` address. +`vault`, `mint`, `withdraw_destination`, and `bump`. The `vault` is a associated +token account where the authority is set as a PDA derived using the `mint` +address. The `withdraw_insecure` instruction will transfer tokens in the `vault` token account to a `withdraw_destination` token account. @@ -271,13 +274,10 @@ it("Insecure initialize allows pool to be initialized with wrong vault", async ( mint: mint, vault: vaultInsecure.address, withdrawDestination: withdrawDestinationFake, - payer: walletFake.publicKey, }) - .signers([walletFake, poolInsecureFake]) + .signers([poolInsecureFake]) .rpc(); - await new Promise(x => setTimeout(x, 1000)); - await spl.mintTo( connection, wallet.payer, @@ -288,24 +288,21 @@ it("Insecure initialize allows pool to be initialized with wrong vault", async ( ); const account = await spl.getAccount(connection, vaultInsecure.address); - expect(Number(account.amount)).to.equal(100); + expect(account.amount).eq(100n); }); -it("Insecure withdraw allows stealing from vault", async () => { +it("Insecure withdraw allows withdraw to wrong destination", async () => { await program.methods .withdrawInsecure() .accounts({ pool: poolInsecureFake.publicKey, - vault: vaultInsecure.address, - withdrawDestination: withdrawDestinationFake, authority: authInsecure, - signer: walletFake.publicKey, }) - .signers([walletFake]) .rpc(); const account = await spl.getAccount(connection, vaultInsecure.address); - expect(Number(account.amount)).to.equal(0); + + expect(account.amount).eq(0n); }); ``` @@ -319,7 +316,7 @@ Now let's add a new instruction to the program for securely initializing a pool. This new `initialize_pool_secure` instruction will initialize a `pool` account as a PDA derived using the `withdraw_destination`. It will also initialize a -`vault` token account with the authority set as the `pool` PDA. +`vault` associated token account with the authority set as the `pool` PDA. ```rust pub fn initialize_pool_secure(ctx: Context) -> Result<()> { @@ -337,7 +334,7 @@ pub struct InitializePoolSecure<'info> { #[account( init, payer = payer, - space = 8 + 32 + 32 + 32 + 1, + space = DISCRIMINATOR_SIZE + TokenPool::INIT_SPACE, seeds = [withdraw_destination.key().as_ref()], bump )] @@ -376,7 +373,6 @@ pub fn withdraw_secure(ctx: Context) -> Result<()> { ]; token::transfer(ctx.accounts.transfer_ctx().with_signer(&[seeds]), amount) } - ... #[derive(Accounts)] @@ -420,7 +416,7 @@ expected: ```typescript it("Secure pool initialization and withdraw works", async () => { - const withdrawDestinationAccount = await getAccount( + const withdrawDestinationAccount = await spl.getAccount( provider.connection, withdrawDestination, ); @@ -428,7 +424,6 @@ it("Secure pool initialization and withdraw works", async () => { await program.methods .initializePoolSecure() .accounts({ - pool: authSecure, mint: mint, vault: vaultRecommended.publicKey, withdrawDestination: withdrawDestination, @@ -450,20 +445,17 @@ it("Secure pool initialization and withdraw works", async () => { await program.methods .withdrawSecure() .accounts({ - pool: authSecure, vault: vaultRecommended.publicKey, withdrawDestination: withdrawDestination, }) .rpc(); - const afterAccount = await getAccount( + const afterAccount = await spl.getAccount( provider.connection, withdrawDestination, ); - expect( - Number(afterAccount.amount) - Number(withdrawDestinationAccount.amount), - ).to.equal(100); + expect(afterAccount.amount - withdrawDestinationAccount.amount).eq(100n); }); ``` @@ -481,11 +473,10 @@ it("Secure withdraw doesn't allow withdraw to wrong destination", async () => { await program.methods .withdrawSecure() .accounts({ - pool: authSecure, vault: vaultRecommended.publicKey, withdrawDestination: withdrawDestinationFake, }) - .signers([walletFake]) + .signers([vaultRecommended]) .rpc(); assert.fail("expected error"); @@ -508,7 +499,6 @@ it("Secure pool initialization doesn't allow wrong vault", async () => { await program.methods .initializePoolSecure() .accounts({ - pool: authSecure, mint: mint, vault: vaultInsecure.address, withdrawDestination: withdrawDestination, From 198c23a21e38a3eb9197332aa4f598f5814d0c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Fri, 30 Aug 2024 22:36:07 +0800 Subject: [PATCH 06/20] synced to the new repo --- .../courses/program-security/signer-auth.md | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/content/courses/program-security/signer-auth.md b/content/courses/program-security/signer-auth.md index 4d9674931..c5b480cbe 100644 --- a/content/courses/program-security/signer-auth.md +++ b/content/courses/program-security/signer-auth.md @@ -275,7 +275,7 @@ a missing signer check could allow the vault to be drained. #### 1. Starter To get started, download the starter code from the `starter` branch of -[this repository](https://github.com/Unboxed-Software/solana-signer-auth/tree/starter). The +[this repository](https://github.com/solana-developers/solana-signer-auth/tree/starter). The starter code includes a program with two instructions and the boilerplate setup for the test file. @@ -403,7 +403,7 @@ account, but we’ll use a different keypair to sign and send the transaction. ```typescript describe("signer-authorization", () => { ... - it("insecureWithdraw should be success", async () => { + it("Insecure withdraw should be successful", async () => { const tx = await program.methods .insecureWithdraw() .accounts({ @@ -414,10 +414,9 @@ describe("signer-authorization", () => { await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]); const balance = await connection.getTokenAccountBalance( - tokenAccount.publicKey, + tokenAccount.publicKey ); expect(balance.value.uiAmount).to.eq(0); - }); }); ``` @@ -425,8 +424,8 @@ Run `anchor test` to see that both transactions will complete successfully. ```bash signer-authorization - ✔ Initialize Vault (810ms) - ✔ Insecure withdraw (405ms) + ✔ Initialize Vault should be successful (810ms) + ✔ Insecure withdraw should be successful (405ms) ``` Since there is no signer check for the `authority` account, the @@ -505,23 +504,23 @@ transaction to fail the signer check and return an error. ```typescript describe("signer-authorization", () => { - ... - it("secureWithdraw should throw an exception", async () => { + ... + it("Secure withdraw should throw an exception", async () => { try { const tx = await program.methods .secureWithdraw() .accounts({ withdrawDestination: withdrawDestinationFake, }) - .transaction() + .transaction(); - await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]) + await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]); } catch (err) { - expect(err) - console.log(err) + expect(err); + console.log(err); } - }) -}) + }); +}); ``` Run `anchor test` to see that the transaction will now return a signature @@ -537,7 +536,7 @@ instructions and make sure that each is a signer on the transaction. If you want to take a look at the final solution code you can find it on the `solution` branch of -[the repository](https://github.com/Unboxed-Software/solana-signer-auth/tree/solution). +[the repository](https://github.com/solana-developers/solana-signer-auth//tree/solution). ## Challenge From b7b31ea503a52dbddf514fc5009e5244452a14e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Sat, 31 Aug 2024 11:52:15 +0800 Subject: [PATCH 07/20] =?UTF-8?q?Update=20anchor-lang=20and=20anchor-spl?= =?UTF-8?q?=20to=20the=20latest=20version.=20Use=C2=A0InitSpace=20to=20cal?= =?UTF-8?q?culate=20space=20needed=20for=20accounts.=20Use=20the=20latest?= =?UTF-8?q?=20"connection.confirmTransaction()"=20Delete=20Unnecessary=20p?= =?UTF-8?q?arameters=20found=20in=20the=20test=20typescript.=20Consistentl?= =?UTF-8?q?y=20use=20"rpc()"=20as=20sending=20transactions=20in=20the=20te?= =?UTF-8?q?st=20typescript.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../courses/program-security/owner-checks.md | 133 +++++++++--------- 1 file changed, 65 insertions(+), 68 deletions(-) diff --git a/content/courses/program-security/owner-checks.md b/content/courses/program-security/owner-checks.md index 1b95f7281..70040c2b2 100644 --- a/content/courses/program-security/owner-checks.md +++ b/content/courses/program-security/owner-checks.md @@ -314,7 +314,9 @@ The `owner_check` program includes two instructions: use anchor_lang::prelude::*; use anchor_spl::token::{self, Mint, Token, TokenAccount}; -declare_id!("HQYNznB3XTqxzuEqqKMAD9XkYE5BGrnv8xmkoDNcqHYB"); +declare_id!("2JzQxdnKh6RXwACK6desuPrsbk6Yd3ky4UHAPXQFFC9w"); + +const DISCRIMINATOR_SIZE: usize = 8; #[program] pub mod owner_check { @@ -339,7 +341,7 @@ pub mod owner_check { let seeds = &[ b"token".as_ref(), - &[*ctx.bumps.get("token_account").unwrap()], + &[ctx.bump.stoken_account], ]; let signer = [&seeds[..]]; @@ -363,7 +365,7 @@ pub struct InitializeVault<'info> { #[account( init, payer = authority, - space = 8 + 32 + 32, + space = DISCRIMINATOR_SIZE + Vault::INIT_SPACE, )] pub vault: Account<'info, Vault>, #[account( @@ -400,6 +402,7 @@ pub struct InsecureWithdraw<'info> { } #[account] +#[derive(InitSpace)] pub struct Vault { token_account: Pubkey, authority: Pubkey, @@ -416,7 +419,9 @@ The `clone` program includes a single instruction: use anchor_lang::prelude::*; use anchor_spl::token::TokenAccount; -declare_id!("DUN7nniuatsMC7ReCh5eJRQExnutppN1tAfjfXFmGDq3"); +declare_id!("7967qGRBusM49RUdBiFU8s9YY3fSwA9rxCSELXgk8Tk1"); + +const DISCRIMINATOR_SIZE: usize = 8; #[program] pub mod clone { @@ -434,7 +439,7 @@ pub struct InitializeVault<'info> { #[account( init, payer = authority, - space = 8 + 32 + 32, + space = DISCRIMINATOR_SIZE + Vault::INIT_SPACE, )] pub vault: Account<'info, Vault>, pub token_account: Account<'info, TokenAccount>, @@ -444,6 +449,7 @@ pub struct InitializeVault<'info> { } #[account] +#[derive(InitSpace)] pub struct Vault { token_account: Pubkey, authority: Pubkey, @@ -471,33 +477,31 @@ authority. The tokens from the `tokenPDA` account will then be withdrawn to the ```typescript describe("owner-check", () => { ... - it("Insecure withdraw", async () => { + it("Insecure withdraw should be successful", async () => { const tx = await program.methods - .insecureWithdraw() - .accounts({ - vault: vaultClone.publicKey, - tokenAccount: tokenPDA, - withdrawDestination: withdrawDestinationFake, - authority: walletFake.publicKey, - }) - .transaction() - - await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]) - - const balance = await connection.getTokenAccountBalance(tokenPDA) - expect(balance.value.uiAmount).to.eq(0) - }) - -}) + .insecureWithdraw() + .accounts({ + vault: vaultClone.publicKey, + withdrawDestination: withdrawDestinationFake, + authority: walletFake.publicKey, + }) + .signers([walletFake]) + .rpc(); + + const balance = await connection.getTokenAccountBalance(tokenPDA); + expect(balance.value.uiAmount).to.eq(0); + }); + +}); ``` Run `anchor test` to see that the `insecure_withdraw` completes successfully. ```bash owner-check - ✔ Initialize Vault (808ms) - ✔ Initialize Fake Vault (404ms) - ✔ Insecure withdraw (409ms) + ✔ Initialize Vault should be successful (808ms) + ✔ Initialize Fake Vault should be successful (404ms) + ✔ Insecure withdraw should be successful (409ms) ``` Note that `vaultClone` deserializes successfully even though Anchor @@ -507,6 +511,7 @@ discriminator is a hash of the account type name. ```rust #[account] +#[derive(InitSpace)] pub struct Vault { token_account: Pubkey, authority: Pubkey, @@ -540,7 +545,7 @@ pub mod owner_check { let seeds = &[ b"token".as_ref(), - &[*ctx.bumps.get("token_account").unwrap()], + &[ctx.bumps.token_account], ]; let signer = [&seeds[..]]; @@ -590,49 +595,41 @@ account to check that the instruction works as intended. ```typescript describe("owner-check", () => { ... - it("Secure withdraw, expect error", async () => { - try { - const tx = await program.methods - .secureWithdraw() - .accounts({ - vault: vaultClone.publicKey, - tokenAccount: tokenPDA, - withdrawDestination: withdrawDestinationFake, - authority: walletFake.publicKey, - }) - .transaction() - - await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]) - } catch (err) { - expect(err) - console.log(err) - } - }) - - it("Secure withdraw", async () => { - await spl.mintTo( - connection, - wallet.payer, - mint, - tokenPDA, - wallet.payer, - 100 - ) - - await program.methods + it("Secure withdraw should throw an error", async () => { + try { + await program.methods .secureWithdraw() .accounts({ - vault: vault.publicKey, - tokenAccount: tokenPDA, - withdrawDestination: withdrawDestination, - authority: wallet.publicKey, + vault: vaultClone.publicKey, + withdrawDestination: withdrawDestinationFake, }) - .rpc() - - const balance = await connection.getTokenAccountBalance(tokenPDA) - expect(balance.value.uiAmount).to.eq(0) - }) -}) + .rpc(); + } catch (err) { + expect(err); + console.log(err); + } + }); + + it("Secure withdraw should be successful", async () => { + await spl.mintTo( + connection, + wallet.payer, + mint, + tokenPDA, + wallet.payer, + 100 + ); + await program.methods + .secureWithdraw() + .accounts({ + vault: vault.publicKey, + withdrawDestination: withdrawDestination, + }) + .rpc(); + const balance = await connection.getTokenAccountBalance(tokenPDA); + expect(balance.value.uiAmount).to.eq(0); + }); +}); ``` Run `anchor test` to see that the transaction using the `vaultClone` account @@ -658,8 +655,8 @@ of the logs above say `AnchorError caused by account: vault`). This can be very helpful when debugging. ```bash -✔ Secure withdraw, expect error (78ms) -✔ Secure withdraw (10063ms) +✔ Secure withdraw should throw an error (78ms) +✔ Secure withdraw should be successful (10063ms) ``` That’s all you need to ensure you check the owner on an account! Like some other From b7ddc3900ce9c79f05359170aa7de0b9f4bfb915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Sun, 1 Sep 2024 16:35:39 +0800 Subject: [PATCH 08/20] =?UTF-8?q?Use=C2=A0InitSpace=20to=20calculate=20spa?= =?UTF-8?q?ce=20needed=20for=20accounts.=20Replace=20"BorshDeserialize"=20?= =?UTF-8?q?and=20"BorshSerialize"=20with=20"AnchorDeserialize"=20and=20"An?= =?UTF-8?q?chorSerialize".=20Make=20test=20descriptions=20more=20clear.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../duplicate-mutable-accounts.md | 69 ++++++++++--------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/content/courses/program-security/duplicate-mutable-accounts.md b/content/courses/program-security/duplicate-mutable-accounts.md index 39ee8c079..a1329453b 100644 --- a/content/courses/program-security/duplicate-mutable-accounts.md +++ b/content/courses/program-security/duplicate-mutable-accounts.md @@ -217,10 +217,11 @@ accounts in the instruction. ```rust use anchor_lang::prelude::*; -use borsh::{BorshDeserialize, BorshSerialize}; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); +const DISCRIMINATOR_SIZE: usize = 8; + #[program] pub mod duplicate_mutable_accounts { use super::*; @@ -248,7 +249,7 @@ pub struct Initialize<'info> { #[account( init, payer = payer, - space = 8 + 32 + 8 + space = DISCRIMINATOR_SIZE + PlayerState::INIT_SPACE )] pub new_player: Account<'info, PlayerState>, #[account(mut)] @@ -265,12 +266,14 @@ pub struct RockPaperScissorsInsecure<'info> { } #[account] +#[derive(InitSpace)] pub struct PlayerState { player: Pubkey, choice: Option, } -#[derive(Clone, Copy, BorshDeserialize, BorshSerialize)] +#[derive(Clone, Copy, AnchorDeserialize, AnchorSerialize)] +#[derive(InitSpace)] pub enum RockPaperScissors { Rock, Paper, @@ -289,7 +292,7 @@ passing in the `playerOne.publicKey` for as both `playerOne` and `playerTwo`. ```typescript describe("duplicate-mutable-accounts", () => { ... - it("Invoke insecure instruction", async () => { + it("Invoke insecure instruction with the same player should be successful", async () => { await program.methods .rockPaperScissorsShootInsecure({ rock: {} }, { scissors: {} }) .accounts({ @@ -313,9 +316,9 @@ incorrectly as `scissors`. ```bash duplicate-mutable-accounts - ✔ Initialized Player One (461ms) - ✔ Initialized Player Two (404ms) - ✔ Invoke insecure instruction (406ms) + ✔ Initialized Player One should be successful (461ms) + ✔ Initialized Player Two should be successful (404ms) + ✔ Invoke insecure instruction with the same player should be successful (406ms) ``` Not only does allowing duplicate accounts not make a whole lot of sense for the @@ -371,35 +374,35 @@ which we expect to fail. ```typescript describe("duplicate-mutable-accounts", () => { ... - it("Invoke secure instruction", async () => { - await program.methods + it("Invoke secure instruction with different players should be successful", async () => { + await program.methods + .rockPaperScissorsShootSecure({ rock: {} }, { scissors: {} }) + .accounts({ + playerOne: playerOne.publicKey, + playerTwo: playerTwo.publicKey, + }) + .rpc(); + + const p1 = await program.account.playerState.fetch(playerOne.publicKey); + const p2 = await program.account.playerState.fetch(playerTwo.publicKey); + assert.equal(JSON.stringify(p1.choice), JSON.stringify({ rock: {} })); + assert.equal(JSON.stringify(p2.choice), JSON.stringify({ scissors: {} })); + }); + + it("Invoke secure instruction with the same player should throw an expection", async () => { + try { + await program.methods .rockPaperScissorsShootSecure({ rock: {} }, { scissors: {} }) .accounts({ - playerOne: playerOne.publicKey, - playerTwo: playerTwo.publicKey, + playerOne: playerOne.publicKey, + playerTwo: playerOne.publicKey, }) - .rpc() - - const p1 = await program.account.playerState.fetch(playerOne.publicKey) - const p2 = await program.account.playerState.fetch(playerTwo.publicKey) - assert.equal(JSON.stringify(p1.choice), JSON.stringify({ rock: {} })) - assert.equal(JSON.stringify(p2.choice), JSON.stringify({ scissors: {} })) - }) - - it("Invoke secure instruction - expect error", async () => { - try { - await program.methods - .rockPaperScissorsShootSecure({ rock: {} }, { scissors: {} }) - .accounts({ - playerOne: playerOne.publicKey, - playerTwo: playerOne.publicKey, - }) - .rpc() - } catch (err) { - expect(err) - console.log(err) - } - }) + .rpc(); + } catch (err) { + expect(err); + console.log(err); + } + }); }) ``` From 262a09a4c700493b41d2273a378e1acb6afdde80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Mon, 2 Sep 2024 20:48:02 +0800 Subject: [PATCH 09/20] =?UTF-8?q?Use=C2=A0InitSpace=20to=20calculate=20spa?= =?UTF-8?q?ce=20needed=20for=20accounts.=20Delete=20the=20"Secure=20accoun?= =?UTF-8?q?t=20closing=20section"=20section=20because=20=E2=80=9CCLOSED=5F?= =?UTF-8?q?ACCOUNT=5FDISCRIMINATOR=E2=80=9D=20was=20removed=20in=20the=20l?= =?UTF-8?q?atest=20version=20of=20anchor-lang.=20Delete=20"force=5Fdefund"?= =?UTF-8?q?=20=C2=A0because=20=E2=80=9CCLOSED=5FACCOUNT=5FDISCRIMINATOR?= =?UTF-8?q?=E2=80=9D=20was=20removed=20in=20the=20latest=20version=20of=20?= =?UTF-8?q?anchor-lang.=20Add=20the=20new=20secure=20instruction=20of=20ac?= =?UTF-8?q?count=20closing.=20Delete=20Unnecessary=20parameters=20found=20?= =?UTF-8?q?in=20the=20test=20typescript.=20Change=20the=20Logic=20of=20clo?= =?UTF-8?q?sing=20account=20"Sets=20the=20account=20discriminator=20to=20t?= =?UTF-8?q?he=20`CLOSED=5FACCOUNT=5FDISCRIMINATOR`=20variant"=20to=20"Assi?= =?UTF-8?q?gning=20the=20owner=20to=20the=20System=20Program"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../program-security/closing-accounts.md | 170 +++++------------- 1 file changed, 45 insertions(+), 125 deletions(-) diff --git a/content/courses/program-security/closing-accounts.md b/content/courses/program-security/closing-accounts.md index 2f62f0c9c..19443fd95 100644 --- a/content/courses/program-security/closing-accounts.md +++ b/content/courses/program-security/closing-accounts.md @@ -18,8 +18,7 @@ description: exempt. Closing accounts involves transferring the lamports stored in the account for rent exemption to another account of your choosing. - You can use the Anchor `#[account(close = )]` - constraint to securely close accounts and set the account discriminator to the - `CLOSED_ACCOUNT_DISCRIMINATOR` + constraint to securely close accounts. ```rust #[account(mut, close = receiver)] pub data_account: Account<'info, MyData>, @@ -96,58 +95,39 @@ the program and even drain a protocol. ### Secure account closing -The two most important things you can do to close this loophole are to zero out -the account data and add an account discriminator that represents the account -has been closed. You need _both_ of these things to avoid unintended program -behavior. - -An account with zeroed out data can still be used for some things, especially if -it's a PDA whose address derivation is used within the program for verification -purposes. However, the damage may be potentially limited if the attacker can't -access the previously-stored data. - -To further secure the program, however, closed accounts should be given an -account discriminator that designates it as "closed," and all instructions -should perform checks on all passed-in accounts that return an error if the -account is marked closed. +The two most important things you can do to close this loophole are to change +the account's ownership and reallocate the size of the account's data with 0 +bytes. Look at the example below. This program transfers the lamports out of an -account, zeroes out the account data, and sets an account discriminator in a -single instruction in hopes of preventing a subsequent instruction from -utilizing this account again before it has been garbage collected. Failing to do -any one of these things would result in a security vulnerability. +account, changes the account's ownership to the system program, and reallocates +the size of the account's data with 0 bytes in hopes of preventing a subsequent +instruction from utilizing th is account again before it has been garbage +collected. Failing to do any one of these things would result in a security +vulnerability. ```rust use anchor_lang::prelude::*; -use std::io::Write; -use std::ops::DerefMut; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] -pub mod closing_accounts_insecure_still_still { +pub mod closing_accounts_insecure { use super::*; + use anchor_lang::solana_program::system_program; - pub fn close(ctx: Context) -> ProgramResult { - let account = ctx.accounts.account.to_account_info(); + pub fn close(ctx: Context) -> ProgramResult { let dest_starting_lamports = ctx.accounts.destination.lamports(); + let account_to_close = tx.accounts.lottery_entry.to_account_info(); **ctx.accounts.destination.lamports.borrow_mut() = dest_starting_lamports - .checked_add(account.lamports()) + .checked_add(account_to_close.lamports()) .unwrap(); - **account.lamports.borrow_mut() = 0; + **account_to_close.lamports.borrow_mut() = 0; - let mut data = account.try_borrow_mut_data()?; - for byte in data.deref_mut().iter_mut() { - *byte = 0; - } - - let dst: &mut [u8] = &mut data; - let mut cursor = std::io::Cursor::new(dst); - cursor - .write_all(&anchor_lang::__private::CLOSED_ACCOUNT_DISCRIMINATOR) - .unwrap(); + account_to_close.assign(&system_program::ID); + account_to_close.realloc(0, false)?; Ok(()) } @@ -155,7 +135,7 @@ pub mod closing_accounts_insecure_still_still { #[derive(Accounts)] pub struct Close<'info> { - account: Account<'info, Data>, + account_to_close: Account<'info, Data>, destination: AccountInfo<'info>, } @@ -165,73 +145,6 @@ pub struct Data { } ``` -Note that the example above is using Anchor's `CLOSED_ACCOUNT_DISCRIMINATOR`. -This is simply an account discriminator where each byte is `255`. The -discriminator doesn't have any inherent meaning, but if you couple it with -account validation checks that return errors any time an account with this -discriminator is passed to an instruction, you'll stop your program from -unintentionally processing an instruction with a closed account. - -#### Manual Force Defund - -There is still one small issue. While the practice of zeroing out account data -and adding a "closed" account discriminator will stop your program from being -exploited, a user can still keep an account from being garbage collected by -refunding the account's lamports before the end of an instruction. This results -in one or potentially many accounts existing in a limbo state where they cannot -be used but also cannot be garbage collected. - -To handle this edge case, you may consider adding an instruction that will allow -_anyone_ to defund accounts tagged with the "closed" account discriminator. The -only account validation this instruction would perform is to ensure that the -account being defunded is marked as closed. It may look something like this: - -```rust -use anchor_lang::__private::CLOSED_ACCOUNT_DISCRIMINATOR; -use anchor_lang::prelude::*; -use std::io::{Cursor, Write}; -use std::ops::DerefMut; - -... - - pub fn force_defund(ctx: Context) -> ProgramResult { - let account = &ctx.accounts.account; - - let data = account.try_borrow_data()?; - assert!(data.len() > 8); - - let mut discriminator = [0u8; 8]; - discriminator.copy_from_slice(&data[0..8]); - if discriminator != CLOSED_ACCOUNT_DISCRIMINATOR { - return Err(ProgramError::InvalidAccountData); - } - - let dest_starting_lamports = ctx.accounts.destination.lamports(); - - **ctx.accounts.destination.lamports.borrow_mut() = dest_starting_lamports - .checked_add(account.lamports()) - .unwrap(); - **account.lamports.borrow_mut() = 0; - - Ok(()) - } - -... - -#[derive(Accounts)] -pub struct ForceDefund<'info> { - account: AccountInfo<'info>, - destination: AccountInfo<'info>, -} -``` - -Since anyone can call this instruction, this can act as a deterrent to attempted -revival attacks since the attacker is paying for account rent exemption but -anyone else can claim the lamports in a refunded account for themselves. - -While not necessary, this can help eliminate the waste of space and lamports -associated with these "limbo" accounts. - ### Use the Anchor `close` constraint Fortunately, Anchor makes all of this much simpler with the @@ -240,7 +153,8 @@ everything required to securely close an account: 1. Transfers the account’s lamports to the given `` 2. Zeroes out the account data -3. Sets the account discriminator to the `CLOSED_ACCOUNT_DISCRIMINATOR` variant +3. Assigning the owner of the account to the System Program and rellocating the + size of the account with 0 bytes. All you have to do is add it in the account validation struct to the account you want closed: @@ -258,9 +172,6 @@ pub struct CloseAccount { } ``` -The `force_defund` instruction is an optional addition that you’ll have to -implement on your own if you’d like to utilize it. - ## Lab To clarify how an attacker might take advantage of a revival attack, let's work @@ -316,22 +227,27 @@ alive even after claiming rewards and then claim rewards again. That test looks like this: ```typescript -it("attacker can close + refund lottery acct + claim multiple rewards", async () => { +it("attacker can close + refund lottery acct + claim multiple rewards successfully", async () => { + const [attackerLotteryEntry, bump] = PublicKey.findProgramAddressSync( + [Buffer.from("test-seed"), authority.publicKey.toBuffer()], + program.programId, + ); // claim multiple times for (let i = 0; i < 2; i++) { + let tokenAcct = await getAccount(provider.connection, attackerAta); + const tx = new Transaction(); + // instruction claims rewards, program will try to close account tx.add( await program.methods .redeemWinningsInsecure() .accounts({ - lotteryEntry: attackerLotteryEntry, - user: attacker.publicKey, userAta: attackerAta, rewardMint: rewardMint, - mintAuth: mintAuth, - tokenProgram: TOKEN_PROGRAM_ID, + user: authority.publicKey, }) + .signers([authority]) .instruction(), ); @@ -343,21 +259,22 @@ it("attacker can close + refund lottery acct + claim multiple rewards", async ( ); tx.add( SystemProgram.transfer({ - fromPubkey: attacker.publicKey, + fromPubkey: authority.publicKey, toPubkey: attackerLotteryEntry, lamports: rentExemptLamports, }), ); // send tx - await sendAndConfirmTransaction(provider.connection, tx, [attacker]); + await sendAndConfirmTransaction(provider.connection, tx, [authority]); await new Promise(x => setTimeout(x, 5000)); } - const ata = await getAccount(provider.connection, attackerAta); + const tokenAcct = await getAccount(provider.connection, attackerAta); + const lotteryEntry = await program.account.lotteryAccount.fetch(attackerLotteryEntry); - expect(Number(ata.amount)).to.equal( + expect(Number(tokenAcct.amount)).to.equal( lotteryEntry.timestamp.toNumber() * 10 * 2, ); }); @@ -390,7 +307,7 @@ pub struct RedeemWinningsSecure<'info> { // program expects this account to be initialized #[account( mut, - seeds = [user.key().as_ref()], + seeds = [DATA_PDA_SEED.as_bytes(),user.key.as_ref()], bump = lottery_entry.bump, has_one = user, close = user @@ -533,16 +450,19 @@ like this: ```bash closing-accounts - ✔ Enter lottery (451ms) - ✔ attacker can close + refund lottery acct + claim multiple rewards (18760ms) -AnchorError caused by account: lottery_entry. Error Code: AccountDiscriminatorMismatch. Error Number: 3002. Error Message: 8 byte discriminator did not match what was expected. - ✔ attacker cannot claim multiple rewards with secure claim (414ms) + ✔ Enter lottery should be successful (451ms) + ✔ attacker can close + refund lottery acct + claim multiple rewards successfully (18760ms) +AnchorError caused by account: lottery_entry. Error Code: AccountOwnedByWrongProgram. Error Number: 3007. Error Message: The given account is owned by a different program than expected. +Program log: Left: +Program log: 11111111111111111111111111111111 +Program log: Right: +Program log: FqETzdh6PsE7aNjrdapuoyFeYGdjPKN8AgG2ZUghje8A + ✔ attacker cannot claim multiple rewards with secure claim successfully (414ms) ``` Note, this does not prevent the malicious user from refunding their account altogether - it just protects our program from accidentally re-using the account -when it should be closed. We haven't implemented a `force_defund` instruction so -far, but we could. If you're feeling up for it, give it a try yourself! +when it should be closed. The simplest and most secure way to close accounts is using Anchor's `close` constraint. If you ever need more custom behavior and can't use this constraint, From 46e3395fe907ffa9a116189a49903ecc2a8aa9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Tue, 3 Sep 2024 12:06:03 +0800 Subject: [PATCH 10/20] =?UTF-8?q?Fix=20the=20anchor=20cpi=20lesson=20link.?= =?UTF-8?q?=20Use=C2=A0InitSpace=20to=20calculate=20space=20needed=20for?= =?UTF-8?q?=20accounts.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/courses/program-security/arbitrary-cpi.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/content/courses/program-security/arbitrary-cpi.md b/content/courses/program-security/arbitrary-cpi.md index 737cf8cb6..2fc6c93b3 100644 --- a/content/courses/program-security/arbitrary-cpi.md +++ b/content/courses/program-security/arbitrary-cpi.md @@ -128,9 +128,8 @@ crate provides the address of the SPL Token Program. ### Use an Anchor CPI module A simpler way to manage program checks is to use Anchor CPI modules. We learned -in a -[previous lesson](https://github.com/Unboxed-Software/solana-course/blob/main/content/anchor-cpi) -that Anchor can automatically generate CPI modules to make CPIs into the program +in a [previous lesson](/content/courses/onchain-development/anchor-cpi.md) that +Anchor can automatically generate CPI modules to make CPIs into the program simpler. These modules also enhance security by verifying the public key of the program that’s passed into one of its public instructions. @@ -256,7 +255,7 @@ There is already a test in the `tests` directory for this. It's long, but take a minute to look at it before we talk through it together: ```typescript -it("Insecure instructions allow attacker to win every time", async () => { +it("Insecure instructions allow attacker to win every time successfully", async () => { // Initialize player one with real metadata program await gameplayProgram.methods .createCharacterInsecure() @@ -352,7 +351,7 @@ pub struct CreateCharacterSecure<'info> { #[account( init, payer = authority, - space = 8 + 32 + 32 + 64, + space = DISCRIMINATOR_SIZE + Character::INIT_SPACE, seeds = [authority.key().as_ref()], bump )] @@ -404,12 +403,12 @@ new test. This test just needs to attempt to initialize the attacker's character and expect an error to be thrown. ```typescript -it("Secure character creation doesn't allow fake program", async () => { +it("Secure character creation with fake program should throw an exception", async () => { try { await gameplayProgram.methods .createCharacterSecure() .accounts({ - metadataProgram: fakeMetadataProgram.programId, + metadataProgram: fakeMetadataProgram.programId, // despite the compile error on this line. authority: attacker.publicKey, }) .signers([attacker]) From d7474c13b6d53662efbf030091e6a4bd07c265c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Wed, 4 Sep 2024 14:55:31 +0800 Subject: [PATCH 11/20] =?UTF-8?q?Use=C2=A0InitSpace=20to=20calculate=20spa?= =?UTF-8?q?ce=20needed=20for=20accounts.=20Update=20the=20error=20message?= =?UTF-8?q?=20in=20the=20section=20"Run=20the=20existing=20test".=20Add=20?= =?UTF-8?q?=E2=80=9C--skip-deploy=E2=80=9D=20to=20the=20test=20command.=20?= =?UTF-8?q?Delete=20an=20unnecessary=20setup=20in=20the=20section=20"Addin?= =?UTF-8?q?g=20a=20`local-testing`=20feature".=20Delete=20Unnecessary=20pa?= =?UTF-8?q?rameters=20found=20in=20the=20test=20typescript.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../program-configuration.md | 209 +++++++++--------- 1 file changed, 103 insertions(+), 106 deletions(-) diff --git a/content/courses/program-optimization/program-configuration.md b/content/courses/program-optimization/program-configuration.md index 637fc751a..150fe58dc 100644 --- a/content/courses/program-optimization/program-configuration.md +++ b/content/courses/program-optimization/program-configuration.md @@ -1,5 +1,5 @@ --- -title: Program Configuration +title: admin configuration objectives: - Define program features in the `Cargo.toml` file - Use the Rust `cfg` attribute to conditionally compile code based on which @@ -7,7 +7,7 @@ objectives: - Use the Rust `cfg!` macro to conditionally compile code based on which features are or are not enabled - Create an admin-only instruction to set up a program account that can be - used to store program configuration values + used to store admin configuration values description: "Create distinct environments, feature flags and admin-only instructions." --- @@ -243,7 +243,7 @@ In Anchor, that simply means creating an account struct and using a single seed to derive the account's address. ```rust -pub const SEED_PROGRAM_CONFIG: &[u8] = b"program_config"; +pub const SEED_ADMIN_CONFIG: &[u8] = b"admin_config"; #[account] pub struct ProgramConfig { @@ -287,14 +287,14 @@ mod my_program { } } -pub const SEED_PROGRAM_CONFIG: &[u8] = b"program_config"; +pub const SEED_ADMIN_CONFIG: &[u8] = b"admin_config"; #[constant] pub const ADMIN_PUBKEY: Pubkey = pubkey!("ADMIN_WALLET_ADDRESS_HERE"); #[derive(Accounts)] pub struct UpdateProgramConfig<'info> { - #[account(mut, seeds = SEED_PROGRAM_CONFIG, bump)] + #[account(mut, seeds = SEED_ADMIN_CONFIG, bump)] pub program_config: Account<'info, ProgramConfig>, #[account(constraint = authority.key() == ADMIN_PUBKEY)] pub authority: Signer<'info>, @@ -339,7 +339,7 @@ When completed, that looks like this: #[derive(Accounts)] pub struct UpdateProgramConfig<'info> { - #[account(mut, seeds = SEED_PROGRAM_CONFIG, bump)] + #[account(mut, seeds = [SEED_ADMIN_CONFIG], bump)] pub program_config: Account<'info, ProgramConfig>, #[account(constraint = program.programdata_address()? == Some(program_data.key()))] pub program: Program<'info, MyProgram>, @@ -365,7 +365,7 @@ want to update the admin to be someone else? For that, you can store the admin on the config account. ```rust -pub const SEED_PROGRAM_CONFIG: &[u8] = b"program_config"; +pub const SEED_ADMIN_CONFIG: &[u8] = b"admin_config"; #[account] pub struct ProgramConfig { @@ -381,11 +381,11 @@ against the config account's `admin` field. ```rust ... -pub const SEED_PROGRAM_CONFIG: &[u8] = b"program_config"; +pub const SEED_ADMIN_CONFIG: &[u8] = b"admin_config"; #[derive(Accounts)] pub struct UpdateProgramConfig<'info> { - #[account(mut, seeds = SEED_PROGRAM_CONFIG, bump)] + #[account(mut, seeds = [SEED_ADMIN_CONFIG], bump)] pub program_config: Account<'info, ProgramConfig>, #[account(constraint = authority.key() == program_config.admin)] pub authority: Signer<'info>, @@ -456,11 +456,11 @@ public key for your program printed to the console. This differs based on the keypair you have locally, so you need to update `lib.rs` and `Anchor.toml` to use _your_ key. -Finally, run `anchor test` to start the test. It should fail with the following -output: +Finally, run `anchor test --skip-deploy` to start the test. It should fail with +the following output: ``` -Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: incorrect program id for instruction +Error: AnchorError occurred. Error Code: ConstraintTokenMint. Error Number: 2014. Error Message: A token mint constraint was violated. ``` The reason for this error is that we're attempting to use the mainnet USDC mint @@ -534,13 +534,7 @@ local-testing = [] ``` Next, update the `config.ts` test file to create a mint using the generated -keypair. Start by deleting the `mint` constant. - -```typescript -const mint = new anchor.web3.PublicKey( - "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", -); -``` +keypair. Next, update the test to create a mint using the keypair, which will enable us to reuse the same mint address each time the tests are run. Remember to replace @@ -550,57 +544,54 @@ the file name with the one generated in the previous step. let mint: anchor.web3.PublicKey before(async () => { - let data = fs.readFileSync( - "env9Y3szLdqMLU9rXpEGPqkjdvVn8YNHtxYNvCKXmHe.json" - ) - - let keypair = anchor.web3.Keypair.fromSecretKey( - new Uint8Array(JSON.parse(data)) - ) - - const mint = await spl.createMint( - connection, - wallet.payer, - wallet.publicKey, - null, - 0, - keypair - ) + let rawdata = fs.readFileSync( + "env9Y3szLdqMLU9rXpEGPqkjdvVn8YNHtxYNvCKXmHe.json" + ); + let keyData = JSON.parse(rawdata); + let key = anchor.web3.Keypair.fromSecretKey(new Uint8Array(keyData)); + mint = await spl.createMint( + connection, + wallet.payer, + wallet.publicKey, + null, + 0, + key + ); ... ``` Lastly, run the test with the `local-testing` feature enabled. ``` -anchor test -- --features "local-testing" +anchor test --skip-deploy -- --features "local-testing" ``` You should see the following output: ``` config - ✔ Payment completes successfully (406ms) + ✔ Initialize Admin should be successfully (416ms) + ✔ Payment should complete successfully (426ms) - -1 passing (3s) + 2 passing (2s) ``` Boom. Just like that, you've used features to run two different code paths for different environments. -#### 4. Program Config +#### 4. admin config Features are great for setting different values at compilation, but what if you wanted to be able to dynamically update the fee percentage used by the program? -Let's make that possible by creating a Program Config account that allows us to +Let's make that possible by creating a admin config account that allows us to update the fee without upgrading the program. To begin, let's first update the `lib.rs` file to: -1. Include a `SEED_PROGRAM_CONFIG` constant, which will be used to generate the - PDA for the program config account. +1. Include a `SEED_ADMIN_CONFIG` constant, which will be used to generate the + PDA for the admin config account. 2. Include an `ADMIN` constant, which will be used as a constraint when - initializing the program config account. Run the `solana address` command to + initializing the admin config account. Run the `solana address` command to get your address to use as the constant's value. 3. Include a `state` module that we'll implement shortly. 4. Include the `initialize_program_config` and `update_program_config` @@ -609,7 +600,6 @@ To begin, let's first update the `lib.rs` file to: ```rust use anchor_lang::prelude::*; -use solana_program::{pubkey, pubkey::Pubkey}; mod instructions; mod state; use instructions::*; @@ -624,7 +614,7 @@ pub const USDC_MINT_PUBKEY: Pubkey = pubkey!("envgiPXWwmpkHFKdy4QLv2cypgAWmVTVEm #[constant] pub const USDC_MINT_PUBKEY: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); -pub const SEED_PROGRAM_CONFIG: &[u8] = b"program_config"; +pub const SEED_ADMIN_CONFIG: &[u8] = b"admin_config"; #[constant] pub const ADMIN: Pubkey = pubkey!("..."); @@ -650,7 +640,7 @@ pub mod config { } ``` -#### 5. Program Config State +#### 5. admin config State Next, let's define the structure for the `ProgramConfig` state. This account will store the admin, the token account where fees are sent, and the fee rate. @@ -662,59 +652,62 @@ following code. ```rust use anchor_lang::prelude::*; +const DISCRIMINATOR_SIZE: usize = 8; + #[account] -pub struct ProgramConfig { +#[derive(InitSpace)] +pub struct AdminConfig { pub admin: Pubkey, pub fee_destination: Pubkey, pub fee_basis_points: u64, } -impl ProgramConfig { - pub const LEN: usize = 8 + 32 + 32 + 8; +impl AdminConfig { + pub const LEN: usize = DISCRIMINATOR_SIZE + AdminConfig::INIT_SPACE; } ``` -#### 6. Add Initialize Program Config Account Instruction +#### 6. Add Initialize admin config Account Instruction -Now let's create the instruction logic for initializing the program config +Now let's create the instruction logic for initializing the admin config account. It should only be callable by a transaction signed by the `ADMIN` key and should set all the properties on the `ProgramConfig` account. Create a folder called `program_config` at the path `/src/instructions/program_config`. This folder will store all instructions -related to the program config account. +related to the admin config account. Within the `program_config` folder, create a file called `initialize_program_config.rs` and add the following code. ```rust -use crate::state::ProgramConfig; -use crate::ADMIN; -use crate::SEED_PROGRAM_CONFIG; +use crate::state::AdminConfig; +use crate::ADMIN_PUBKEY; +use crate::SEED_ADMIN_CONFIG; use crate::USDC_MINT_PUBKEY; use anchor_lang::prelude::*; use anchor_spl::token::TokenAccount; #[derive(Accounts)] -pub struct InitializeProgramConfig<'info> { - #[account(init, seeds = [SEED_PROGRAM_CONFIG], bump, payer = authority, space = ProgramConfig::LEN)] - pub program_config: Account<'info, ProgramConfig>, +pub struct InitializeAdminConfig<'info> { + #[account(init, seeds = [SEED_ADMIN_CONFIG], bump, payer = authority, space = AdminConfig::LEN)] + pub admin_config: Account<'info, AdminConfig>, #[account( token::mint = USDC_MINT_PUBKEY)] pub fee_destination: Account<'info, TokenAccount>, - #[account(mut, address = ADMIN)] + #[account(mut,address = ADMIN_PUBKEY)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, } -pub fn initialize_program_config_handler(ctx: Context) -> Result<()> { - ctx.accounts.program_config.admin = ctx.accounts.authority.key(); - ctx.accounts.program_config.fee_destination = ctx.accounts.fee_destination.key(); - ctx.accounts.program_config.fee_basis_points = 100; +pub fn initialize_admin_config_handler(ctx: Context) -> Result<()> { + ctx.accounts.admin_config.admin = ctx.accounts.authority.key(); + ctx.accounts.admin_config.fee_destination = ctx.accounts.fee_destination.key(); + ctx.accounts.admin_config.fee_basis_points = 100; Ok(()) } ``` -#### 7. Add Update Program Config Fee Instruction +#### 7. Add Update admin config Fee Instruction Next, implement the instruction logic for updating the config account. The instruction should require that the signer match the `admin` stored in the @@ -725,14 +718,14 @@ Within the `program_config` folder, create a file called ```rust use crate::state::ProgramConfig; -use crate::SEED_PROGRAM_CONFIG; +use crate::SEED_ADMIN_CONFIG; use crate::USDC_MINT_PUBKEY; use anchor_lang::prelude::*; use anchor_spl::token::TokenAccount; #[derive(Accounts)] pub struct UpdateProgramConfig<'info> { - #[account(mut, seeds = [SEED_PROGRAM_CONFIG], bump)] + #[account(mut, seeds = [SEED_ADMIN_CONFIG], bump)] pub program_config: Account<'info, ProgramConfig>, #[account( token::mint = USDC_MINT_PUBKEY)] pub fee_destination: Account<'info, TokenAccount>, @@ -787,11 +780,11 @@ pub use payment::*; Lastly, let's update the payment instruction to check that the `fee_destination` account in the instruction matches the `fee_destination` stored in the program config account. Then update the instruction's fee calculation to be based on the -`fee_basis_point` stored in the program config account. +`fee_basis_point` stored in the admin config account. ```rust use crate::state::ProgramConfig; -use crate::SEED_PROGRAM_CONFIG; +use crate::SEED_ADMIN_CONFIG; use crate::USDC_MINT_PUBKEY; use anchor_lang::prelude::*; use anchor_spl::token::{self, Token, TokenAccount}; @@ -799,7 +792,7 @@ use anchor_spl::token::{self, Token, TokenAccount}; #[derive(Accounts)] pub struct Payment<'info> { #[account( - seeds = [SEED_PROGRAM_CONFIG], + seeds = [SEED_ADMIN_CONFIG], bump, has_one = fee_destination )] @@ -866,51 +859,50 @@ pub fn payment_handler(ctx: Context, amount: u64) -> Result<()> { #### 10. Test -Now that we're done implementing our new program configuration struct and +Now that we're done implementing our new admin configuration struct and instructions, let's move on to testing our updated program. To begin, add the -PDA for the program config account to the test file. +PDA for the admin config account to the test file. ```typescript describe("config", () => { ... - const programConfig = findProgramAddressSync( - [Buffer.from("program_config")], + const adminConfig = PublicKey.findProgramAddressSync( + [Buffer.from("admin_config")], program.programId - )[0] + )[0]; ... ``` Next, update the test file with three more tests testing that: -1. The program config account is initialized correctly +1. The admin config account is initialized correctly 2. The payment instruction is functioning as intended 3. The config account can be updated successfully by the admin 4. The config account cannot be updated by someone other than the admin -The first test initializes the program config account and verifies that the -correct fee is set and that the correct admin is stored on the program config +The first test initializes the admin config account and verifies that the +correct fee is set and that the correct admin is stored on the admin config account. ```typescript -it("Initialize Program Config Account", async () => { +it("Initialize Admin config should be successfully", async () => { const tx = await program.methods - .initializeProgramConfig() + .initializeAdminConfig() .accounts({ - programConfig: programConfig, feeDestination: feeDestination, authority: wallet.publicKey, - systemProgram: anchor.web3.SystemProgram.programId, + programData: programDataAddress, }) .rpc(); assert.strictEqual( ( - await program.account.programConfig.fetch(programConfig) + await program.account.adminConfig.fetch(adminConfig) ).feeBasisPoints.toNumber(), 100, ); assert.strictEqual( - (await program.account.programConfig.fetch(programConfig)).admin.toString(), + (await program.account.adminConfig.fetch(adminConfig)).admin.toString(), wallet.publicKey.toString(), ); }); @@ -919,10 +911,10 @@ it("Initialize Program Config Account", async () => { The second test verifies that the payment instruction is working correctly, with the fee being sent to the fee destination and the remaining balance being transferred to the receiver. Here we update the existing test to include the -`programConfig` account. +`adminConfig` account. ```typescript -it("Payment completes successfully", async () => { +it("Payment should complete successfully", async () => { const tx = await program.methods .payment(new anchor.BN(10000)) .accounts({ @@ -955,16 +947,14 @@ it("Payment completes successfully", async () => { }); ``` -The third test attempts to update the fee on the program config account, which +The third test attempts to update the fee on the admin config account, which should be successful. ```typescript -it("Update Program Config Account", async () => { +it("Admin Config Update should be successfully", async () => { const tx = await program.methods - .updateProgramConfig(new anchor.BN(200)) + .updateAdminConfig(new anchor.BN(200)) .accounts({ - programConfig: programConfig, - admin: wallet.publicKey, feeDestination: feeDestination, newAdmin: sender.publicKey, }) @@ -972,50 +962,57 @@ it("Update Program Config Account", async () => { assert.strictEqual( ( - await program.account.programConfig.fetch(programConfig) + await program.account.adminConfig.fetch(adminConfig) ).feeBasisPoints.toNumber(), 200, ); }); ``` -The fourth test tries to update the fee on the program config account, where the -admin is not the one stored on the program config account, and this should fail. +The fourth test tries to update the fee on the admin config account, where the +admin is not the one stored on the admin config account, and this should fail. ```typescript -it("Update Program Config Account with unauthorized admin (expect fail)", async () => { +it("Admin Config Update with unauthorized admin should throw an exception", async () => { try { const tx = await program.methods - .updateProgramConfig(new anchor.BN(300)) + .updateAdminConfig(new anchor.BN(300)) .accounts({ - programConfig: programConfig, - admin: sender.publicKey, feeDestination: feeDestination, newAdmin: sender.publicKey, + admin: sender.publicKey, //ignore an error on this line }) - .transaction(); - - await anchor.web3.sendAndConfirmTransaction(connection, tx, [sender]); + .signers([sender]) + .rpc(); } catch (err) { expect(err); + console.log(err.message); + return; } + + assert.fail("should throw an exception"); }); ``` Finally, run the test using the following command: ``` -anchor test -- --features "local-testing" +anchor test --skip-deploy -- --features "local-testing" ``` You should see the following output: ``` config - ✔ Initialize Program Config Account (199ms) - ✔ Payment completes successfully (405ms) - ✔ Update Program Config Account (403ms) - ✔ Update Program Config Account with unauthorized admin (expect fail) + ✔ Initialize Admin config should be successfully (416ms) + ✔ Payment should complete successfully (419ms) + ✔ Admin Config Update should be successfully (414ms) +AnchorError caused by account: admin. Error Code: ConstraintAddress. Error Number: 2012. Error Message: An address constraint was violated. +Program log: Left: +Program log: 9DFY8t5NsZ2g8bQDypYZ5spHzBt6qjbVUgKhFxATMBRv +Program log: Right: +Program log: CbcfaDHPwur22CzyWmzn6b4kBe3ntsw9Hu1UTWt9q33Y + ✔ Admin Config Update with unauthorized admin should throw an exception 4 passing (8s) ``` From bdb9d845ebc7cc8e94bd658405b9a82aa3be9b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Wed, 4 Sep 2024 15:11:27 +0800 Subject: [PATCH 12/20] Remove the challenge section. --- .../program-configuration.md | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/content/courses/program-optimization/program-configuration.md b/content/courses/program-optimization/program-configuration.md index 150fe58dc..be17cab16 100644 --- a/content/courses/program-optimization/program-configuration.md +++ b/content/courses/program-optimization/program-configuration.md @@ -1024,52 +1024,6 @@ of [the same repository](https://github.com/Unboxed-Software/solana-admin-instr ## Challenge -Now it's time for you to do some of this on your own. We mentioned being able to -use the program's upgrade authority as the initial admin. Go ahead and update -the lab's `initialize_program_config` so that only the upgrade authority can -call it rather than having a hardcoded `ADMIN`. - -Note that the `anchor test` command, when run on a local network, starts a new -test validator using `solana-test-validator`. This test validator uses a -non-upgradeable loader. The non-upgradeable loader makes it so the program's -`program_data` account isn't initialized when the validator starts. You'll -recall from the lesson that this account is how we access the upgrade authority -from the program. - -To work around this, you can add a `deploy` function to the test file that runs -the deploy command for the program with an upgradeable loader. To use it, run -`anchor test --skip-deploy`, and call the `deploy` function within the test to -run the deploy command after the test validator has started. - -```typescript -import { execSync } from "child_process" - -... - -const deploy = () => { - const deployCmd = `solana program deploy --url localhost -v --program-id $(pwd)/target/deploy/config-keypair.json $(pwd)/target/deploy/config.so` - execSync(deployCmd) -} - -... - -before(async () => { - ... - deploy() -}) -``` - -For example, the command to run the test with features would look like this: - -``` -anchor test --skip-deploy -- --features "local-testing" -``` - -Try doing this on your own, but if you get stuck, feel free to reference the -`challenge` branch of -[the same repository](https://github.com/Unboxed-Software/solana-admin-instructions/tree/challenge) -to see one possible solution. - Push your code to GitHub and [tell us what you thought of this lesson](https://form.typeform.com/to/IPH0UGz7#answers-lesson=02a7dab7-d9c1-495b-928c-a4412006ec20)! From 8037a3d208ddd1ef1e893bd75ea922f85232d26e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Thu, 5 Sep 2024 10:07:23 +0800 Subject: [PATCH 13/20] update reinitialization-attacks repo link --- content/courses/program-security/reinitialization-attacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/courses/program-security/reinitialization-attacks.md b/content/courses/program-security/reinitialization-attacks.md index dc649af00..3b7a62b26 100644 --- a/content/courses/program-security/reinitialization-attacks.md +++ b/content/courses/program-security/reinitialization-attacks.md @@ -223,7 +223,7 @@ accounts. We’ll include two instructions: #### 1. Starter To get started, download the starter code from the `starter` branch of -[this repository](https://github.com/Unboxed-Software/solana-reinitialization-attacks/tree/starter). +[this repository](https://github.com/solana-developers/reinitialization-attacks/tree/starter). The starter code includes a program with one instruction and the boilerplate setup for the test file. @@ -463,7 +463,7 @@ state. If you want to take a look at the final solution code you can find it on the `solution` branch of -[this repository](https://github.com/Unboxed-Software/solana-reinitialization-attacks/tree/solution). +[this repository](https://github.com/solana-developers/reinitialization-attacks/tree/solution). ## Challenge From bcd87fd91b80175b9cd5461e441c9604070bae70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Thu, 5 Sep 2024 10:08:50 +0800 Subject: [PATCH 14/20] update pda-sharing repo link --- content/courses/program-security/pda-sharing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/courses/program-security/pda-sharing.md b/content/courses/program-security/pda-sharing.md index 2cdef7410..dd1868ba8 100644 --- a/content/courses/program-security/pda-sharing.md +++ b/content/courses/program-security/pda-sharing.md @@ -233,7 +233,7 @@ program accounts. #### 1. Starter To get started, download the starter code on the `starter` branch of -[this repository](https://github.com/Unboxed-Software/solana-pda-sharing/tree/starter). +[this repository](https://github.com/solana-developers/pda-sharing/tree/starter). The starter code includes a program with two instructions and the boilerplate setup for the test file. @@ -537,7 +537,7 @@ program and ensure that you aren't sharing PDAs across different domains. If you want to take a look at the final solution code you can find it on the `solution` branch of -[the same repository](https://github.com/Unboxed-Software/solana-pda-sharing/tree/solution). +[the same repository](https://github.com/solana-developers/pda-sharing/tree/solution). ## Challenge From ccee0cfde87b3a1574aa3b1d2190851ab58ce738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=9D=E5=8D=81=E6=9B=BF=20=E9=99=88=E4=B8=BD=E8=99=B9?= Date: Thu, 5 Sep 2024 10:11:04 +0800 Subject: [PATCH 15/20] update closing-accounts repo link --- content/courses/program-security/closing-accounts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/courses/program-security/closing-accounts.md b/content/courses/program-security/closing-accounts.md index 19443fd95..5a55a1be7 100644 --- a/content/courses/program-security/closing-accounts.md +++ b/content/courses/program-security/closing-accounts.md @@ -181,7 +181,7 @@ participation in the lottery. ### 1. Setup Start by getting the code on the `starter` branch from the -[following repo](https://github.com/Unboxed-Software/solana-closing-accounts/tree/starter). +[following repo](https://github.com/solana-developers/closing-accounts/tree/starter). The code has two instructions on the program and two tests in the `tests` directory. @@ -470,7 +470,7 @@ make sure to replicate its functionality to ensure your program is secure. If you want to take a look at the final solution code you can find it on the `solution` branch of -[the same repository](https://github.com/Unboxed-Software/solana-closing-accounts/tree/solution). +[the same repository](https://github.com/solana-developers/closing-accounts/tree/solution). ## Challenge From 076451cdec64be7bdee31b70fda3da46d65cf442 Mon Sep 17 00:00:00 2001 From: wuuer Date: Fri, 13 Sep 2024 09:59:29 +0800 Subject: [PATCH 16/20] update repositoy link --- content/courses/program-security/owner-checks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/courses/program-security/owner-checks.md b/content/courses/program-security/owner-checks.md index 70040c2b2..a66df0e63 100644 --- a/content/courses/program-security/owner-checks.md +++ b/content/courses/program-security/owner-checks.md @@ -299,7 +299,7 @@ execute. #### 1. Starter To get started, download the starter code from the `starter` branch of -[this repository](https://github.com/Unboxed-Software/solana-owner-checks/tree/starter). +[this repository](https://github.com/solana-developers/owner-checks/tree/starter). The starter code includes two programs `clone` and `owner_check` and the boilerplate setup for the test file. @@ -666,7 +666,7 @@ you add appropriate validation. If you want to take a look at the final solution code you can find it on the `solution` branch of -[the repository](https://github.com/Unboxed-Software/solana-owner-checks/tree/solution). +[the repository](https://github.com/solana-developers/owner-checks/tree/solution). ## Challenge From e1dbd0ed0699397595689ca82088b1563741fbe7 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Fri, 4 Oct 2024 15:54:05 +1000 Subject: [PATCH 17/20] Update content/courses/program-optimization/program-configuration.md --- content/courses/program-optimization/program-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/courses/program-optimization/program-configuration.md b/content/courses/program-optimization/program-configuration.md index be17cab16..c3ddc95a8 100644 --- a/content/courses/program-optimization/program-configuration.md +++ b/content/courses/program-optimization/program-configuration.md @@ -1,5 +1,5 @@ --- -title: admin configuration +title: Admin configuration objectives: - Define program features in the `Cargo.toml` file - Use the Rust `cfg` attribute to conditionally compile code based on which From 27dd9036fc6c462573d1ba52c58a56c9c9a8b122 Mon Sep 17 00:00:00 2001 From: wuuer Date: Wed, 9 Oct 2024 09:36:18 +0800 Subject: [PATCH 18/20] Update content/courses/program-security/pda-sharing.md Co-authored-by: Mike MacCana --- content/courses/program-security/pda-sharing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/courses/program-security/pda-sharing.md b/content/courses/program-security/pda-sharing.md index dd1868ba8..2156af7c1 100644 --- a/content/courses/program-security/pda-sharing.md +++ b/content/courses/program-security/pda-sharing.md @@ -233,7 +233,7 @@ program accounts. #### 1. Starter To get started, download the starter code on the `starter` branch of -[this repository](https://github.com/solana-developers/pda-sharing/tree/starter). +[the `pda-sharing` repository](https://github.com/solana-developers/pda-sharing/tree/starter). The starter code includes a program with two instructions and the boilerplate setup for the test file. From 458ae79d46450e6343a6506f8d8a7ced67d35a77 Mon Sep 17 00:00:00 2001 From: wuuer Date: Wed, 9 Oct 2024 09:55:36 +0800 Subject: [PATCH 19/20] update `close` constraint comment --- content/courses/program-security/closing-accounts.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/content/courses/program-security/closing-accounts.md b/content/courses/program-security/closing-accounts.md index 5a55a1be7..da2ed2b6c 100644 --- a/content/courses/program-security/closing-accounts.md +++ b/content/courses/program-security/closing-accounts.md @@ -338,10 +338,9 @@ pub struct RedeemWinningsSecure<'info> { It should be the exact same as the original `RedeemWinnings` account validation struct, except there is an additional `close = user` constraint on the `lottery_entry` account. This will tell Anchor to close the account by zeroing -out the data, transferring its lamports to the `user` account, and setting the -account discriminator to the `CLOSED_ACCOUNT_DISCRIMINATOR`. This last step is -what will prevent the account from being used again if the program has attempted -to close it already. +out the data, Assigning the owner to the System Program, transferring its +lamports to the `user` account. This last step is what will prevent the account +from being used again if the program has attempted to close it already. Then, we can create a `mint_ctx` method on the new `RedeemWinningsSecure` struct to help with the minting CPI to the token program. From 984a0f8d94fefc0c10fdf50bdfe00169001dedfe Mon Sep 17 00:00:00 2001 From: wuuer Date: Wed, 9 Oct 2024 10:02:37 +0800 Subject: [PATCH 20/20] update `close` constraint comment --- content/courses/program-security/closing-accounts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/courses/program-security/closing-accounts.md b/content/courses/program-security/closing-accounts.md index da2ed2b6c..09edd28ed 100644 --- a/content/courses/program-security/closing-accounts.md +++ b/content/courses/program-security/closing-accounts.md @@ -338,7 +338,7 @@ pub struct RedeemWinningsSecure<'info> { It should be the exact same as the original `RedeemWinnings` account validation struct, except there is an additional `close = user` constraint on the `lottery_entry` account. This will tell Anchor to close the account by zeroing -out the data, Assigning the owner to the System Program, transferring its +out the data, assigning the owner to the System Program, transferring its lamports to the `user` account. This last step is what will prevent the account from being used again if the program has attempted to close it already.