Writing A Simple Bond Smart Contract

A bond is a good use case for smart contract

What is a bond? Bond is the money that you lend to a company, say USD 1,000 (the principal). Then the company must pay back to you in a specific time, say 3 years (maturity). On top of that the company needs to pay you additional money (the interest) on top of the principal. For example, the company should pay you USD 100 every year. This interest is the incentive for you to lend the money.

So you gave money as much as USD 1,000 to the company. Year 1: the company paid you USD 100. Year 2: the company paid you USD 100. Year 3: the company paid you USD 100 and USD 1,000. So after 3 years, you would get USD 1,300. Whether this is a good deal or not, it depends on the discount rate, etc. There are a plethora of finance theories covering bonds.

So let's write a bond smart contract. Install Mamba. Create a project directory and initialize it as Mamba project directory.


$ mkdir simple_bond; cd simple_bond
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ pip install black-mamba
(.venv) $ mamba init

Write a SimpleBond.vy in contracts directory.


# @version 0.2.3
"""
@license MIT
@title A simple bond smart contract
@author Arjuna Sky Kok
@notice This smart contract enables bonds mechanism in a simple way
@dev Nothing for now
"""

maturity: public(uint256)
annual_return: public(uint256)
principal: public(uint256)

debtor: public(address)
creditor: public(address)

payment_times: uint256
withdraw_times: uint256

@external
def __init__(_maturity: uint256, _annual_return: uint256, _principal: uint256):
    self.maturity = _maturity                                  # years
    self.annual_return = as_wei_value(_annual_return, "ether") # annual return
    self.principal = as_wei_value(_principal, "ether")         # principal
    self.debtor = msg.sender
    self.payment_times = 0

@payable
@nonreentrant("lend")
@external
def lend():
    assert msg.sender != self.debtor
    assert msg.value == self.principal
    self.creditor = msg.sender

@payable
@external
def payback():
    assert self.payment_times < self.maturity
    assert self.creditor != ZERO_ADDRESS
    assert msg.sender == self.debtor

    if self.payment_times == self.maturity - 1:
        assert msg.value == self.annual_return + self.principal
    else:
        assert msg.value == self.annual_return
    self.payment_times += 1

@payable
@external
def withdraw_by_creditor():
    assert self.withdraw_times < self.maturity
    assert self.payment_times > 0
    assert self.withdraw_times <= self.payment_times
    assert msg.sender == self.creditor

    if self.withdraw_times == self.maturity - 1:
      send(self.creditor, self.annual_return + self.principal)
    else:
      send(self.creditor, self.annual_return)
    self.withdraw_times += 1

@payable
@nonreentrant("withdraw_by_debtor")
@external
def withdraw_by_debtor():
    assert msg.sender == self.debtor
    assert self.creditor != ZERO_ADDRESS

    send(self.debtor, self.principal)

When initializing the bond smart contract with __init__, we set the maturity, the annual return, the principal. Both annual return and principal are in ETH. Then we set the debtor as the one who launched this smart contract.

The lend method is called by someone who wants to lend money. The creditor needs to send ETH as much as the principal.

The withdraw_by_debtor method is called by the debtor. Now that the creditor has lend the money, the debtor can use the money for developing a million dollar business.

But the debtor needs to pay the interest rate every year. In this smart contract we would not punish the debtor if they fail to pay the interest rate in time. Maybe in future version. There is a reason I call this smart contract SimpleBond. So the debtor needs to pay the interest rate using payback method. We check if the time is the end of the maturity, the debtor needs to pay the principal as well.

The withdraw_by_creditor method is called by the creditor. The creditor can enjoy the fixed income security. Every year, they receive the interest rate. In the end of the maturity, the creditor get back their money (the principal) on top of the interest rate or annual return.

Run Ganache.

Compile your source code using Mamba.


(.venv) $ mamba compile

Then edit migrations/deploy_SimpleBond.py.


from black_mamba.deploy import DeployContract


private_key = "0bf89b27648bd7fb6ed5478a9865a05968f14b3644153adaa7d603f755a436f5"

deploy_contract_instance = DeployContract()
parameters = [3, 1, 10]
tx_params = { "from": "0x49DFf23da6518ad602f6a4d261f6A41E7FdF7ec6" }

deploy_contract_instance.deploy_contract("SimpleBond", parameters, tx_params, private_key)

Change "0bf89..." to your debtor's private key. Just choose the first account in Ganache. The "0x49DF..." is the address of your debtor.

