Codementor Events

Exercise: Simple Solidity Smart Contract for Ethereum Blockchain

Published Aug 08, 2018Last updated Sep 25, 2019
Exercise: Simple Solidity Smart Contract for Ethereum Blockchain

This is a modified version of the typical smart contract example Simple Bank using Solidity, like: https://github.com/ScottWorks/ConsenSys-Academy-Notes/blob/master/Smart-Contracts/Simple-Bank-Exercise/simple_bank/contracts/SimpleBank.sol

However, instead of rewarding all clients that enroll like the original example, which means that the bank contract should hold all that Ether beforehand for the rewards, it only rewards the 3 first clients with 10 ether each. This way it can opperate without the problem of running out of Ether for the rewards.

In addition to enrolling, client can also deposit and withdraw ether and read their balance. Finally, the contract features an additional method to check the overall balance of all deposits in the contract.

The complete source code for this exercise is available from: https://github.com/rogargon/simple_bank

Requirements

For this exercise, the root requirements and Node and its pacakage manager npm. To install them follow these intructions: https://www.npmjs.com/get-npm

We will then use the Truffle smart contract development framework to bootstrap the development process and take care of deployment and testing.

To install Truffle, once we have Node and npm:

npm install -g truffle

NOTE: if running Truffle on Windows, there might be some naming conflicts that could prevent Truffle from executing properly as detailed in:
https://truffleframework.com/docs/advanced/configuration#resolving-naming-conflicts-on-windows

Simple Bank Smart Contract Development

To bootstrap the development process, we will use Truffle to generate the skeleton of our smart contract project. To do that, create a directory for your project, move there and execute Truffle initialization as folows:

mkdir simple_bank
cd simple_bank
truffle init

This generates a smart project structure similar to the one described in https://truffleframework.com/docs/getting_started/project#exploring-the-project

In our case, it is an empty project without an initial example contract like MetaCoin.sol

Therefore, first we create a new file in the "contracts/" folder called "SimpleBank.sol" with the following content:

pragma solidity ^0.5.8;

contract SimpleBank {
    uint8 private clientCount;
    mapping (address => uint) private balances;
    address public owner;

  // Log the event about a deposit being made by an address and its amount
    event LogDepositMade(address indexed accountAddress, uint amount);

    // Constructor is "payable" so it can receive the initial funding of 30, 
    // required to reward the first 3 clients
    constructor() public payable {
        require(msg.value == 30 ether, "30 ether initial funding required");
        /* Set the owner to the creator of this contract */
        owner = msg.sender;
        clientCount = 0;
    }

    /// @notice Enroll a customer with the bank, 
    /// giving the first 3 of them 10 ether as reward
    /// @return The balance of the user after enrolling
    function enroll() public returns (uint) {
        if (clientCount < 3) {
            clientCount++;
            balances[msg.sender] = 10 ether;
        }
        return balances[msg.sender];
    }

    /// @notice Deposit ether into bank, requires method is "payable"
    /// @return The balance of the user after the deposit is made
    function deposit() public payable returns (uint) {
        balances[msg.sender] += msg.value;
        emit LogDepositMade(msg.sender, msg.value);
        return balances[msg.sender];
    }

    /// @notice Withdraw ether from bank
    /// @return The balance remaining for the user
    function withdraw(uint withdrawAmount) public returns (uint remainingBal) {
        // Check enough balance available, otherwise just return balance
        if (withdrawAmount <= balances[msg.sender]) {
            balances[msg.sender] -= withdrawAmount;
            msg.sender.transfer(withdrawAmount);
        }
        return balances[msg.sender];
    }

    /// @notice Just reads balance of the account requesting, so "constant"
    /// @return The balance of the user
    function balance() public view returns (uint) {
        return balances[msg.sender];
    }

    /// @return The balance of the Simple Bank contract
    function depositsBalance() public view returns (uint) {
        return address(this).balance;
    }
}

Deployment

To instruct Truffle to deploy the previous contract, the following script "2_deploy_contracts.js" should be added to the "migrations/" folder. The deployment should include sending 30 ether (equivalent to 30000000000000000000 wei) to the constructor, which is marked as "payable" to receive this initial balance in the contract.

var SimpleBank = artifacts.require("./SimpleBank.sol");

module.exports = function(deployer) {
  deployer.deploy(SimpleBank, { value: 30000000000000000000 });
};

We will then deploy the contract to a local development network also provided by truffle. The command to start this testing network is:

truffle develop

This will start Truffle Develop at http://127.0.0.1:9545 together with 10 sample accounts.

Then, compile the contracts in a different terminal (truffle develop keeps running the network):

truffle compile

If there are no errors, the contracts can be deployed to the local development network. To configure this network, the file "truffle-config.js" should contain, at least:

module.exports = {
  networks: {
      development: {
      host: "127.0.0.1",
      port: 9545,
      network_id: "*",
    },
  },
}

Then, to trigger the deployment of the smart contract:

