Predecessor vs signer
NEAR access control: authorize with env::predecessor_account_id(), not signer_account_id() — the NEAR equivalent of the Solidity tx.origin phishing bug.
Intermediate5 min read3-question check
Every NEAR function call knows two accounts. env::signer_account_id() is the account that signed the original transaction. env::predecessor_account_id() is the account that directly called this method — a user for a direct call, or a contract when the call came through a cross-contract promise.
They are the same for a plain user → contract call, which is why the wrong one slips through testing. They differ the moment another contract sits in the middle.
The bug#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Wallet {
address public owner;
constructor() { owner = msg.sender; }
function withdrawAll(address payable to) external {
// BUG: tx.origin is whoever signed the tx,
// even if a malicious contract made this call
require(tx.origin == owner, "not owner");
to.transfer(address(this).balance);
}
}#[near]
impl Treasury {
pub fn withdraw_all(&mut self, to: AccountId) -> Promise {
// BUG: signer is whoever signed the original
// transaction, even if evil.near made this call
require!(env::signer_account_id() == self.owner, "not owner");
let amount = env::account_balance()
.saturating_sub(NearToken::from_near(1)); // keep a reserve
Promise::new(to).transfer(amount)
}
}tx.origin ↔ env::signer_account_id(); msg.sender ↔ env::predecessor_account_id(). The bug and the fix are identical on both chains.
Why it happens on NEAR#
The fix#
use near_sdk::{env, near, require, AccountId, NearToken, PanicOnDefault, Promise};
#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Treasury {
owner: AccountId,
}
#[near]
impl Treasury {
#[init]
pub fn new(owner: AccountId) -> Self {
Self { owner }
}
pub fn withdraw_all(&mut self, to: AccountId) -> Promise {
self.assert_owner();
let amount = env::account_balance().saturating_sub(NearToken::from_near(1));
Promise::new(to).transfer(amount)
}
}
impl Treasury {
fn assert_owner(&self) {
// the account that called THIS method, not the tx signer
require!(env::predecessor_account_id() == self.owner, "not owner");
}
}Check yourself
3 questions · progress saved in this browser