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 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);
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?
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.