truffle migrate

Testing

The previously deployed contract can be now tested to check if it works as expected. We will also use Truffle for testing.

Tests can be written also in Solidity or using JavaScript. In our case we use JavaScript and the following code is in the file "simpleBank.test.js" in the "test/" folder:

var SimpleBank = artifacts.require("./SimpleBank.sol");

const ether = 10**18; // 1 ether = 1000000000000000000 wei
const reward = 10 * ether;
const initialDepositsBalance = 30 * ether;

contract("SimpleBank - basic initialization", function(accounts) {
  const alice = accounts[1];
  const bob = accounts[2];
  const charlie = accounts[3];
  const dave = accounts[4];

  it("should reward 3 first clients with 10 balance", async () => {
    const bank = await SimpleBank.deployed();

    await bank.enroll({from: alice});
    const aliceBalance = await bank.balance({from: alice});
    assert.equal(aliceBalance, reward, "initial balance is incorrect");

    await bank.enroll({from: bob});
    const bobBalance = await bank.balance({from: bob});
    assert.equal(bobBalance, reward, "initial balance is incorrect");

    await bank.enroll({from: charlie});
    const charlieBalance = await bank.balance({from: charlie});
    assert.equal(charlieBalance, reward, "initial balance is incorrect");

    await bank.enroll({from: dave});
    const daveBalance = await bank.balance({from: dave});
    assert.equal(daveBalance, 0, "initial balance is incorrect");

    const depositsBalance = await bank.depositsBalance();
    assert.equal(depositsBalance, initialDepositsBalance, "initial balance is incorrect");
  });

  it("should deposit correct amount", async () => {
    const bank = await SimpleBank.deployed();
    const deposit = 2 * ether;

    const receipt = await bank.deposit({from: alice, value: web3.utils.toBN(deposit)});

    const balance = await bank.balance({from: alice});
    assert.equal(balance, reward + deposit,
        "deposit amount incorrect, check deposit method");
    const depositsBalance = await bank.depositsBalance();
    assert.equal(depositsBalance, initialDepositsBalance + deposit,
        "bank deposits balance should be increased");

    const expectedEventResult = {accountAddress: alice, amount: deposit};
    assert.equal(receipt.logs[0].args.accountAddress, expectedEventResult.accountAddress,
        "LogDepositMade event accountAddress property not emitted");
    assert.equal(receipt.logs[0].args.amount, expectedEventResult.amount,
        "LogDepositMade event amount property not emitted");
  });
});

contract("SimpleBank - proper withdrawal", function(accounts) {
  const alice = accounts[1];

  it("should withdraw correct amount", async () => {
    const bank = await SimpleBank.deployed();
    const deposit = 5 * ether;

    await bank.deposit({from: alice, value: web3.utils.toBN(deposit)});
    await bank.withdraw(web3.utils.toBN(deposit), {from: alice});

    const balance = await bank.balance({from: alice});
    assert.equal(balance, deposit - deposit, "withdraw amount incorrect");
  });
});

contract("SimpleBank - incorrect withdrawal", function(accounts) {
  const alice = accounts[1];

  it("should keep balance unchanged if withdraw greater than balance", async() => {
    const bank = await SimpleBank.deployed();
    const deposit = 3 * ether;

    await bank.deposit({from: alice, value: web3.utils.toBN(deposit)});
    await bank.withdraw(web3.utils.toBN(deposit + 1*ether), {from: alice});

    const balance = await bank.balance({from: alice});
    assert.equal(balance, deposit, "balance should be kept intact");
  });
});

contract("SimpleBank - fallback works", function(accounts) {
  const alice = accounts[1];

  it("should revert ether sent to this contract through fallback", async() => {
    const bank = await SimpleBank.deployed();
    const deposit = 3 * ether;

    try {
      await bank.send(web3.utils.toBN(deposit), {from: alice});
    } catch(e) {
      assert(e, "Error: VM Exception while processing transaction: revert");
    }

    const depositsBalance = await bank.depositsBalance();
    assert.equal(depositsBalance, initialDepositsBalance, "balance should be kept intact");
  });
});

To run the previous test, using Truffle, do:

truffle test

If everything is fine, this should be the expected output:

Using network 'development'.

  Contract: SimpleBank - basic initialization
    ✓ should reward 3 first clients with 10 balance (168ms)
    ✓ should deposit correct amount (63ms)

  Contract: SimpleBank - proper withdrawal
    ✓ should withdraw correct amount (63ms)

  Contract: SimpleBank - incorrect withdrawal
    ✓ should keep balance unchanged if withdraw greater than balance (73ms)

  Contract: SimpleBank - fallback works
    ✓ should revert ether sent to this contract through fallback

  5 passing (482ms)

The complete source code for this exercise is available from: https://github.com/rogargon/simple_bank

Interacting with the Contract

To test the contract interactively in a testing environment, without requiring us to spend any ether, we can use the Remix online IDE tool. No need to install anything, just browse to: https://remix.ethereum.org