Here, we initialize a bond with 3 years in maturity, 1 ETH as annual return (interest rate), and 10 ETH as the principal (the money the debtor wants to borrow).

Execute the script.


(.venv) $ python migrations/deploy_SimpleBond.py

Your smart contract has been deployed on Ganache. It's time for the creditor to lend the money. Write decentralized_app/lend_money.py.


from black_mamba.deploy import DeployContract
from web3 import Web3


deployed_contract = DeployContract().deployed_contract("SimpleBond")
tx_params = { "from": "0x5B5e5efB562965abE32e2bEeEA0A897D2393F1a6", "value": Web3.toWei(10, "ether") }
deployed_contract.functions.lend().transact(tx_params)

Change "0x5B5e..." to your creditor's address. We don't need private key when developing in Ganache. Just use the second account in Ganache as your creditor's account. Execute the script.


(.venv) $ python decentralized_app/lend_money.py

Notice that your creditor's account is short of 10 ETH. Now, it's time for the debtor to withdraw the money to expand the business. Write decentralized_app/withdraw_money.py.


from black_mamba.deploy import DeployContract


deployed_contract = DeployContract().deployed_contract("SimpleBond")
tx_params = { "from": "0x49DFf23da6518ad602f6a4d261f6A41E7FdF7ec6"  }
deployed_contract.functions.withdraw_by_debtor().transact(tx_params)

Execute the script.


(.venv) $ python decentralized_app/withdraw_money.py

Notice, the debtor's account got additional 10 ETH. Now that the debtor has used the money for business, the debtor needs to pay the interest rate and the principal. We just combine all payments in one script. In real world, they will take different time, separated by a long time, say, a year. We don't have the patience to wait 3 years. Create decentralized_app/payback.py.


from black_mamba.deploy import DeployContract
from web3 import Web3


deploy_contract = DeployContract()
deployed_contract = deploy_contract.deployed_contract("SimpleBond")

tx_params = { "from": "0x49DFf23da6518ad602f6a4d261f6A41E7FdF7ec6", "value": Web3.toWei(1, "ether")  }
tx_hash = deployed_contract.functions.payback().transact(tx_params)
deploy_contract.w3.eth.waitForTransactionReceipt(tx_hash)

tx_params = { "from": "0x49DFf23da6518ad602f6a4d261f6A41E7FdF7ec6", "value": Web3.toWei(1, "ether")  }
tx_hash = deployed_contract.functions.payback().transact(tx_params)
deploy_contract.w3.eth.waitForTransactionReceipt(tx_hash)

tx_params = { "from": "0x49DFf23da6518ad602f6a4d261f6A41E7FdF7ec6", "value": Web3.toWei(11, "ether")  }
tx_hash = deployed_contract.functions.payback().transact(tx_params)
deploy_contract.w3.eth.waitForTransactionReceipt(tx_hash)

Execute it.


(.venv) $ python decentralized_app/payback.py

Notice, the account of debtor is short of 13 ETH. It's time for the creditor receives the money back including the interest rate. Yeah, profit!!! Write decentralized_app/withdraw_by_creditor.py. Remember, in real world, the creditor would receive the payment not in one chunk but every year (or 6 months, depends on the bond). The interest rate would be paid every year, while the principal would be paid in the end of 3 years. Again, we don't have the patience so we combine them together.


from black_mamba.deploy import DeployContract


deploy_contract = DeployContract()
deployed_contract = deploy_contract.deployed_contract("SimpleBond")

tx_params = { "from": "0x5B5e5efB562965abE32e2bEeEA0A897D2393F1a6"  }
tx_hash = deployed_contract.functions.withdraw_by_creditor().transact(tx_params)
deploy_contract.w3.eth.waitForTransactionReceipt(tx_hash)

tx_params = { "from": "0x5B5e5efB562965abE32e2bEeEA0A897D2393F1a6"  }
tx_hash = deployed_contract.functions.withdraw_by_creditor().transact(tx_params)
deploy_contract.w3.eth.waitForTransactionReceipt(tx_hash)

tx_params = { "from": "0x5B5e5efB562965abE32e2bEeEA0A897D2393F1a6"  }
tx_hash = deployed_contract.functions.withdraw_by_creditor().transact(tx_params)
deploy_contract.w3.eth.waitForTransactionReceipt(tx_hash)

Execute it.


(.venv) $ python decentralized_app/withdraw_by_creditor.py

Notice, the account of the creditor gets back their 10 ETH and the annual return for 3 years, which is 3 ETH.

Later, I'll add the test case, and create a more sophisticated bond using ERC20 token.