Is it possible to check what contract functions are called in a transaction from a predicate?

Let’s say, I have a deployed contract with the following ABI:

abi TestContract {
    #[storage(read, write)]
    fn one();

    #[storage(read)]
    fn two();

    #[storage(write)]
    fn three();
}

And I want to write a predicate that would check that a specific function of this contract was called within the transaction. Something like:

predicate;

fn check_function_called(contract_id: ContractId, function_name: String) -> bool {
  ???
}

fn main() -> bool {
    check_function_called(CONTRACT_ID, "two")
}

This predicate should return true if and only if the function “two” was called.

From my understanding of predicates, that should be possible to do, but I don’t understand how to implement it

2 Likes

You can enforce this by having a script that calls that function, then have the predicate check that it’s the same script by verifying the script hash.

This is the technique we’ve used in this project (still WIP):

I’ll try to simplify the logic here, first create a script:

script;

use std::constants::ZERO_B256;
use shared::{Mint};

configurable {
    NFT_CONTRACT: ContractId = ContractId::from(ZERO_B256),
}

fn main(recipient: Address) {
    let nft_contract = abi(Mint, NFT_CONTRACT.into());
    let _out = nft_contract.mint(Identity::Address(recipient));
}

Then create a predicate that verifies the script:

predicate;

use std::{
    constants::ZERO_B256,
    tx::tx_script_bytecode_hash,
};

configurable {
    EXPECTED_SCRIPT_BYTECODE_HASH: b256 = ZERO_B256,
}

fn main() -> bool {
    tx_script_bytecode_hash() == EXPECTED_SCRIPT_BYTECODE_HASH
}

Then you just need to make sure your code generates the correct script & predicates, filling in the configurables. This is roughly how we implemented it:

    let configurables = NFTScriptConfigurables::new()
        .with_NFT_CONTRACT(nft);
    let script = NFTScript::new(account.clone(), "./nft_script/out/debug/nft_script.bin")
        .with_configurables(configurables);

    let mut hasher = Sha256::new();
    hasher.update(
        script
            .main(account.address())
            .script_call
            .script_binary,
    );
    let script_hash = Bits256(hasher.finalize().into());


    let configurables = GasPredicateConfigurables::new()
        .with_EXPECTED_SCRIPT_BYTECODE_HASH(script_hash);

    let predicate: Predicate =
        Predicate::load_from("../gas_predicate/out/debug/gas_predicate.bin")
            .unwrap()
            .with_configurables(configurables);
2 Likes

Awesome, thank you! I’ll try it out

That’s only valid if the entire transaction is predefined, right? Is it possible to check if a specific function was called in case it’s only one step in the transaction (eg a multicall transaction).

Modifying your example, I can have two scripts:

script;

use std::constants::ZERO_B256;
use shared::{Mint};

configurable {
    NFT_CONTRACT: ContractId = ContractId::from(ZERO_B256),
}

fn main(recipient: Address) {
    let nft_contract = abi(Mint, NFT_CONTRACT.into());
    let _out = nft_contract.mint(Identity::Address(recipient));
}

and

script;

use std::constants::ZERO_B256;
use shared::{Mint};

configurable {
    NFT_CONTRACT: ContractId = ContractId::from(ZERO_B256),
}

fn main(recipient: Address, recipient2: Address) {
    let nft_contract = abi(Mint, NFT_CONTRACT.into());
    let _out = nft_contract.mint(Identity::Address(recipient));
    nft_contract.transfer(_out.tokenId, Identity::Address(recipient2));
}

Both calling the same function nft_contract.mint.
Can I check that this function was called from within a predicate for any of these scripts?

1 Like

It’s theoretically possible, you could introspect the script bytecode and just try to validate part of it. But it would be a bit complicated, it’s not something we have an example for yet.

Also there would be ways to “trick” the validation, by inserting bytecode into the script that gets skipped over.

2 Likes