The Basics of Smart Contracts 3: Function Types
- andy1265
- Jun 20, 2022
- 4 min read
Updated: Jun 26, 2022
In this installment we will be covering function types. There are three types of function types in Solidity: Pure, View and Payable. As discussed in the previous article both Pure and View functions if called directly do not have an associated cost however when called by other contracts do have an associated cost. Payable functions will always have a cost associated with them. A brief description of what these modifiers do can be seen below:
Pure: functions that do not modify or view the state.
View: functions that do not modify the state but can view it.
Payable functions that have an associated cost.
What is the state?
The state is anything held by the EVM apart from the input parameters of the function. This includes contract variables, events, other contracts, ether and anything else not directly provided as input to the function. The following are all examples of modifying the state:
Writing or updating contract variable.
Constructing other contracts
Self destructing contracts
Emitting events
Making calls to other functions
Examples of things considered to be reading the state are checking balances, getting the values of variables, accessing members of a block or msg.
Setting up the Environment
Environment setup is the same as in the first 2 installments of this series and can be found here and here. The steps are exactly the same for this installment.
Solidity Contract Code
The contract code below has 3 standard methods each with a different type and a constructor. They all do largely the same thing however in slightly different ways. First lets look at the contract code:
pragma solidity ^0.8.11;
contract modifiers {
string public setInConstructor;
constructor() {
setInConstructor = "This is set in the constructor.";
}
function purePrint() public pure returns (string memory) {
return "This is a pure function.";
}
function viewPrint() public view returns (string memory) {
return setInConstructor;
}
function payablePrint() public payable returns (string memory){
require(msg.value == 1 ether);
return "This is from a payable function and cost 1 ether.";
}
}
Unlike in other languages where the constructor has the same name as the class in solidity the constructor is aptly named "constructor". You can set these as public if you wish however unless you set the constructor as abstract the access modifier will be ignored.
In this instance all our constructor does is set the value of a contact variable of type string named "setInConstructor" to the value "This is set in the constructor".
constructor() {
setInConstructor = "This is set in the constructor.";
}
Following on from this we have our 3 function calls. The first of which is a pure function called purePrint. It does not read or alter the state due to the fact that it returns a static string.
function purePrint() public pure returns (string memory) {
return "This is a pure function.";
}
Next we have a view function which reads the value of a contract variable however does not alter anything and as such still does not cost ether. This function is named viewPrint and can be seen below:
function viewPrint() public view returns (string memory) {
return setInConstructor;
}
Finally we have our payable function. This is a very simplistic payable function for demonstration purposes. All this method does is checks that the payment received was 1 ether and returns a static string. This function could be a pure function if it wasn't for the payment requirement.
function payablePrint() public payable returns (string memory){
require(msg.value == 1 ether);
return "This is from a payable function and cost 1 ether.";
}
This function has 2 lines, the first checks that the value of the transaction provided was 1 ether (excluding gas fees) and the second returns the static string.
Now that our contract is written we can deploy it the same way we did in the previous 2 installments simply swap all instances of the contract name in the previous examples with "modifiers" and you are good to go.
Calling the contract with Javascript
This again is very similar to last time however with some differences we will note later in our javascript code. As with last time first grab your ABI:
solcjs --abi path/to/your/greeting.sol
This will create a file with a JSON string which will look something like the following:
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"payablePrint","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"purePrint","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"setInConstructor","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"viewPrint","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}]
Now that we have these two we are ready to start writing our JavaScript. Take the Javascript below and paste it into the file that was just created.
const ethers = require('ethers');
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
const modifiersContractAddress = "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9"; //put your contract address here
const signer = new ethers.Wallet("0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd", ethers.getDefaultProvider("http://localhost:8545"));
const modifiersContractAbi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"payablePrint","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"purePrint","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"setInConstructor","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"viewPrint","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}]
var modifiersContract = new web3.eth.Contract(modifiersContractAbi, modifiersContractAddress);
async function getOutput(){
console.log(await web3.eth.getBalance("0xbDA5747bFD65F08deb54cb465eB87D40e51B197E"));
console.log("Pure method: " + await modifiersContract.methods.purePrint().call());
console.log("View method: " + await modifiersContract.methods.viewPrint().call());
console.log("Payable method: " + await modifiersContract.methods.payablePrint().call({from: signer.address, gas: 30000000, value: web3.utils.toWei('1', 'ether')}));
}
getOutput();
There is really only two lines we need to look at here and that is the signer declaration and the payable function call. The signer declaration is below and simply makes a new wallet based on the provided private key. This is a testnet private key and should never be used on mainnet.
const signer = new ethers.Wallet("0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd", ethers.getDefaultProvider("http://localhost:8545"));
This function is different to ones we have previously looked at because it costs ether and as such requires a from address (the address of the account requesting the function to be called and that will be paying in ether). Note that we use the call method and not send. This is so we get the output of the function and not just a completed transaction receipt.
modifiersContract.methods.payablePrint().call({from: signer.address, gas: 30000000, value: web3.utils.toWei('1', 'ether')}));
Now our JavaScript caller is complete we can test it out and look at the output. Simply run the following and take a look at the output.
node ContractCall.js
You should see output that looks something like the following:
Pure method: This is a pure function.
View method: This is set in the constructor.
Payable method: This is from the payable function and cost 1 ether.
Excellent, we now have seen how different types of solidity functions operate and the differences between them.
Challenges
Add functionality that costs gas to the payable function. See previous installments for ideas.
What happens if you have a pure function call a payable function?
How can you alter the purePrint method to make it so you can call payablePrint from purePrint?
コメント