Introducing Ethereum Development – Part 4 – Smarter Smart Contract

on

On Alice’s continuing mission to monetise her blog through Ether donations, she realises that her previous solution could be improved by making it more generic and flexible. She decides to create a Dapp to produce an embeddable button generated from a smart contract with an administrative UI. Taking this approach, she can build a solution usable by others who share her needs.

Before starting on her new design, she reflects on the problems with her existing work.

Her last contract contained hardcoded account values and logic. If she wanted to add, remove or amend a payee she would need to build a new contract and amend any existing HTML buttons manually.

Every time Ether was sent to the contract it would split and forward funds. This means Bob the benefactor would have to pay additional gas fees to cover two additional transactions when making a donation. This may not be so expensive with just two participants, but if there were many additional payees in the contract, it would be very inefficient. She resolves instead to keep a track of how much each participant should receive and allow them to withdraw whenever they wish. This means gas fees are borne by the payee on demand and multiple transaction fees are not incurred every time someone makes a donation.

Alice’s sketch of intended behavior.

Diagram of Dapp behaviour

We will begin by explaining some of the features of Solidity and then show the full code with comments

Struct
We create a structured object to represent a payee in our system. Solidity does not support deleting items from arrays so we add a boolean property of “status” to mark a participant active. We also create a mapping (associative array) to act as an index of payee addresses and keep count of the total number of items to help us iterate in a loop.

struct Payee {
bool status;
uint weight;
uint balance;
}

mapping(address => Payee) public payees;
mapping (int8 => address) public payeesIndex;
int8 public payeesIndexSize;
Constructor
When the contract is instantiated we can pass in or set default values in much the same way as other OO languages. Property types passed to the constructor must be of fixed size. For example, if we pass an array of strings, the array length and byte size of array elements must be consistent. In our case, we only capture the account which deployed the contract (which is available implicitly).

owner = msg.sender

Event Log
We use the event log to communicate with our Dapp. Events are fired when a transaction is first processed and are available in the transaction receipt. By setting an index we can easily watch for actions involving those addresses in our front end code. The event log offers a mechanism of storage which is cheaper than persisting other kinds or arbitrary data on the blockchain.

We define our Event log message with

event NewDonation(address indexed donator, uint amt);

Then we can call it in a function with
NewDonation(msg.sender, msg.value);

Modifiers
Modifiers allow us to restrict access to methods by adding the name of a modifier to the declaration. For example, we can check if the account calling a method is the owner of the contract, or check for a specific contract state. The code for the function being modified is inserted where the _ is placed in the modifier. The payable modifier must be used on any function which accepts or sends Ether.

modifier isOwner() {
if (msg.sender != owner) throw;
_;
}
Getters
All public variables have a getter method which is created automatically with the same name as a variable. In the code, we explicitly define some getters to make our lives easier when accessing structs inside a mapping.

function getBalance(address _address) isPayee returns (uint) {
return payees[_address].balance;
}
Putting it all together in Donation.sol
pragma solidity ^0.4.4;

// TestRPC HD wallet
// warrior minimum breeze raven garden express solar flavor obvious twenty alpha actress

