Skip to main content

Function modifiers

The =nil; Solidity library provides several function modifiers that simplify the processing of external messages and async calls.

onlyInternal and onlyExternal

=nil; adds the following function modifiers:

  • onlyInternal
  • onlyExternal
  • onlyResponse

If a function has the onlyInternal modifier, it can only be called by another contract regardless of where this contract is located. A function with the onlyExternal modifier behaves the opposite way: it must always be called via an external message (e.g., from a user).

The onlyResponse modifier limits makes it so a function can only be called as callback function when processing the response to sendRequest().

info

It is technically possible to call a function marked with onlyExternal/onlyInternal modifier with message whose type is opposite to the type in the modifier. However, such a call will produce an error.

tip

To use the onlyInternal, onlyExternal, and onlyResponse function modifiers, the contract must use NilBase as its base contract.

Examples

onlyInternal and onlyExternal

Consider the below examples that simulate on-chain voting.

Contract 1 is a 'ballot' contract that is deployed for every voter:

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

contract Ballot is NilBase {
bytes pubkey;
constructor(bytes memory _pubkey) payable {
pubkey = _pubkey;
}
function vote(address election, string candidate) external onlyExternal {
int feeCredit = 500_000;
bool deploy = false;
uint value = 0;
election.asyncCall(
address(this), feeCredit, value,
abi.encodeWithSignature("requestVote(string)", candidate));
}
function verifyExternal(
uint256 hash,
bytes memory authData
) external view returns (bool) {
return Nil.validateSignature(pubkey, hash, authData);
}
}

Contract 1 contains the voter's public key. The vote() function can only be called externally by the voter for whom the ballot contract was deployed. This is made possible by the verifyExternal() function that validates the caller's signature.

Contract 2 records votes:

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

contract Election {
using Nil for address;
mapping(address => string) public votes;
mapping(address => bool) private voters; // Filled by the contract owner

function requestVote(string candidate) public {
address voter = msg.sender;
if (voters[voter] != true) {
return;
}
votes[voter] = candidate;
}
}

Note that Contract 2 lacks the verifyExternal() function and, therefore, cannot accept external messages. This means that requestVote() does not need to have the onlyInternal modifier. Vote verification in Contract 2 can only be initiated by another contract.

Contract 1 makes an async call to Contract 2 requesting vote registration. If the caller is a valid voter, the votes map is updated to include the new vote for the specified candidate. Careful use of modifiers and the verifyExternal() function means that the voting system cannot be abused: votes cannot be requested directly, and additional verification occurs at the level of the election contract.

onlyResponse

The following escrow contract uses onlyResponse to limit access to its resolve() function:

pragma solidity ^0.8.9;

import "@nilfoundation/smart-contracts/contracts/Nil.sol";

contract Escrow is NilBase {
using Nil for address;
mapping(address => uint256) private deposits;

function deposit() public payable {
deposits[msg.sender] += msg.value;
}

function submitForVerification(
address validator,
address participantOne,
address participantTwo
) public payable {
bytes memory context = abi.encodeWithSelector(
this.resolve.selector,
participantOne,
participantTwo,
msg.value
);
bytes memory callData = abi.encodeWithSignature(
"validate(address, address)",
participantOne,
participantTwo
);
Nil.sendRequest(
validator,
0,
Nil.ASYNC_REQUEST_MIN_GAS,
context,
callData
);
}

function resolve(
bool success,
bytes memory returnData,
bytes memory context
) public payable onlyResponse {
require(success, "Request failed!");
(address participantOne, address participantTwo, uint256 value) = abi
.decode(context, (address, address, uint256));
bool isValidated = abi.decode(returnData, (bool));
if (isValidated) {
deposits[participantOne] -= value;
deposits[participantTwo] += value;
}
}

function verifyExternal(
uint256 messageHash,
bytes calldata authData
) external view returns (bool) {
return true;
}
}