Tx.origin, msg.sender, and onlyOwner in Solana: identifying the caller

tx.origin msg.sender onlyOwner in Solana

In Solidity, the msg.sender is a global variable that represents the address that called or initiated a function call on a smart contract. The global variable tx.origin is the wallet that signed the transaction.

In Solana, there is no equivalent to msg.sender.

There is an equivalent to tx.origin but you should be aware that Solana transactions can have multiple signers, so we could think of it as having “multiple tx.origins”.

To get the “tx.origin” address in Solana, you need to set it up by adding Signer account to the function context and pass the caller’s account to it when calling the function.

Let’s see an example of how we can access the transaction signer’s address in Solana:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

#[program]
pub mod day14 {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let the_signer1: &mut Signer = &mut ctx.accounts.signer1;

        // Function logic....

        msg!("The signer1: {:?}", *the_signer1.key);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub signer1: Signer<'info>,
}

From the above code snippet, the Signer<'info> is used to verify that the signer1 account in the Initialize<'info> account struct has signed the transaction.

In the initialize function, the signer1 account is mutably referenced from the context and assigned to the_signer1 variable.

Then lastly, we logged the signer1‘s pubkey (address) using the msg! macro and passing in *the_signer1.key , which dereferences and access the key field or method on the actual value being pointed to by the_signer1.

Next is to write a test for the above program:

describe("Day14", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  it("Is signed by a single signer", async () => {
    // Add your test here.
    const tx = await program.methods.initialize().accounts({
      signer1: program.provider.publicKey
    }).rpc();

    console.log("The signer1: ", program.provider.publicKey.toBase58());
  });
});

In the test, we passed our wallet account as signer to the signer1 account, then called the initialize function. Following that, we logged the wallet account on the console to verify its consistency with the one in our program.

Exercise: What did you notice from the outputs in shell_1 (commands terminal) and shell_3 (logs terminal) after running the test?

Multiple signers

In Solana, we can also have more than one signer sign a transaction, you can think of this as batching up a bunch of signatures and sending it in one transaction. One use-case is doing a multisig transaction in one transaction.

To do that, we just add more Signer structs to the account struct in our program, then ensure the necessary accounts are passed when calling the function:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

#[program]
pub mod day14 {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let the_signer1: &mut Signer = &mut ctx.accounts.signer1;
        let the_signer2: &mut Signer = &mut ctx.accounts.signer2;

        msg!("The signer1: {:?}", *the_signer1.key);
        msg!("The signer2: {:?}", *the_signer2.key);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    pub signer1: Signer<'info>,
    pub signer2: Signer<'info>,
}

The above example is somewhat the same as the single signer example, with one notable difference. In this case, we added another Signer account (signer2) to the Initialize struct and also logged both signers pubkey in the initialize function.

Calling the initialize function with multiple signers is different, compared to a single signer. The test below shows how to invoke a function with multiple signers:

describe("Day14", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  // generate a signer to call our function
  let myKeypair = anchor.web3.Keypair.generate();

  it("Is signed by multiple signers", async () => {
    // Add your test here.
    const tx = await program.methods
      .initialize()
      .accounts({
        signer1: program.provider.publicKey,
        signer2: myKeypair.publicKey,
      })
      .signers([myKeypair])
      .rpc();

    console.log("The signer1: ", program.provider.publicKey.toBase58());
    console.log("The signer2: ", myKeypair.publicKey.toBase58());
  });
});

So what is different about the above test? First is the signers() method, which takes in an array of signers which signs a transaction as an argument. But we only have one signer in the array, instead of two. Anchor automatically passes the wallet account in the provider as a signer, so we don’t need to add it to the signers array again.

Generating random addresses to test with

The second change is the myKeypair variable, which stores the Keypair (A publickey and corresponding private key for accessing an account) that is randomly generated by the anchor.web3 module. In the test, we assigned the Keypair’s (which is stored in the myKeypair variable) publickey to the signer2 account, that is why it is passed as argument in the .signers([myKeypair]) method.

Run the test multiple times, you will notice that signer1 pubkey does not change but signer2 pubkey changes. This is because the wallet account assigned to the signer1 account (in the test) is from the provider, which is also the Solana wallet account in your local machine and the account assigned to signer2 is randomly generated each time you run anchor test —skip-local-validator.

Exercise: Create another function (you can call it whatever) that requires three signers (the provider wallet account and two randomly generated accounts) and write a test for it.

onlyOwner

This is a common pattern used in Solidity to restrict a function’s access to only the owner of the contract. Using #[access_control] attribute from Anchor, we can also implement the only owner pattern, that is, restrict a function’s access in our Solana program to a PubKey (owner’s address).

Here’s an example of how to implement “onlyOwner” functionality in Solana:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