Just, copy and paste the content of SimpleBank.sol in Remix. There shouldn't be any compilation error so we can switch to the "Deploy" tab to deploy it in a "JavaScript VM" simulated Ethereum blockchain from the first of the available accounts, which is preloaded with 100 ether. Before deploying, we should set the value of the deployment transaction to 30 ether so we provide the required initial funding for rewards, as shown in the screenshot:

Remix SimpleBank Deploy

After deployment, we can observe that the amount of ether in the deployment account has been reduced to less that 70 ether, due to the 30 ether transferred to the bank contract plus the transaction gas cost. We can also see the contract in the "Deployed Contracts" section, as shown below:

Remix SimpleBank Deployed

Now, we can start interaction with the bank contract. After selecting the second account, we can enroll it by clicking the "enroll" button in the deployed contract. This will reward the new client with 10 ether, though it will be stored in the contract so the balance of the account should be slightly below the initial 100 ether due to the cost of the enrollment transaction, as shown in the figure:

Remix SimpleBank Enrolled

After the reward, the second account has 10 ether in the bank. Therefore, part of it can be withdrawn. To withdraw half of it, we can use the "withdraw" button after specifying in the associated input box that the amount to withdraw is 5000000000000000000, this is in wei and equivalent to 5 ether. After doing so, the amount of ether held by the second account should be slightly below 105 ether, as shown in the screenshot:

Remix SimpleBank Withdrawal

Finally, we can check that the balance for the second account in the bank is the remaining part of the reward. We check the balance using the blue button "balance", which corresponds to a read operation that does not have gas cost. The result should be 5000000000000000000 wei, so 5 ether, as shown below:

Remix SimpleBank Balance

You can continue experimenting with Remix and interacting with the contract. For instance getting the balance of all the deposits in the contract, which should be right now 25 ether, or enrolling other accounts.

After some experimentation you might encounter some strange behaviours. For instance, the second account can enroll again and, if all rewards havent been already taken, his balance will reset to 10 ether...

What can be done in the enroll method to avoid that clients re-enroll?

Source Code

The source code for this exercise is available from:
https://github.com/rogargon/simple_bank

Cover picture by https://pixabay.com/photo-3019120/ (CC0)

Discover and read more posts from Roberto García
get started
post comments7Replies
Izad Imran Tan
4 years ago

This post was very helpful in helping me practice Solidity! Just a note, when writing the tests in JS for this example, shouldn’t we call the functions from the contract like so:

MyContract.methodName.call({from: abc}) vs MyContract.methodName({from: abc})

Just needed some clarification (I’m new to web3 and Solidity so pardon me!) on how we should be calling contract functions with web3. Thanks in advance!

Roberto García
4 years ago

The syntax with MyContract.methodName(…).call(…) is for the web3.js library: https://web3js.readthedocs.io/en/v1.2.11/web3-eth-contract.html#methods-mymethod-call

In this case, we are using Truffle and the Contract class it provides, which simplifies things a little. More details: https://www.trufflesuite.com/docs/truffle/getting-started/interacting-with-your-contracts

tommy shelby1221
6 years ago

The given post will be very useful as the thing is that the given exercise has to be implemented in a way that it will be easy for the users to get it understood.

https://itunessupport.org/blog/fix-itunes-error-9/

Michael Kohl
6 years ago

Your withdraw function has a potential re-entrancy problem. To remedy this you should follow the Check-Effects-Interactions pattern, so something like

function withdraw(uint withdrawAmount) public returns (uint remainingBal) {
    // Check enough balance available, otherwise just return balance
    if (withdrawAmount <= balances[msg.sender]) {
        balances[msg.sender] -= withdrawAmount;
        msg.sender.transfer(withdrawAmount);            
    }
    
    return balances[msg.sender];
}

If the transfer fails the state changes will be rolled back anyway. BTW, Remix actually generates a warning for this, as it does for using require where you should be using assert (documentation).

Roberto García
6 years ago

Thank you, Michael, you’re right. Updated the post accordingly.

Regarding “require” vs. “assert”, in this case, require seems more appropriate as we are evaluating contract inputs. Remix always warns about checking “require” and “assert” uses…

Moreover, we can use here and additional “require” to check if the balance minus the withdrawn amount is smaller than the initial balance. Otherwise, it should point to a potential Integer Underflow problem that should be avoided.

Michael Kohl
6 years ago

I know Remix warns about this a lot. I’m on the fence re the require, if the goal of the contract is to reward the first 3 users with 10 ETH each, then it could be considered an invariant, since the contract doesn’t follow specifications otherwise. Mostly semantics though. :)

Regarding the underflow, one could just use SafeMath from OpenZeppelin, but I guess that’s outside the scope of this article (though might be worth a mention).

Roberto García
6 years ago

Good points. Out of the scope of this post, my first one on the topic, but something to consider for follow-ups. Thanks

Show more replies