Part 10: Dependencies
=====================
.. contents:: :local:
So far we have worked with, and deployed, one contract at a time.
However, Solidity allows you to inherit
from other contracts, which is especially useful when you a more generic functionality in
a basic core contract, a functionality you want to inherit and re-use in another, more specific use case.
A Contract that is Controlled by it's Owner
-------------------------------------------
The ``Donator2`` contract is better than ``Donator``, because it allows to withdraw the donations that the contract accepts.
But not much better: at any given moment *anyone* can withdraw the entire donations balance, no questioned asked.
If you recall the withdraw function from the contract:
.. code-block:: solidity
//demo only allows ANYONE to withdraw
function withdrawAll() external {
require(msg.sender.send(this.balance));
}
The ``withdrawAll`` function just sends the entire balane, ``this.balance``,
to whoever request it. Send everything to ``msg.sender``, without any further verification.
A better implemntation would be to allow **only the owner of the contract to withdraw the funds**. An *owne*, in the Ethereum
world, is an *account*. An *address*. The owner is the account that the contract creation transaction was sent from.
Restricting persmission to run some actions only to the owner is a very common design pattern. There are many
open-source implemntations of this pattern, and we will work here with the `OpenZeppelin `_ library.
.. note::
When you adapt an open sourced Solidity contract from github, or elsewhere, be careful. You should trust only
respected, known authors, and always review the code yourself. Smart contracts can induce some smart bugs,
which can lead directly to loosing money. Yes, the Ethereum platform has many innovations,
but loosing money from software bugs is not one of them. However, it's not fun to loose Ether like this. Be careful from intentional or unintentional bugs.
See some really clever examples in
`Underhanded Solidity Coding Contest `_
This is the OpenZeppelin `Ownable.sol `_ Contract:
.. literalinclude:: ./assets/Ownable.sol
:language: solidity
Save it to your conatracts directory:
.. code-block:: shell
$ nano contracts Ownable.sol
Inheritence & Imports
---------------------
Create the new improved ``Donator3``:
.. literalinclude:: ./assets/Donator3.sol
:language: solidity
Almost the same code of ``Donator2``, with 3 important additions:
**[1]** An import statement: ``import "./Ownable.sol" ``: Used to when you use constructs
from other source files, a common practice in
almost any programming language. The format of local path with ``./Filename.sol``
is used for an import of a file in the same directory.
.. note::
Solidity supports quite comprehensive import options. See the Solidity documentation
of `Importing other Source Files `_
**[2]** Subclassing: ``contract Donator3 is Ownable {...}``
**[3]** Use a parent member in the subclass:
.. code-block:: solidity
function withdrawAll() external onlyOwner {
require(msg.sender.send(this.balance));
}
The ``onlyOwner`` modifier was *not* defined in ``Donator3``, but it is inhereted,
and thus can be used in the subclass.
Your contracts directory should look as follows:
.. code-block:: shell
$ ls contracts
Donator2.sol Donator3.sol Donator.sol Greeter.sol Ownable.sol
Compile the project:
.. code-block:: shell
$ populus compile
> Found 5 contract source files
- contracts/Donator.sol
- contracts/Donator2.sol
- contracts/Donator3.sol
- contracts/Greeter.sol
- contracts/Ownable.sol
> Compiled 5 contracts
- contracts/Donator.sol:Donator
- contracts/Donator2.sol:Donator2
- contracts/Donator3.sol:Donator3
- contracts/Greeter.sol:Greeter
- contracts/Ownable.sol:Ownable
Compilation works, ``solc`` successfuly found ``Ownable.sol`` and imported it.
Testing the Subclass Contract
-----------------------------
The test is similar to a regular contract test. All the parents' memebers are inherited and available for testing
(if a parent member was overidden, use ``super`` to access the parent member)
Add a test:
.. code-block:: shell
$ nano tests/test_donator3.py
The test should look as follows:
.. code-block:: python
import pytest
from ethereum.tester import TransactionFailed
ONE_ETH_IN_WEI = 10**18
def test_ownership(chain):
donator3, deploy_tx_hash = chain.provider.get_or_deploy_contract('Donator3')
w3 = chain.web3
owner = w3.eth.coinbase # alias
# prep: set a second test account, unlocked, with Wei for gas
password = "demopassword"
non_owner = w3.personal.newAccount(password=password)
w3.personal.unlockAccount(non_owner,passphrase=password)
w3.eth.sendTransaction({'value':ONE_ETH_IN_WEI,'to':non_owner,'from':w3.eth.coinbase})
# prep: initial contract balance
donation = 42 * ONE_ETH_IN_WEI
effective_usd_rate = 5
transaction = {'value': donation, 'from':w3.eth.coinbase}
donator3.transact(transaction).donate(effective_usd_rate)
assert w3.eth.getBalance(donator3.address) == donation
# test: non owner withdraw, should fail
with pytest.raises(TransactionFailed):
donator3.transact({'from':non_owner}).withdrawAll()
assert w3.eth.getBalance(donator3.address) == donation
# test: owner withdraw, ok
donator3.transact({'from':owner}).withdrawAll()
assert w3.eth.getBalance(donator3.address) == 0
The test is similar to the previous tests. Py.test and the Populus plugin provide
a ``chain`` fixture, as an argument to the test function which is a handle to the ``tester`` ephemeral chain.
``Donator3`` is deployed, and the test function gets a contract object to ``doantor3``.
Then the test creates a second account, ``non_owner``, unlocks it, and send to this account 1 Ether to pay for the gas.
Next, send 42 Ether to the contract.
.. note::
The test was deployed with the default account, the ``coinbase``. So ``coinbase``, or the alias
``owner``, is the owner of the contract.
When the test tries to withdraw with ``non_owner``, which is *not* the owner, the transaction fails:
.. code-block:: python
# test: non owner withdraw, should fail
with pytest.raises(TransactionFailed):
donator3.transact({'from':non_owner}).withdrawAll()
assert w3.eth.getBalance(donator3.address) == donation
When the owner tries to withdraw it works, and the balance is back to 0:
.. code-block:: python
# test: owner withdraw, ok
donator3.transact({'from':owner}).withdrawAll()
assert w3.eth.getBalance(donator3.address) == 0
Run the test:
.. code-block:: shell
$ py.test --disable-pytest-warnings
===================================== test session starts ======================
platform linux -- Python 3.5.2, pytest-3.1.3, py-1.4.34, pluggy-0.4.0
rootdir: /home/mary/projects/donations, inifile: pytest.ini
plugins: populus-1.8.0, hypothesis-3.14.0
collected 5 items
tests/test_donator.py ....
tests/test_donator3.py .
=========================== 5 passed, 24 warnings in 3.52 seconds ==============
Passed.
The 2nd test will test the``Ownable`` function that allows to transfer ownership. Only the current owner
can run it. Let's test it.
Edit the test file:
.. code-block:: shell
$ nano tests/test_donator3.py
And add the following test function:
.. code-block:: python
def test_transfer_ownership(chain):
donator3, deploy_tx_hash = chain.provider.get_or_deploy_contract('Donator4')
w3 = chain.web3
first_owner = w3.eth.coinbase # alias
# set unlocked test accounts, with Wei for gas
password = "demopassword"
second_owner = w3.personal.newAccount(password=password)
w3.personal.unlockAccount(second_owner,passphrase=password)
w3.eth.sendTransaction({'value':ONE_ETH_IN_WEI,'to':second_owner,'from':w3.eth.coinbase})
# initial contract balance
donation = 42 * ONE_ETH_IN_WEI
effective_usd_rate = 5
transaction = {'value': donation, 'from':w3.eth.coinbase}
donator3.transact(transaction).donate(effective_usd_rate)
assert w3.eth.getBalance(donator3.address) == donation
# test: transfer ownership
assert donator3.call().owner == first_owner
transaction = {'from':first_owner}
donator3.transact(transaction).transferOwnership(second_owner)
assert donator3.call().owner == second_owner
# test: first owner withdraw, should fail after transfer ownership
with pytest.raises(TransactionFailed):
donator3.transact({'from':first_owner}).withdrawAll()
assert w3.eth.getBalance(donator3.address) == donation
# test: second owner withdraw, ok after transfer ownership
donator3.transact({'from':second_owner}).withdrawAll()
assert w3.eth.getBalance(donator3.address) == 0
# test: transfer ownership by non owner, should fail
transaction = {'from':first_owner}
with pytest.raises(TransactionFailed):
donator3.transact(transaction).transferOwnership(second_owner)
assert donator3.call().owner == second_owner
Run the test:
.. code-block:: shell
$ py.test --disable-pytest-warnings
The test should fail:
.. code-block:: shell
# transfer ownership
> assert donator3.call().owner == first_owner
E AssertionError: assert functools.partial(, , 'owner', {'to': '0xc305c901078781c232a2a521c2af7980f8385ee9'}) == '0x82a978b3f5962a5b0957d9ee9eef472ee55b42f1'
Yes. ``donator3.call().owner`` is wrong. Confusing. Reminder: ``owner`` should be accessed as a *function*,
and *not* as a property, The compiler builds these functions for every public state variable.
Fix every occurence of ``call().owner`` to ``call().owner()``. E.g., into:
.. code-block:: python
assert donator3.call().owner() == first_owner
Then Run again:
.. code-block:: shell
$ py.test --disable-pytest-warnings
Fails again:
.. code-block:: shell
# test: transfer ownership
> assert donator3.call().owner() == first_owner
E AssertionError: assert '0x82A978B3f5...f472EE55B42F1' == '0x82a978b3f59...f472ee55b42f1'
E - 0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1
E
In the Ethereum world, it does not matter if an address is uppercase or lowercase (capitalisation is used for checksum,
to avoid errors in client applications). We will use all lower case.
Fix the test as follows:
.. code-block:: python
def test_transfer_ownership(chain):
donator3, deploy_tx_hash = chain.provider.get_or_deploy_contract('Donator4')
w3 = chain.web3
first_owner = w3.eth.coinbase # alias
# set unlocked test accounts, with Wei for gas
password = "demopassword"
second_owner = w3.personal.newAccount(password=password)
w3.personal.unlockAccount(second_owner,passphrase=password)
w3.eth.sendTransaction({'value':ONE_ETH_IN_WEI,'to':second_owner,'from':w3.eth.coinbase})
# initial contract balance
donation = 42 * ONE_ETH_IN_WEI
effective_usd_rate = 5
transaction = {'value': donation, 'from':w3.eth.coinbase}
donator3.transact(transaction).donate(effective_usd_rate)
assert w3.eth.getBalance(donator3.address) == donation
# test: transfer ownership
assert donator3.call().owner().lower() == first_owner.lower()
transaction = {'from':first_owner}
donator3.transact(transaction).transferOwnership(second_owner)
assert donator3.call().owner().lower() == second_owner.lower()
# test: first owner withdraw, should fail after transfer ownership
with pytest.raises(TransactionFailed):
donator3.transact({'from':first_owner}).withdrawAll()
assert w3.eth.getBalance(donator3.address) == donation
# test: second owner withdraw, ok after transfer ownership
donator3.transact({'from':second_owner}).withdrawAll()
assert w3.eth.getBalance(donator3.address) == 0
# test: transfer ownership by non owner, should fail
transaction = {'from':first_owner}
with pytest.raises(TransactionFailed):
donator3.transact(transaction).transferOwnership(second_owner)
assert donator3.call().owner().lower() == second_owner.lower()
Run the test:
.. code-block:: shell
$ py.test --disable-pytest-warnings
================================================== test session starts ==============
platform linux -- Python 3.5.2, pytest-3.1.3, py-1.4.34, pluggy-0.4.0
rootdir: /home/may/projects/donations, inifile: pytest.ini
plugins: populus-1.8.0, hypothesis-3.14.0
collected 6 items
tests/test_donator.py ....
tests/test_donator3.py ..
========================================== 6 passed, 29 warnings in 1.93 seconds ====
Ok, all the inherited members passed: The ``Ownable`` constructor that sets ``owner`` ran when you deployed
it's subclass, ``Donator3``. The parent modifier ``onlyOwner`` works as a modifier to a subclass function,
and the ``transferOwnership`` parent's funcion can be called by clients via the subclass interface.
.. note::
If you will deploy ``Donator3`` to a local chain, say ``horton``, and look at the ``registrar.json``, you
will not see an entry for ``Ownable``. The reason is that although Solidity has a full complex multiple inheritence
model (and ``super``), the final result is once contract. Solidity just copies the inherited code to this contract.
Interim Summary
---------------
* You used an open sourced contract
* You imported one contract to another
* You added an ownership control to a contract
* You used inheritence and tested it