Writing a Banking Contract

January 5, 2018 by Todd Proebsting

This article will demonstrate how to write a simple, but complete, smart contract in Solidity that acts like a bank that stores ether on behalf of its clients. The contract will allow deposits from any account, and can be trusted to allow withdrawals only by accounts that have sufficient funds to cover the requested withdrawal.

That post demonstrated how to restrict ether withdrawals to an “owner’s” account. It did this by persistently storing the owner account’s address, and then comparing it to the msg.sender value for any withdrawal attempt. Here’s a slightly simplified version of that smart contract, which allows anybody to deposit money, but only allows the owner to make withdrawals:

pragma solidity^0.4.19;contractTipJar{addressowner;// current owner of the contractfunctionTipJar()public{owner=msg.sender;}functionwithdraw()public{require(owner==msg.sender);msg.sender.transfer(address(this).balance);}functiondeposit(uint256amount)publicpayable{require(msg.value==amount);}functiongetBalance()publicviewreturns(uint256){returnaddress(this).balance;}}

Maintaining Individual Account Balances

I am going to generalize this contract to keep track of ether deposits based on the account address of the depositor, and then only allow that same account to make withdrawals of that ether. To do this, we need a way keep track of account balances for each depositing account—a mapping from accounts to balances. Fortunately, Solidity provides a ready-made mapping data type that can map account addresses to integers, which will make this bookkeeping job quite simple. (This mapping structure is much more general key/value mapping than just addresses to integers, but that’s all we need here.)

mapping(address => uint256) public balanceOf; declares a persistent public variable, balanceOf, that is a mapping from account addresses to 256-bit unsigned integers. Those integers will represent the current balance of ether stored by the contract on behalf of the corresponding address.

Mappings can be indexed just like arrays/lists/dictionaries/tables in most modern programming languages.

The value of a missing mapping value is 0. Therefore, we can trust that the beginning balance for all account addresses will effectively be zero prior to the first deposit.

It’s important to note that balanceOf keeps track of the ether balances assigned to each account, but it does not actually move any ether anywhere. The bank contract’s ether balance is the sum of all the balances of all accounts—only balanceOf tracks how much of that is assigned to each account.

Note also that this contract doesn’t need a constructor. There is no persistent state to initialize other than the balanceOf mapping, which already provides default values of 0.

Withdrawals and Account Balances

Given the balanceOf mapping from account addresses to ether amounts, the remaining code for a fully-functional bank contract is pretty small. I’ll simply add a withdrawal function:

The require(amount <= balances[msg.sender]) checks to make sure the sender has sufficient funds to cover the requested withdrawal. If not, then the transaction aborts without making any state changes or ether transfers.

The balanceOf mapping must be updated to reflect the lowered residual amount after the withdrawal.

The funds must be sent to the sender requesting the withdrawal.

Important: Avoiding the Reentrancy Vulnerability

In the withdraw() function above, it is very important to adjust balanceOf[msg.sender]before transferring ether to avoid an exploitable vulnerability. The reason is specific to smart contracts and the fact that a transfer to a smart contract executes code in that smart contract.
(The essentials of Ethereum transactions are discussed in
How Ethereum Transactions Work.)

Now, suppose that the code in withdraw() did not adjust balanceOf[msg.sender] before making the transfer and suppose that msg.sender was a malicious smart contract. Upon receiving the transfer—handled by msg.sender’s fallback function—that malicious contract could initiate another withdrawal from the banking contract. When the banking contract handles this second withdrawal request, it would have already transferred ether for the original withdrawal, but it would not have an updated balance, so it would allow this second withdrawal!

This vulnerability is called a “reentrancy” bug because it happens when a smart contract invokes code in a different smart contract that then calls back into the original, thereby reentering the exploitable contract. For this reason, it’s essential to always make sure a contract’s internal state is fully updated before it potentially invokes code in another smart contract. (And, it’s essential to remember that every transfer to a smart contract executes that contract’s code.)

To avoid this sort of reentrancy bug, follow the “Checks-Effects-Interactions pattern” as described in the Solidity documentation. The withdraw() function above is an example of implementing this pattern.1

Summary

Solidity supports a key/value data type called mapping. The default value associated with a missing key is 0.

Resources

Much more detailed explanations of the reentrancy vulnerability can be found
here,
here, and
here.

Note that transfer and send themselves also mitigate this vulnerability. They forward very little gas to the recipient—so little that a reentrant call is not possible. Nonetheless, we recommend that you follow the Checks-Effects-Interactions pattern as a matter of habit, because other types of interactions do not share this mitigation.
↩