// NOTE: Replace with your wallet's public key
const OWNER: &str = "8os8PKYmeVjU1mmwHZZNTEv5hpBXi5VvEKGzykduZAik";

#[program]
pub mod day14 {
    use super::*;

    #[access_control(check(&ctx))]
    pub fn initialize(ctx: Context<OnlyOwner>) -> Result<()> {
        // Function logic...

        msg!("Holla, I'm the owner.");
        Ok(())
    }
}

fn check(ctx: &Context<OnlyOwner>) -> Result<()> {
    // Check if signer === owner
    require_keys_eq!(
        ctx.accounts.signer_account.key(),
        OWNER.parse::<Pubkey>().unwrap(),
        OnlyOwnerError::NotOwner
    );

    Ok(())
}

#[derive(Accounts)]
pub struct OnlyOwner<'info> {
    signer_account: Signer<'info>,
}

// An enum for custom error codes
#[error_code]
pub enum OnlyOwnerError {
    #[msg("Only owner can call this function!")]
    NotOwner,
}

In the context of the code above, the OWNER variable stores the pubkey (address) associated with my local Solana wallet. Be sure to replace the OWNER variable with your wallet’s pubkey before testing. You can easily retrieve your pubkey by running the solana address command.

The #[access_control] attribute executes the given access control method before running the main instruction. When the initialize function is called, the access control method (check) is executed prior to the initialize function. The check method accepts a referenced context as argument, then it checks if the signer of the transaction equals the value of the OWNER variable. The require_keys_eq! macro ensures two pubkeys values are equal, if true, it executes the initialize function, else, it reverts with the NotOwner custom error.

Testing the onlyOwner functionality — happy case

In the test below, we are calling the initialize function and signing the transaction using the owner’s keypair:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Day14 } from "../target/types/day14";

describe("day14", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  it("Is called by the owner", async () => {
    // Add your test here.
    const tx = await program.methods
      .initialize()
      .accounts({
        signerAccount: program.provider.publicKey,
      })
      .rpc();

    console.log("Transaction hash:", tx);
  });
});

We called the initialize function and passed the wallet account (local Solana wallet account) in the provider to the signerAccount which has the Signer<'info> struct, to validate that the wallet account actually signed the transaction. Also remember that Anchor secretly signs any transaction using the wallet account in the provider.

Run test anchor test --skip-local-validator , if everything was done correctly, the test should pass: Anchor test passing

Testing if the signer is not the owner — attack case

Using a different keypair that is not the owner to call the initialize function and sign the transaction will throw an error since the function call is restricted to only the owner:

describe("day14", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  let Keypair = anchor.web3.Keypair.generate();

  it("Is NOT called by the owner", async () => {
    // Add your test here.
    const tx = await program.methods
      .initialize()
      .accounts({
        signerAccount: Keypair.publicKey,
      })
      .signers([Keypair])
      .rpc();

    console.log("Transaction hash:", tx);
  });
});

Here we generated a random keypair and used it to sign the transaction. Let’s run test again: anchor test fail due to wrong signer As expected, we got an error, since the signer’s pubkey is not equal to the owner’s pubkey.

Modify the owner

To change the owner in a program, the pubkey assigned to the owner needs to be stored on-chain. However, discussions about “storage” in Solana will be covered in a future tutorial.

The owner can just redeploy the bytecode.

Exercise: Upgrade a program like the one above to have a new owner.

Learn more with RareSkills

This tutorial is chapter 14 in our Solana course.

Originally Published February, 21, 2024

Cross Program Invocation In Anchor

Cross Program Invocation In Anchor Cross Program Invocation (CPI) is Solana’s terminology for a program calling the public function of another program. We’ve already done CPI before when we sent a transfer SOL transaction to the system program. Here is the relevant snippet by way of reminder: pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> […]

Reading Another Anchor Program’s Account Data On Chain

Reading Another Anchor Program’s Account Data On Chain In Solidity, reading another contract’s storage requires calling a view function or the storage variable being public. In Solana, an off-chain client can read a storage account directly. This tutorial shows how an on-chain Solana program can read the data in an account it does not own. […]

#[derive(Accounts)] in Anchor: different kinds of accounts

[derive(Accounts)] in Anchor: different kinds of accounts #[derive(Accounts)] in Solana Anchor is an attribute-like macro for structs that holds references to all the accounts the function will access during its execution. In Solana, every account the transaction will access must be specified in advance One reason Solana is so fast is that it executes transactions […]

Modifying accounts using different signers

Modifying accounts using different signers In our Solana tutorials thus far, we’ve only had one account initialize and write to the account. In practice, this is very restrictive. For example, if user Alice is transferring points to Bob, Alice must be able to write to an account initialized by user Bob. In this tutorial we […]