contract Donation {

// Instantiate a variable to hold the account address of the contract administrator
address public owner;

// Create a data structure to reperesent each of the participants.
struct Payee {
// If the payee can administer their account.
bool status;
// The amount relative to aggregate weight that a payee will receive.
uint weight;
// A record of the amount held for the payee.
uint balance;
}

// Create an associative arrays with account address as key and payee data sturcture as value.
mapping(address => Payee) public payees;
// Create an array like mapping to behave as an index of addreesses
mapping (int8 => address) public payeesIndex;
// Keep note of total number of participants in the system so we can iterate over index.
int8 public payeesIndexSize;

// Declare events for actions we may want to watch
event NewDonation(address indexed donator, uint amt);
event Transfer(address indexed from, address indexed to, uint amt);
event PayeeAction(address indexed payee, bytes32 action);
event Withdrawal(address indexed payee, uint amt);
event OwnerChanged(address indexed owner, address indexed newOwner);
event ContractDestroyed(address indexed contractAddress);

// Constructor
function Donation() {
// Set the address of the contract deployer to be owner.
owner = msg.sender;
payees[owner].status = true;
payees[owner].weight = 10;
payeesIndex[0] = owner;
payeesIndexSize = 1;
}

// Check if current account calling methods is the owner.
modifier isOwner() {
if (msg.sender != owner) throw;
_;
}

// Check if current account calling methods is a valid payee of the contract.
modifier isPayee() {
if (payees[msg.sender].status != true) throw;
_;
}

// Aggregate all payee weights.
function getTotalWeight() private returns (uint) {

int8 i;
uint totalWeight = 0;

for (i=0;i payees[msg.sender].balance) throw;
if (!msg.sender.send(amount)) throw;
Withdrawal(msg.sender, amount);
payees[msg.sender].balance -= amount;
}

// Transfer some Ether available to withdraw to another account.
function transferBalance(address _from, address _to, uint amount) isOwner {
if (payees[_from].balance < amount) throw; payees[_from].balance -= amount; payees[_to].balance += amount; Transfer(_from, _to, amount); } function getBalance(address _address) isPayee returns (uint) { return payees[_address].balance; } function getWeight(address _address) isPayee returns(uint) { return payees[_address].weight; } function getStatus(address _address) returns(bool) { return payees[_address].status; } // Change ownership of the contract. function transferOwner(address newOwner) isOwner returns (bool) { if (!payees[newOwner].status == true) throw; OwnerChanged(owner, newOwner); owner = newOwner; } // Destroy the contract and pay out all enabled members. // Any outstanding value will be transferred to owner. function kill() payable isOwner { int8 i; address payee; for (i=0;i 0 ) {
if (payee.send(payees[payee].balance)) {
Withdrawal(payee, payees[payee].balance);
}
}
}

ContractDestroyed(this);
selfdestruct(owner);
}
}
view rawdonation.sol.js hosted with ❤ by GitHub
Test Code
We then use Truffle suite to run our tests in the same way as tutorial part 3.

The test results should yield:

Test results for donation smart contract

And the test code itself:

const Web3 = require(‘web3’)
let web3 = new Web3(new Web3.providers.HttpProvider(“http://localhost:8545”))
let Donation = artifacts.require(“./Donation”)

contract(‘Donation’, function(accounts) {
var donation

it(“Should retrive deployed contract.”, function(done) {
Donation.deployed().then(function(instance) {
donation = instance
assert.isOk(donation)
done()
})
})

it(“Should add a new payee.”, function(done) {
donation.addPayee(accounts[1], 20)
.then(function(tx) {
assert.equal(tx.logs[0].event, ‘PayeeAction’)
assert.equal(accounts[1], tx.logs[0].args.payee)
donation.getWeight.call(accounts[1])
.then(function(weight){
assert.equal(20, weight)
})
done()
})
})

it(“Should add a another payee.”, function(done) {
donation.addPayee(accounts[2], 10)
.then(function(tx) {
assert.equal(tx.logs[0].event, ‘PayeeAction’)
assert.equal(accounts[2], tx.logs[0].args.payee)
donation.getWeight.call(accounts[2])
.then(function(weight){
assert.equal(10, weight)
})
done()
})
})

it(“Should change weight of second payee”, function(done) {
donation.updatePayeeWeight(accounts[1], 10)
.then(function(tx) {
donation.getWeight.call(accounts[1])
.then(function(weight){
assert.equal(10, weight)
})
done()
})
})

it(“Should retrieve all payee objects”, function(done){
let i = 0
donation.payeesIndexSize.call()
.then(function(payeesIndexSize) {
let indexSize = payeesIndexSize.toNumber()
for (i=0;i