The Computation of Tomorrow … Today!

Boom Solidity

by: burt rosenberg
at: university of miami
date: august 2024

Overview

The the Hello World program was the first example program in the book The C Programming Language, by Kernighan and Ritchie (page 2). This book is still the best book for learning C. My own favorite program for introducing a computer language is the Boom program. The Boom program uses a loop to count down from ten to zero, prints Boom! and then exits.

In this project we write a Boom program in Solidity programming language to run on the Ethereum Virtual Machine. We then deploy and execute the program to various Ethereum block chains, including,

Solidity is a high level language for writing programs for the Ethereum Virtual Machine (EVM).

The code for this is found on github/the_real_blockchain.

What is a Blockchain

The Ethereum blockchain is a blockchain. A blockchain is a ledger of transactions maintained in sequence as a data structure by a distributed computation. The transactions are organized into blocks, and the blocks are chained together, thus maintaining an agreed upon sequence of transactions.

The distributed computation uses an incentivized consensus protocol to both add to the blockchain, and to validate that all transactions in the blockchain satisfy some common notion of validity. A blockchain is always immutable (in a probabilistic sense, since otherwise the problem is impossible).

The interplay of these elements allow users of the blockchain to rely on the sequence and validity of the transactions while making very few trust assumptions.

The Bitcoin Blockchain uses a Proof of Work consensus protocol, with miners that are incentivized by two sorts of payouts — a coinbase that mints new coins and gives them to the miners, and transaction fees which are volunteered by users. The transactions are of the form of inputs from among the previously unspent transaction outputs (UTXO's), and outputs, that are new UTXO's to be spent in later transactions. The validity constraints include no double spending of transaction outputs, and that the inputs and outputs sum correctly, etc, as well as the use of Public Key cryptography to authorize the use of the inputs and secure the future use of the outputs.

An Ethereum blockchain is a sequence of transitions of a world state. The transitions result from either the execution of contracts by the EVM using the blockchain state, or transfers between Externally Owned Accounts (EOA). Any transaction can be verified as correct, because the a blockchain is immutable. The sequence of state transitions from the chain's inception up to any point can be recomputed by the distrustful observer.

The analog of a Bitcom miner is an Ethereum node. The consensus protocol was originally a Proof of Work protocol, but at The Merge it was replaced by a Proof of Stake protocol.

The Boom Contract

To handle the world state, contracts are object instances that have instance variables and methods. Contracts are given an address on creation, and messages are sent to that address, along with some gas and optionally and amount of ether, denominated in Wei.

The gas is required to pay for the computation. It will be resolved to a payment in ETH, and subtracted from the EOA of who initiated the transaction. Both contracts and EOA can receive ether. In our Boom contract, the count down we will charge a certain amount of ETH for each count. When the count reaches zero, all the accumulated ETH will be sent to the creator of the contract.

The count down method will check the amount of ETH that was sent, and reject if too little was sent, and reimburse the difference if too much was sent. An ethereum transaction is a transaction (in the sense of a database transaction — an all or nothing bundle of state changes). In the case of too little ETH, the contract will initiate a roll back, canceling all changes to the state, including the ETH transfer, although still using whatever gas was required.

In the case of too much ETH being sent, the contract will initiate a send of the excess either back to the sender. If for whatever this fails, the contract will initiate a roll back returning all the ETH to the sender. The EVM will still charge for the gas used.

These roll backs are provided in the Solidity language as,

For this situation, the require statement is the correct one to use, and will create an Error object, and unless this is a caught exception, will roll back the world state, and only charge gas fees.

Note that the ETH cost for max gas is locked at the start of the computation, sort of like a lawyer taking a retainer. What is unused is refunded. If the computation runs out of gas, it is reverted and the cost is covered by the retained ETH amount.

The Solidity Memory Model: data-location specifier

Because the EVM references state stored on the blockchain as well as memory used for execution of the contract which is not on the blockchain, Solidity presents us with a very non-uniform memory model.

Programming on a typical machine presents us with a very uniform memory model of an indexed sequence of bytes. The reality is that a CPU has gradations of memory. There is a hierarchy of memory that spans from the CPU registers, through the caches, the RAM and (in cases of exceptional pressure on virtual memory) block device resident swap space. However, even advanced programmers need to pay very little attention to this.

Not so in Solidity, where various categories of storage have different capabilities and costs. There are the following data-locations to specify,

By default, all instance variables are of type storage. Larger return values should be marked memory. Because they are the cheapest, functions that are directly called from outside the contract can use calldata variables, but if used in subsequent calls, either within the contract or to other contracts, they must be copied to memory.

The EVM is a stack architecture where the bytecodes get their arguments almost exclusively from the stack. The EVM stack has 1024 slots, each slot is 32 bytes. It is accessed by the bytecodes using stack discipline (LIFO). Unless programming Ethereum bytecode, the stack will not be of concern to a programmer, except for its limited size.

Paying a Contract: state-mutability specifier

Every deployed contract, as well as the EOA's that contain public-key secured user balances, has an address and can receive and send ether. An EOA has no contract and can only send and receive either, and initiate actions carried out by the contracts.

For a contract to receive funds, it must have a method marked payable. This is one of three mutually exclusive state-mutability specifiers,

To send ETH, override the the value field in an invocation of a function marked payable.

There are two special methods with reserved names receive and fallback (which are declared without the function tag). Implementing these will make a contract similar to an EOA as an entity that can receive ether without a method call.

Boom.sol

// contracts/Boom.sol

// author: bjr
// last-update: 9 aug 2024

// SPDX-License-Identifier: UNLICENSED
//  ^ means 0.9 etc also acceptable (~ means 0.8.25 also acceptable)
pragma solidity ^0.8.24;


// place this script in ./contracts/
// and compile with npx hardhat compile

import "hardhat/console.sol";

contract Boom {

    uint256 public count;
    address payable public owner;
    
    uint256 constant public TOP_COUNT = 10;
    uint256 constant public STAKE = (1 ether)/1000;
    
    event BoomEvent(address indexed boomer, address indexed owner, string message);
    
    constructor() {
        owner = payable(msg.sender);
        count = 10;
    }

    function getCount() external view returns (uint256) {
        return count;
    }
    
    function getFunds() external view returns (uint256) {
        return address(this).balance;
    }

    // Function to increment count by 1
    function countDown() external payable returns (uint256) {
        uint256 stake = msg.value; 
        require(stake>=STAKE,'Stake 0.001 ETH');
        uint256 d = stake-STAKE; 
        if (d>0) {
            (bool success,) = (msg.sender).call{value: d}("");
            require(success, "Failed to reimburse sender Ether");
        }
        count -= 1;
        if (count<=0) {
            uint256 t = address(this).balance;
            (bool success,) = owner.call{value: t}("");
            require(success, "Failed to send boomed Ether");
            count = 10 ;
            // event
            emit BoomEvent(address(this), owner, "Boom!");
        }
        return 0;
    }
}

Deploy and run Boom on network localhost

To test our contract, we will start up a local blockchain using HardHat. As part of the blockchain creation, HardHat provides several test accounts each with so 10,000 ETH. Run in a separate terminal window, and keep it open.
    $ npx hardhat node
The script to deploy and test is in the following box,

// scripts/boom-test-deploy.js
// author: bjr
// last-update: 13 aug 2024

const CONTRACT_CLASS = 'Boom'

async function main () {
  
    const myAccount = (await ethers.getSigners())[0].address;
    const anAccount = (await ethers.getSigners())[1].address;
    
    const ContractClass = await ethers.getContractFactory(CONTRACT_CLASS);
    const contractInstance = await ContractClass.deploy();
    await contractInstance.waitForDeployment();
    const contractAddress = await contractInstance.getAddress();
    console.log(`Deployed ${CONTRACT_CLASS} to ${contractAddress}`);

    const boom = contractInstance ;
    let count = await boom.getCount();
    let accountFunds = await ethers.provider.getBalance(myAccount);
    let contractFunds = await boom.getFunds();

    for (let i=0;i<10;i++) {
        await boom.countDown({value:ethers.parseEther("0.001")});
        count = await boom.getCount();
        accountFunds = (await ethers.provider.getBalance(myAccount));
        contractFunds = await boom.getFunds();
        console.log(`count: ${count}`);
        console.log(`my funds: ${ethers.formatEther(accountFunds)}`);
        console.log(`contract funds: ${ethers.formatEther(contractFunds)}`);
    }
    
    accountFunds = (await ethers.provider.getBalance(anAccount));
    console.log(`other account funds: ${ethers.formatEther(accountFunds)}`);
}

main().then(
    () => process.exit(0)
	).catch(  
	error => {
        console.error(error);
    	process.exit(1);}
	);

Here is the run of the script.

	
% npx hardhat run --network localhost scripts/boom-test-deploy.js
Deployed Boom to 0xB1c05b498Cb58568B2470369FEB98B00702063dA
count: 9
my funds: 9999.971367501201049543
contract funds: 0.001
count: 8
my funds: 9999.970340499200833527
contract funds: 0.002
count: 7
my funds: 9999.969313497200617511
contract funds: 0.003
count: 6
my funds: 9999.968286495200401495
contract funds: 0.004
count: 5
my funds: 9999.967259493200185479
contract funds: 0.005
count: 4
my funds: 9999.966232491199969463
contract funds: 0.006
count: 3
my funds: 9999.965205489199753447
contract funds: 0.007
count: 2
my funds: 9999.964178487199537431
contract funds: 0.008
count: 1
my funds: 9999.963151485199321415
contract funds: 0.009
count: 10
my funds: 9999.972112407199008791
contract funds: 0.0
other account funds: 10000.0
Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.

author: burton rosenberg
created: 10 aug 2024
update: 14 aug 2024