Ethereum Smart Contracts
Intro: Ethereum
Properties
- Can only be deployed by EOA
- Immutable — can't be changed once deployed(but can be deleted with a
SELFDESTRUCT
opcode) - Deterministic — result is the same for everyone
- No parallelism, behaves like a single-threaded machine
- Transactions are atomic, on error everything is rolled back
- Fails if it doesn't have a
payable
function, but the value is sent, the value amount is returned
Lifecycle
Solidity code → compiled into EVM bytecode → deployed to address 0x0
Predefined variables
msg.sender
— addrss of an originating EOA or Smart Contractmsg.value
msg.data
tx.origin
— address of originating EOAblock.blockhash(blockNumber)
block.number
block.timestamp
address.balance
address.transfer(value)
— send value in wei and raise exception on erroraddress.send(value)
— send value in wei and return false on erroraddress.call(data)
— send data and return false on error
Function definition
function name(params) public|private|internal|external pure|view|payable [modifiers] [returns (return types)]
Visibility:
public
— (default) can be called by other contracts, from within the contract, by EOA transactionexternal
— can be called by other contracts, from within the contract usingthis
, by EOA transactioninternal
— can be called from within the contract and by derived contractsprivate
— can be called from within the contract only
Behaviour:
view
— promises to not modify any statepure
— neither reads or writes variables in storagepayable
— accepts incoming payments
Smart Contracts
Remix IDE: add contract to Files, Compile, Connect MetaMask, Deploy
Simple Faucet
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Faucet {
event Transfer(address indexed from, address indexed to, uint amount);
receive() external payable {}
function withdraw(uint amount) external {
require(amount <= 0.1 ether);
payable(msg.sender).transfer(amount);
emit Transfer(address(this), msg.sender, amount);
}
}
Events are recorded in the smart contract event logs. event
declares the event, emit
fires the event. This simple faucet contract can receive funds with a help of a payable
function and can send ether to the callers. require
is a guard preventing from withdrawing higher amounts.
Destroyable Contract
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Faucet {
address payable owner;
event Deposit(address indexed from, uint amount);
event Withdraw(address indexed to, uint amount);
constructor() {
owner = payable(msg.sender);
}
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint amount) external {
require(amount <= 0.1 ether);
payable(msg.sender).transfer(amount);
emit Withdraw(msg.sender, amount);
}
function destroy() external {
require(owner == msg.sender);
payable(msg.sender).transfer(address(this).balance);
selfdestruct(owner);
}
}
Constructor is called on contract creation and saves the owner of the smart contract. This contract splits deposit and withdawal events. And finally this contract can be destroyed, the guard makes sure only the owner of the contract can do that.
"Casino" Contract
// SPDX-License-Identifier: UNLICENSED
// use 35000 gas!
pragma solidity ^0.8.0;
contract Casino {
address payable owner;
event Outcome(string outcome, bytes32 blockhash_bytes, address indexed caller, uint amount);
constructor() {
owner = payable(msg.sender);
}
receive() external payable {}
function bet() external payable {
uint amount = msg.value;
require(amount <= address(this).balance / 3, "The bet is too high");
uint block_number = block.number - 1;
uint rand = uint(blockhash(block_number)) % 2;
if(rand == 0) {
emit Outcome("Lose", blockhash(block_number), msg.sender, amount);
} else {
payable(msg.sender).transfer(amount * 2);
emit Outcome("Win", blockhash(block_number), msg.sender, amount);
}
}
function destroy() external {
require(msg.sender == owner);
owner.transfer(address(this).balance);
selfdestruct(owner);
}
}
This contract is written for fun — the function bet()
is payable, meaning it accepts a value
, which is an amount you're betting. The player then has 50% chance of winning double that amount. Finding a source of random in a determenistic system is a challenging problem that this example is not attempting to solve. Of course a block number or even a timestamp must not be used as a source of randomness, since they can be predicted in advance.
ERC20
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
interface IERC20 {
function totalSupply() external returns(uint);
function balanceOf(address account) external returns(uint);
function transfer(address recipient, uint amount) external returns(bool);
function allowance(address owner, address spender) external returns(uint);
function approve(address spender, uint amount) external returns(bool);
function transferFrom(address sender, address recipient, uint amount) external returns(bool);
event Transfer(address indexed from, address indexed to, uint amount);
event Approval(address indexed owner, address indexed spender, uint amount);
}
contract ERC20 is IERC20 {
address payable owner;
uint public totalSupply;
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;
string public name = "Rustam Test Token";
string public symbol = "RTT";
uint8 public decimals = 18;
constructor() { owner = payable(msg.sender); }
function transfer(address recipient, uint amount) external returns(bool) {
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
function approve(address spender, uint amount) external returns(bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address sender, address recipient, uint amount) external returns(bool) {
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}
function mint(uint amount) external {
require(owner == msg.sender);
balanceOf[msg.sender] += amount;
totalSupply += amount;
emit Transfer(address(0), msg.sender, amount);
}
function burn(uint amount) external {
balanceOf[msg.sender] -= amount;
totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
function destroy() external {
require(owner == msg.sender);
selfdestruct(owner);
}
}
The list of functions a standard ERC20-contract must implement is described in EIP201. In simple words the contract allows to create(mint) or destroy(burn) unlimited amount of tokens keeping track of the totalSupply
, it also allows to transfer tokens and delegate the ownership of the tokens to the other address, which is tracked within the allowance
map.
After this contract is deployed it is possible to keep track of the tokens in MetaMask. To do this, go to the Account → Assets → Import Tokens and enter the address of the contract.
-
Ethereum Improvement Proposal ↩