We have covered bulk modifiers. Modifiers themselves can actually be stacked:
function test() external view onlyOwner anotherModifier { /* ... */ }
payable
functions are part of what makes Solidity and Ethereum so cool — they are a special type of function that can receive Ether.
Let that sink in for a minute. When you call an API function on a normal web server, you can't send US dollars along with your function call — nor can you send Bitcoin.
But in Ethereum, because both the money (Ether), the data (transaction payload), and the contract code itself all live on Ethereum, it's possible for you to call a function and pay money to the contract at the same time.
This allows for some really interesting logic, like requiring a certain payment to the contract in order to execute a function.
An example contract:
contract OnlineStore { function buySomething() external payable { // Check to make sure 0.001 ether was sent to the function call: require(msg.value == 0.001 ether); // If so, some logic to transfer the digital item to the caller of the function: transferThing(msg.sender); } }
Here, msg.value
is a way to see how much Ether was sent to the contract, and ether
is a built-in unit.
From here, someone would make a call from the web3.js
app:
// Assuming `OnlineStore` points to your contract on Ethereum: OnlineStore.buySomething({ from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001), })
Note: If a function is not marked
payable
and you try to send Ether to it as above, the function will reject your transaction.
Here is an example of the implementation to pay to level up a Zombie:
pragma solidity >=0.5.0 <0.6.0; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { uint levelUpFee = 0.001 ether; modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } function levelUp(uint _zombieId) external payable { require(msg.value == levelUpFee); zombies[_zombieId].level++; } function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].name = _newName; } function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].dna = _newDna; } function getZombiesByOwner(address _owner) external view returns(uint[] memory) { uint[] memory result = new uint[](ownerZombieCount[_owner]); uint counter = 0; for (uint i = 0; i < zombies.length; i++) { if (zombieToOwner[i] == _owner) { result[counter] = i; counter++; } } return result; } }
After you send Ether to a contract, it gets stored in the contract's Ethereum account, and it will be trapped there — unless you add a function to withdraw the Ether from the contract.
contract GetPaid is Ownable { function withdraw() external onlyOwner { address payable _owner = address(uint160(owner())); _owner.transfer(address(this).balance); } }
It is important to note that you cannot transfer Ether to an address unless that address is of type address payable
. But the _owner
variable is of type uint160
, meaning that we must explicitly cast it to address payable
.
Once you cast the address from uint160
to address payable
, you can transfer Ether to that address using the transfer
function, and address(this).balance
will return the total balance stored on the contract. So if 100 users had paid 1 Ether to our contract, address(this).balance
would equal 100 Ether.
You can use transfer
to send funds to any Ethereum address. For example, you could have a function that transfers Ether back to the msg.sender
if they overpaid for an item:
uint itemFee = 0.001 ether; msg.sender.transfer(msg.value - itemFee);
Or in a contract with a buyer and a seller, you could save the seller's address
in storage
, then when someone purchases his item, transfer him the fee paid by the buyer: seller.transfer(msg.value)
.
These are some examples of what makes Ethereum programming really cool — you can have decentralized marketplaces like this that aren't controlled by anyone.
In our example, we added the capability to withdraw Ether from the contract as well as set the level up fee:
pragma solidity >=0.5.0 <0.6.0; import "./zombiefeeding.sol"; contract ZombieHelper is ZombieFeeding { uint levelUpFee = 0.001 ether; modifier aboveLevel(uint _level, uint _zombieId) { require(zombies[_zombieId].level >= _level); _; } function withdraw() external onlyOwner { address payable _owner = address(uint160(owner())); _owner.transfer(address(this).balance); } function setLevelUpFee(uint _fee) external onlyOwner { levelUpFee = _fee; } function levelUp(uint _zombieId) external payable { require(msg.value == levelUpFee); zombies[_zombieId].level++; } function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].name = _newName; } function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) { require(msg.sender == zombieToOwner[_zombieId]); zombies[_zombieId].dna = _newDna; } function getZombiesByOwner(address _owner) external view returns(uint[] memory) { uint[] memory result = new uint[](ownerZombieCount[_owner]); uint counter = 0; for (uint i = 0; i < zombies.length; i++) { if (zombieToOwner[i] == _owner) { result[counter] = i; counter++; } } return result; } }
The best source of randomness we have in Solidity is the keccak256
hash function.
You could do something like...
// Generate a random number between 1 and 100: uint randNonce = 0; uint random = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100; randNonce++; uint random2 = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
What this would do is take the timestamp of now
, the msg.sender
, and an incrementing nonce
(a number that is only ever used once, so we don't run the same hash function with the same input parameters twice).
It would then "pack" the inputs and use keccak
to convert them to a random hash. Next, it would convert that hash to a uint
, and then use % 100
to take only the last 2 digits. This will give us a totally random number between 0 and 99
.
This method is vulnerable to attack by a dishonest node.
In Ethereum, when you call a function on a contract, you broadcast it to a node or nodes on the network as a transaction. The nodes on the network then collect a bunch of transactions, try to be the first to solve a computationally-intensive mathematical problem as a "Proof of Work", and then publish that group of transactions along with their Proof of Work (PoW) as a block to the rest of the network.
Once a node has solved the PoW, the other nodes stop trying to solve the PoW, verify that the other node's list of transactions are valid, and then accept the block and move on to trying to solve the next block.
This makes our random number function exploitable.
Let's say we had a coin flip contract — heads you double your money, tails you lose everything. Let's say it used the above random function to determine heads or tails. (random >= 50 is heads, random < 50 is tails).
If I were running a node, I could publish a transaction only to my own node and not share it. I could then run the coin flip function to see if I won — and if I lost, choose not to include that transaction in the next block I'm solving. I could keep doing this indefinitely until I finally won the coin flip and solved the next block, and profit.
Because the entire contents of the blockchain are visible to all participants, this is a hard problem, and its solution is beyond the scope of this tutorial. You can read this StackOverflow thread for some ideas. One idea would be to use an oracle to access a random number function from outside of the Ethereum blockchain.
We won't be using an oracle
here, but in our example we wrote the following code:
pragma solidity >=0.5.0 <0.6.0; import "./zombiehelper.sol"; contract ZombieAttack is ZombieHelper { uint randNonce = 0; function randMod(uint _modulus) internal returns (uint) { randNonce++; return uint(keccak256(abi.encodePacked(now,msg.sender,randNonce))) % _modulus; } }