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 Contract
  • msg.value
  • msg.data
  • tx.origin — address of originating EOA
  • block.blockhash(blockNumber)
  • block.number
  • block.timestamp
  • address.balance
  • address.transfer(value) — send value in wei and raise exception on error
  • address.send(value) — send value in wei and return false on error
  • address.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 transaction
  • external — can be called by other contracts, from within the contract using this, by EOA transaction
  • internal — can be called from within the contract and by derived contracts
  • private — can be called from within the contract only

Behaviour:

  • view — promises to not modify any state
  • pure — neither reads or writes variables in storage
  • payable — 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.


  1. Ethereum Improvement Proposal