diff --git a/content/courses/program-security/owner-checks.md b/content/courses/program-security/owner-checks.md index 1b95f7281..a30103dc8 100644 --- a/content/courses/program-security/owner-checks.md +++ b/content/courses/program-security/owner-checks.md @@ -3,11 +3,11 @@ title: Owner Checks objectives: - Explain the security risks associated with not performing appropriate owner checks - - Implement owner checks using long-form Rust - - Use Anchor’s `Account<'info, T>` wrapper and an account type to automate + - Use Anchor's `Account<'info, T>` wrapper and an account type to automate owner checks - - Use Anchor’s `#[account(owner = )]` constraint to explicitly define an + - Use Anchor's `#[account(owner = )]` constraint to explicitly define an external program that should own an account + - Implement owner checks using native Rust description: "Understand the use of account owner checks when processing incoming instructions." @@ -15,11 +15,17 @@ description: ## Summary -- Use **Owner Checks** to verify that accounts are owned by the expected - program. Without appropriate owner checks, accounts owned by unexpected - programs could be used in an instruction. -- To implement an owner check in Rust, simply check that an account’s owner - matches an expected program ID +- **Owner checks** ensure that accounts are owned by the expected program. + Without owner checks, accounts owned by other programs can be used in an + instruction handler. +- Anchor program account types implement the `Owner` trait, allowing + `Account<'info, T>` to automatically verify program ownership. +- You can also use Anchor's + [`#[account(owner = )]`](https://www.anchor-lang.com/docs/account-constraints) + constraint to define an account's owner when it's external to the current + program. +- To implement an owner check in native Rust, verify that the account's owner + matches the expected program ID. ```rust if ctx.accounts.account.owner != ctx.program_id { @@ -27,20 +33,15 @@ if ctx.accounts.account.owner != ctx.program_id { } ``` -- Anchor program account types implement the `Owner` trait which allows the - `Account<'info, T>` wrapper to automatically verify program ownership -- Anchor gives you the option to explicitly define the owner of an account if it - should be anything other than the currently executing program - ## Lesson -Owner checks are used to verify that an account passed into an instruction is -owned by an expected program. This prevents accounts owned by an unexpected -program from being used in an instruction. +Owner checks are used to verify that an account passed into an instruction +handler is owned by the expected program, preventing exploitation by accounts +from different programs. -As a refresher, the `AccountInfo` struct contains the following fields. An owner -check refers to checking that the `owner` field in the `AccountInfo` matches an -expected program ID. +The `AccountInfo` struct contains several fields, including the `owner`, which +represents the **program** that owns the account. Owner checks ensure that this +`owner` field in the `AccountInfo` matches the expected program ID. ```rust /// Account information @@ -65,27 +66,12 @@ pub struct AccountInfo<'a> { } ``` -#### Missing owner check - -The example below shows an `admin_instruction` intended to be accessible only by -an `admin` account stored on an `admin_config` account. - -Although the instruction checks the `admin` account signed the transaction and -matches the `admin` field stored on the `admin_config` account, there is no -owner check to verify the `admin_config` account passed into the instruction is -owned by the executing program. - -Since the `admin_config` is unchecked as indicated by the `AccountInfo` type, a -fake `admin_config` account owned by a different program could be used in the -`admin_instruction`. This means that an attacker could create a program with an -`admin_config` whose data structure matches the `admin_config` of your program, -set their public key as the `admin` and pass their `admin_config` account into -your program. This would let them spoof your program into thinking that they are -the authorized admin for your program. +### Missing owner check -This simplified example only prints the `admin` to the program logs. However, -you can imagine how a missing owner check could allow fake accounts to exploit -an instruction. +In the following example, an `admin_instruction` is intended to be restricted to +an `admin` account stored in the `admin_config` account. However, it fails to +check whether the program owns the `admin_config` account. Without this check, +an attacker can spoof the account. ```rust use anchor_lang::prelude::*; @@ -95,7 +81,7 @@ declare_id!("Cft4eTTrt4sJU4Ar35rUQHx6PSXfJju3dixmvApzhWws"); #[program] pub mod owner_check { use super::*; - ... + ... pub fn admin_instruction(ctx: Context) -> Result<()> { let account_data = ctx.accounts.admin_config.try_borrow_data()?; @@ -112,7 +98,8 @@ pub mod owner_check { #[derive(Accounts)] pub struct Unchecked<'info> { - admin_config: AccountInfo<'info>, + /// CHECK: This account will not be checked by Anchor + admin_config: UncheckedAccount<'info>, admin: Signer<'info>, } @@ -122,11 +109,10 @@ pub struct AdminConfig { } ``` -#### Add owner check +### Add owner check -In vanilla Rust, you could solve this problem by comparing the `owner` field on -the account to the program ID. If they do not match, you would return an -`IncorrectProgramId` error. +To resolve this issue in native Rust, compare the `owner` field with the program +ID: ```rust if ctx.accounts.admin_config.owner != ctx.program_id { @@ -134,9 +120,8 @@ if ctx.accounts.admin_config.owner != ctx.program_id { } ``` -Adding an owner check prevents accounts owned by an unexpected program to be -passed in as the `admin_config` account. If a fake `admin_config` account was -used in the `admin_instruction`, then the transaction would fail. +Adding an `owner` check ensures that accounts from other programs cannot be +passed into the instruction handler. ```rust use anchor_lang::prelude::*; @@ -166,7 +151,8 @@ pub mod owner_check { #[derive(Accounts)] pub struct Unchecked<'info> { - admin_config: AccountInfo<'info>, + /// CHECK: This account will not be checked by Anchor + admin_config: UncheckedAccount<'info>, admin: Signer<'info>, } @@ -176,26 +162,14 @@ pub struct AdminConfig { } ``` -#### Use Anchor’s `Account<'info, T>` - -Anchor can make this simpler with the `Account` type. - -`Account<'info, T>` is a wrapper around `AccountInfo` that verifies program -ownership and deserializes underlying data into the specified account type `T`. -This in turn allows you to use `Account<'info, T>` to easily validate ownership. - -For context, the `#[account]` attribute implements various traits for a data -structure representing an account. One of these is the `Owner` trait which -defines an address expected to own an account. The owner is set as the program -ID specified in the `declare_id!` macro. +### Use Anchor's `Account<'info, T>` -In the example below, `Account<'info, AdminConfig>` is used to validate the -`admin_config`. This will automatically perform the owner check and deserialize -the account data. Additionally, the `has_one` constraint is used to check that -the `admin` account matches the `admin` field stored on the `admin_config` -account. +Anchor simplifies owner checks with the `Account` type, which wraps +`AccountInfo` and automatically verifies ownership. -This way, you don’t need to clutter your instruction logic with owner checks. +In the following example, `Account<'info, AdminConfig>` validates the +`admin_config` account, and the `has_one` constraint checks that the admin +account matches the `admin` field in `admin_config`. ```rust use anchor_lang::prelude::*; @@ -205,7 +179,7 @@ declare_id!("Cft4eTTrt4sJU4Ar35rUQHx6PSXfJju3dixmvApzhWws"); #[program] pub mod owner_check { use super::*; - ... + ... pub fn admin_instruction(ctx: Context) -> Result<()> { msg!("Admin: {}", ctx.accounts.admin_config.admin.to_string()); Ok(()) @@ -227,19 +201,19 @@ pub struct AdminConfig { } ``` -#### Use Anchor’s `#[account(owner = )]` constraint +### Use Anchor's `#[account(owner = )]` constraint -In addition to the `Account` type, you can use an `owner` constraint. The -`owner` constraint allows you to define the program that should own an account -if it’s different from the currently executing one. This comes in handy if, for -example, you are writing an instruction that expects an account to be a PDA -derived from a different program. You can use the `seeds` and `bump` constraints -and define the `owner` to properly derive and verify the address of the account -passed in. +In addition to the `Account` type, you can use the Anchor's +[`owner` constraint](https://www.anchor-lang.com/docs/account-constraints) to +specify the program that should own an account when it differs from the +executing program. This is particularly useful when an instruction handler +expects an account to be a PDA created by another program. By using the `seeds` +and `bump` constraints along with the `owner`, you can properly derive and +verify the account's address. -To use the `owner` constraint, you’ll have to have access to the public key of -the program you expect to own an account. You can either pass the program in as -an additional account or hard-code the public key somewhere in your program. +To apply the `owner` constraint, you need access to the public key of the +program expected to own the account. This can be provided either as an +additional account or by hard-coding the public key within your program. ```rust use anchor_lang::prelude::*; @@ -280,41 +254,43 @@ pub struct AdminConfig { ## Lab -In this lab we’ll use two programs to demonstrate how a missing owner check -could allow a fake account to drain the tokens from a simplified token “vault” -account (note that this is very similar to the lab from the Signer Authorization -lesson). +In this lab, we'll demonstrate how the absence of an owner check can allow a +malicious actor to drain tokens from a simplified token vault. This is similar +to the lab from the +[Signer Authorization lesson](/content/courses/program-security/signer-auth.md). -To help illustrate this, one program will be missing an account owner check on -the vault account it withdraws tokens to. +We'll use two programs to illustrate this: -The second program will be a direct clone of the first program created by a -malicious user to create an account identical to the first program’s vault -account. +1. One program lacks an owner check on the vault account it withdraws tokens + from. +2. The second program is a clone created by a malicious user to mimic the first + program's vault account. -Without the owner check, this malicious user will be able to pass in the vault -account owned by their “faked” program and the original program will still -execute. +Without the owner check, the malicious user can pass in their vault account +owned by a fake program, and the original program will still execute the +withdrawal. -#### 1. Starter +### 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). -The starter code includes two programs `clone` and `owner_check` and the -boilerplate setup for the test file. +Begin by downloading the starter code from the +[`starter` branch of this repository](https://github.com/solana-developers/owner-checks/tree/starter). +The starter code includes two programs: `clone` and `owner_check`, and the setup +for the test file. -The `owner_check` program includes two instructions: +The `owner_check` program includes two instruction handlers: -- `initialize_vault` initializes a simplified vault account that stores the - addresses of a token account and an authority account -- `insecure_withdraw` withdraws tokens from the token account, but is missing an - owner check for the vault account +- `initialize_vault`: Initializes a simplified vault account storing the + addresses of a token account and an authority account. +- `insecure_withdraw`: Withdraws tokens from the token account but lacks an + owner check for the vault account. ```rust use anchor_lang::prelude::*; use anchor_spl::token::{self, Mint, Token, TokenAccount}; -declare_id!("HQYNznB3XTqxzuEqqKMAD9XkYE5BGrnv8xmkoDNcqHYB"); +declare_id!("3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9"); + +pub const DISCRIMINATOR_SIZE: usize = 8; #[program] pub mod owner_check { @@ -339,7 +315,7 @@ pub mod owner_check { let seeds = &[ b"token".as_ref(), - &[*ctx.bumps.get("token_account").unwrap()], + &[ctx.bumps.token_account], ]; let signer = [&seeds[..]]; @@ -363,7 +339,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( @@ -385,7 +361,7 @@ pub struct InitializeVault<'info> { #[derive(Accounts)] pub struct InsecureWithdraw<'info> { - /// CHECK: + /// CHECK: This account will not be checked by anchor pub vault: UncheckedAccount<'info>, #[account( mut, @@ -400,23 +376,26 @@ pub struct InsecureWithdraw<'info> { } #[account] +#[derive(Default, InitSpace)] pub struct Vault { token_account: Pubkey, authority: Pubkey, } ``` -The `clone` program includes a single instruction: +The `clone` program includes a single instruction handler: -- `initialize_vault` initializes a “vault” account that mimics the vault account - of the `owner_check` program. It stores the address of the real vault’s token - account, but allows the malicious user to put their own authority account. +- `initialize_vault`: Initializes a fake vault account that mimics the vault + account of the `owner_check` program, allowing the malicious user to set their + own authority. ```rust use anchor_lang::prelude::*; use anchor_spl::token::TokenAccount; -declare_id!("DUN7nniuatsMC7ReCh5eJRQExnutppN1tAfjfXFmGDq3"); +declare_id!("2Gn5MFGMvRjd548z6vhreh84UiL7L5TFzV5kKGmk4Fga"); + +pub const DISCRIMINATOR_SIZE: usize = 8; #[program] pub mod clone { @@ -434,7 +413,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,98 +423,92 @@ pub struct InitializeVault<'info> { } #[account] +#[derive(Default, InitSpace)] pub struct Vault { token_account: Pubkey, authority: Pubkey, } ``` -#### 2. Test `insecure_withdraw` instruction - -The test file includes a test to invoke the `initialize_vault` instruction on -the `owner_check` program using the provider wallet as the `authority` and then -mints 100 tokens to the token account. - -The test file also includes a test to invoke the `initialize_vault` instruction -on the `clone` program to initialize a fake `vault` account storing the same -`tokenPDA` account, but a different `authority`. Note that no new tokens are -minted here. +### 2. Test insecure_withdraw Instruction Handler -Let’s add a test to invoke the `insecure_withdraw` instruction. This test should -pass in the cloned vault and the fake authority. Since there is no owner check -to verify the `vaultClone` account is owned by the `owner_check` program, the -instruction’s data validation check will pass and show `walletFake` as a valid -authority. The tokens from the `tokenPDA` account will then be withdrawn to the -`withdrawDestinationFake` account. +The test file contains tests that initialize a vault in both programs. We'll add +a test to invoke the `insecure_withdraw` instruction handler, showing how the +lack of an owner check allows token withdrawal from the original program's +vault. ```typescript -describe("owner-check", () => { - ... - it("Insecure withdraw", async () => { - const tx = await program.methods +describe("Owner Check", () => { + ... + it("performs insecure withdraw", async () => { + try { + const transaction = await program.methods .insecureWithdraw() .accounts({ - vault: vaultClone.publicKey, - tokenAccount: tokenPDA, - withdrawDestination: withdrawDestinationFake, - authority: walletFake.publicKey, + vault: vaultCloneAccount.publicKey, + tokenAccount: tokenPDA, + withdrawDestination: unauthorizedWithdrawDestination, + authority: unauthorizedWallet.publicKey, }) - .transaction() - - await anchor.web3.sendAndConfirmTransaction(connection, tx, [walletFake]) + .transaction(); - const balance = await connection.getTokenAccountBalance(tokenPDA) - expect(balance.value.uiAmount).to.eq(0) - }) + await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + unauthorizedWallet, + ]); + const tokenAccountInfo = await getAccount(connection, tokenPDA); + expect(Number(tokenAccountInfo.amount)).to.equal(0); + } catch (error) { + console.error("Insecure withdraw failed:", error); + throw error; + } + }); }) ``` -Run `anchor test` to see that the `insecure_withdraw` completes successfully. +Run an `anchor test` to verify that the `insecure_withdraw` is complete +successfully. ```bash owner-check - ✔ Initialize Vault (808ms) - ✔ Initialize Fake Vault (404ms) - ✔ Insecure withdraw (409ms) + ✔ initializes vault (866ms) + ✔ initializes fake vault (443ms) + ✔ performs insecure withdraw (444ms) ``` -Note that `vaultClone` deserializes successfully even though Anchor -automatically initializes new accounts with a unique 8 byte discriminator and -checks the discriminator when deserializing an account. This is because the -discriminator is a hash of the account type name. + + +The `vaultCloneAccount` deserializes successfully due to both programs using the +same discriminator, derived from the identical `Vault` struct name. ```rust #[account] +#[derive(Default, InitSpace)] pub struct Vault { token_account: Pubkey, authority: Pubkey, } ``` -Since both programs initialize identical accounts and both structs are named -`Vault`, the accounts have the same discriminator even though they are owned by -different programs. +### 3. Add secure_withdraw Instruction Handler -#### 3. Add `secure_withdraw` instruction +We'll now close the security loophole by adding a `secure_withdraw` instruction +handler with an `Account<'info, Vault>` type to ensure an owner check is +performed. -Let’s close up this security loophole. - -In the `lib.rs` file of the `owner_check` program add a `secure_withdraw` -instruction and a `SecureWithdraw` accounts struct. - -In the `SecureWithdraw` struct, let’s use `Account<'info, Vault>` to ensure that -an owner check is performed on the `vault` account. We’ll also use the `has_one` -constraint to check that the `token_account` and `authority` passed into the -instruction match the values stored on the `vault` account. +In the `lib.rs` file of the `owner_check` program, add a `secure_withdraw` +instruction handler and a `SecureWithdraw` accounts struct. The `has_one` +constraint will be used to ensure that the `token_account` and `authority` +passed into the instruction handler match the values stored in the `vault` +account. ```rust #[program] pub mod owner_check { use super::*; - ... + ... - pub fn secure_withdraw(ctx: Context) -> Result<()> { + pub fn secure_withdraw(ctx: Context) -> Result<()> { let amount = ctx.accounts.token_account.amount; let seeds = &[ @@ -580,109 +553,116 @@ pub struct SecureWithdraw<'info> { } ``` -#### 4. Test `secure_withdraw` instruction +### 4. Test secure_withdraw Instruction Handler -To test the `secure_withdraw` instruction, we’ll invoke the instruction twice. -First, we’ll invoke the instruction using the `vaultClone` account, which we -expect to fail. Then, we’ll invoke the instruction using the correct `vault` -account to check that the instruction works as intended. +To test the `secure_withdraw` instruction handler, we'll invoke it twice. First, +we'll use the `vaultCloneAccount` account, expecting it to fail. Then, we'll +invoke the instruction handler with the correct `vaultAccount` account to verify +the instruction handler 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 +describe("Owner Check", () => { + ... + it("fails secure withdraw with incorrect authority", async () => { + try { + const transaction = await program.methods + .secureWithdraw() + .accounts({ + vault: vaultCloneAccount.publicKey, + tokenAccount: tokenPDA, + withdrawDestination: unauthorizedWithdrawDestination, + authority: unauthorizedWallet.publicKey, + }) + .transaction(); + + await anchor.web3.sendAndConfirmTransaction(connection, transaction, [ + unauthorizedWallet, + ]); + throw new Error("Expected transaction to fail, but it succeeded"); + } catch (error) { + expect(error).to.be.an("error"); + console.log("Error message:", error.message); + } + }); + + it("performs secure withdraw successfully", async () => { + try { + await mintTo( + connection, + walletAuthority.payer, + tokenMint, + tokenPDA, + walletAuthority.payer, + INITIAL_TOKEN_AMOUNT + ); + + await program.methods .secureWithdraw() .accounts({ - vault: vault.publicKey, - tokenAccount: tokenPDA, - withdrawDestination: withdrawDestination, - authority: wallet.publicKey, + vault: vaultAccount.publicKey, + tokenAccount: tokenPDA, + withdrawDestination: authorizedWithdrawDestination, + authority: walletAuthority.publicKey, }) - .rpc() + .rpc(); - const balance = await connection.getTokenAccountBalance(tokenPDA) - expect(balance.value.uiAmount).to.eq(0) - }) + const tokenAccountInfo = await getAccount(connection, tokenPDA); + expect(Number(tokenAccountInfo.amount)).to.equal(0); + } catch (error) { + console.error("Secure withdraw failed:", error); + throw error; + } + }); }) ``` -Run `anchor test` to see that the transaction using the `vaultClone` account -will now return an Anchor Error while the transaction using the `vault` account -completes successfully. +Running `anchor test` will show that the transaction using the +`vaultCloneAccount` account fails, while the transaction using the +`vaultAccount` account withdraws successfully. ```bash -'Program HQYNznB3XTqxzuEqqKMAD9XkYE5BGrnv8xmkoDNcqHYB invoke [1]', -'Program log: Instruction: SecureWithdraw', -'Program log: AnchorError caused by account: vault. Error Code: AccountOwnedByWrongProgram. Error Number: 3007. Error Message: The given account is owned by a different program than expected.', -'Program log: Left:', -'Program log: DUN7nniuatsMC7ReCh5eJRQExnutppN1tAfjfXFmGDq3', -'Program log: Right:', -'Program log: HQYNznB3XTqxzuEqqKMAD9XkYE5BGrnv8xmkoDNcqHYB', -'Program HQYNznB3XTqxzuEqqKMAD9XkYE5BGrnv8xmkoDNcqHYB consumed 5554 of 200000 compute units', -'Program HQYNznB3XTqxzuEqqKMAD9XkYE5BGrnv8xmkoDNcqHYB failed: custom program error: 0xbbf' +"Program 3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9 invoke [1]", +"Program log: Instruction: SecureWithdraw", +"Program log: AnchorError caused by account: vault. Error Code: AccountOwnedByWrongProgram. Error Number: 3007. Error Message: The given account is owned by a different program than expected.", +"Program log: Left:", +"Program log: 2Gn5MFGMvRjd548z6vhreh84UiL7L5TFzV5kKGmk4Fga", +"Program log: Right:", +"Program log: 3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9", +"Program 3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9 consumed 4449 of 200000 compute units", +"Program 3uF3yaymq1YBmDDHpRPwifiaBf4eK8M2jLgaMcCTg9n9 failed: custom program error: 0xbbf" ``` -Here we see how using Anchor’s `Account<'info, T>` type can simplify the account -validation process to automate the ownership check. Additionally, note that -Anchor Errors can specify the account that causes the error (e.g. the third line -of the logs above say `AnchorError caused by account: vault`). This can be very -helpful when debugging. +Here we see how using Anchor's `Account<'info, T>` type simplifies the account +validation process by automating ownership checks. Additionally, Anchor errors +provide specific details, such as which account caused the error. For example, +the log indicates `AnchorError caused by account: vault`, which aids in +debugging. ```bash -✔ Secure withdraw, expect error (78ms) -✔ Secure withdraw (10063ms) +✔ fails secure withdraw with incorrect authority +✔ performs secure withdraw successfully (847ms) ``` -That’s all you need to ensure you check the owner on an account! Like some other -exploits, it’s fairly simple to avoid but very important. Be sure to always -think through which accounts should be owned by which programs and ensure that -you add appropriate validation. +Ensuring account ownership checks is critical to avoid security vulnerabilities. +This example demonstrates how simple it is to implement proper validation, but +it’s vital to always verify which accounts are owned by specific programs. -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). +If you'd like to review the final solution code, it's available on the +[`solution` branch of the repository](https://github.com/solana-developers/owner-checks/tree/solution). ## Challenge -Just as with other lessons in this unit, your opportunity to practice avoiding -this security exploit lies in auditing your own or other programs. +As with other lessons in this unit, practice preventing security exploits by +auditing your own or other programs. -Take some time to review at least one program and ensure that proper owner -checks are performed on the accounts passed into each instruction. +Take time to review at least one program to confirm that ownership checks are +properly enforced on all accounts passed into each instruction handler. -Remember, if you find a bug or exploit in somebody else's program, please alert -them! If you find one in your own program, be sure to patch it right away. +If you find a bug or exploit in another program, notify the developer. If you +find one in your own program, patch it immediately. + Push your code to GitHub and [tell us what you thought of this lesson](https://form.typeform.com/to/IPH0UGz7#answers-lesson=e3069010-3038-4984-b9d3-2dc6585147b1)!