📜 ⬆️ ⬇️

Testing Ethereum smart contracts on the example of DAO

When creating smart contracts on the Ethereum platform, the developer lays down certain work logic, defining how methods should change the state of the contract, what events should be emitted, when and to whom the transfer of funds should be effected and when to throw an exception. Smart contract debugging tools are not yet well developed, so tests often become a necessary development tool, because to run contracts after each change can be quite a long procedure. Also, in case of detection of errors, it is already impossible to change the code of a contract deployed in the network, you can only destroy the contract and create a new one, so testing should be carried out in as much detail as possible, especially the methods associated with payments. The article will show some testing techniques that developers encounter when creating and debugging smart contracts for Solidity .

Decentralized Autonomous Organization (DAO)


In the summer of 2016, the story with THE DAO, from which the attacker took a lot of money , made a noise. DAO is a smart contract that positions itself as an organization, all processes of which are described by code that operates in the blockchain environment, while not being a legal entity and are managed collectively by all its investors. Back in March, the DAO developers emphasized the importance of testing and even covered their smart contract with tests using their own framework in a mixture of Python and Javascript, but unfortunately the tests did not close the vulnerability that was used later.

The DAO smart contract code is too big for an example, so let's take the smart contract of Congress , which implements the principles of DAO, which is given in the article http://ethereum.org on the blockchain article. In the future, it is assumed familiarity with the basic principles of the development of smart contracts .
')

How does smart contract testing occur?


The general principle is similar to testing any other code - a set of reference method calls is created in a predefined environment, for the results of which statements are written. For testing, it is convenient to use the practices of BDD - Behavior Driven Development, which, along with tests, allow you to create documentation and examples of use.

Testing Tools


Currently, a number of Ethereum smart contract frameworks and libraries have been developed:

Truffle


Truffle v.2 tests are developed in JavaScript, using the Mocha framework and the Chai library. In version 3, the ability to write tests for Solidity was added.

DApple


In DApple, tests are implemented on Solidity, using methods of specially developed basic smart contracts.

Embarkjs


In EmbarkJS approach is similar to Truffle, tests are written in Javascript, Mocha framework is used.

Development of tests for Solidity is quite limited by the capabilities of this language, so we will use Javascript, all the examples will be using the Cruffle Framework. You can also use the Truffle Framework components, such as truffle-contract or truffle-artifactor , to create your custom solutions for interacting with smart contracts.

Test client


Taking into account the fact that the blockchain systems, in particular Ethereum, do not work very quickly, the “test” blockchain clients are used for testing, for example, TestRPC , which almost completely emulates Ethereum clients JSON RPC API . In addition to standard methods, TestRPC also implements a number of additional methods that are convenient to use when testing, such as evm_increaseTime , evm_mine , etc.

Alternatively, you can use one of the standard clients, for example Parity, which works in dev mode , in which transactions are confirmed instantly. In the following examples, TestRPC will be used.

Setting up the environment


Test client


Installation via npm:

npm install -g ethereumjs-testrpc 

TestRPC should be run in a separate terminal. Each time the test client starts, it generates 10 new accounts, on each of which funds are already placed.

Framework truffle


Installation via npm:

 npm install -g truffle 

To create a project structure you need to run the command truffle init

 $ mkdir solidity-test-example $ cd solidity-test-example/ $ truffle init 

Contracts must be located in the contracts / directory, when compiling contracts the Truffle Framework expects each contract to be placed in a separate file, the name of the contract is equal to the file name. Tests are located in the test / directory. When you execute the truffle init command, test contracts are also created by Metacoin, etc.

In further examples, the project https://github.com/vitiko/solidity-test-example will be used, which contains the smart code for the contract Congress and tests for it. Tests are performed in the Truffle v.2 environment, in the v.3 version that was recently released, there are some minor differences in terms of the connection between the generated Truffle code and the format of the transaction data that is returned after calling the state-changing methods.

Development of tests based on the Truffle framework


Test organization


The tests use JavaScript objects, which are abstractions for working with contracts, producing mapping between operations on objects and calls to JSON RPC client methods Ethereum. These objects are created automatically when compiling the source code of * .sol files. Calls of all methods are asynchronous and return Promise, it allows not to worry about tracking the confirmation of transactions, everything is implemented under the hood Truffle components.

The examples on the Truffle Framework site use the style of writing with .then () chains. If you describe a large script, the test code is quite voluminous. Much more concise and readable is the test code using async / await, then this style of writing tests will be used. Also, in the examples on the developer's website, instances of smart contracts are used, the deployment of which is prescribed in migrations . In order not to mix migrations and creating test instances, it is more convenient to create them explicitly in a test, for this you can use the code that creates a new instance of the contract before calling each test function. The example below shows the beforeEach function in which an instance of the Congress object is created.

Designer Smart Contract Contract
 /* First time setup */ function Congress( uint minimumQuorumForProposals, uint minutesForDebate, int marginOfVotesForMajority, address congressLeader ) payable { changeVotingRules(minimumQuorumForProposals, minutesForDebate, marginOfVotesForMajority); if (congressLeader != 0) owner = congressLeader; // It's necessary to add an empty first member addMember(0, ''); // and let's add the founder, to save a step later addMember(owner, 'founder'); } 

 const congressInitialParams = { minimumQuorumForProposals: 3, minutesForDebate: 5, marginOfVotesForMajority: 1, congressLeader: accounts[0] }; let congress; beforeEach(async function() { congress = await Congress.new(...Object.values(congressInitialParams)); }); 

Testing a smart contract state change


To begin with, let's try to test the addMember method changing the state of a smart contract - the method should write information about the DAO member into an array of member structures.

Function code of addMember smart contract
 /*make member*/ function addMember(address targetMember, string memberName) onlyOwner { uint id; if (memberId[targetMember] == 0) { memberId[targetMember] = members.length; id = members.length++; members[id] = Member({member: targetMember, memberSince: now, name: memberName}); } else { id = memberId[targetMember]; Member m = members[id]; } MembershipChanged(targetMember, true); } 

In the test, using the array with test accounts, we add participants to the contract. Then we check that the members function (getter for an array of member structures) returns the entered data. It should be noted that each time the addMember method is called , a transaction is created and the state of the blockchain changes; information is recorded in a distributed registry.

 it("should allow owner to add members", async function() { //  3  for (let i = 1; i <= 3; i++) { let addResult = await congress.addMember(accounts[i], 'Name for account ' + i); //    members      2. // .. members[0] - empty, members[1] - ,   (. ) let memberInfoFromContract = await congress.members(i + 1); //  members(pos)       Member //  [0] -   , [1] -    assert.equal(memberInfoFromContract[0], accounts[i]); assert.equal(memberInfoFromContract[1], 'Name for account ' + i); } }); 

Event Testing


Events in Ethereum have a fairly universal application, they can be used:


In the following example, we will check that when calling the newProposal method, which adds proposals to the Congress contract, a record of the event Proposal Added is created.

Function code of the newProposal smart contract
 /* Function to create a new proposal */ function newProposal( address beneficiary, uint etherAmount, string JobDescription, bytes transactionBytecode ) onlyMembers returns (uint proposalID) { proposalID = proposals.length++; Proposal p = proposals[proposalID]; p.recipient = beneficiary; p.amount = etherAmount; p.description = JobDescription; p.proposalHash = sha3(beneficiary, etherAmount, transactionBytecode); p.votingDeadline = now + debatingPeriodInMinutes * 1 minutes; p.executed = false; p.proposalPassed = false; p.numberOfVotes = 0; ProposalAdded(proposalID, beneficiary, etherAmount, JobDescription); numProposals = proposalID+1; return proposalID; } 

To do this, we first create a DAO participant in the test and create a proposal on its behalf. Then we create a subscriber for the ProposalAdded event and check that after calling the newProposal method , the event has occurred and its attributes correspond to the data passed.

 it("should fire event 'ProposalAdded' when member add proposal", async function() { let proposedAddedEventListener = congress.ProposalAdded(); const proposalParams = { beneficiary : accounts[9], etherAmount: 100, JobDescription : 'Some job description', transactionBytecode : web3.sha3('some content') }; await congress.addMember(accounts[5], 'Name for account 5'); await congress.newProposal(...Object.values (proposalParams), { from: accounts[5] }); let proposalAddedLog = await new Promise( (resolve, reject) => proposedAddedEventListener.get( (error, log) => error ? reject(error) : resolve(log) )); assert.equal(proposalAddedLog.length, 1, 'should be 1 event'); let eventArgs = proposalAddedLog[0].args; assert.equal(eventArgs.proposalID , 0); assert.equal(eventArgs.recipient , proposalParams.beneficiary); assert.equal(eventArgs.amount , proposalParams.etherAmount); assert.equal(eventArgs.description , proposalParams.JobDescription); }); }); 

Error Testing and Message Sender Verification


The standard method for terminating the contract method is exceptions , which can be created using the throw statement. An exception may be needed, for example, if you need to restrict access to the method. For this, a modifier is implemented that checks the address of the account that called the method, and if an exception is not met, an exception is created. For example, let's create a test that checks that if the addMember method is not the contract owner, an exception is created. In the code below, the Contract contract is created on behalf of accounts [0], then the addMember method is called on behalf of another account.

 it("should disallow no owner to add members", async function() { let addError; try { //  ,  accounts[0] != accounts[9] await congress.addMember(accounts[1], 'Name for account 1', { from: accounts[9] }); } catch (error) { addError = error; } assert.notEqual(addError, undefined, 'Error must be thrown'); //      ,       //  "invalid JUMP" assert.isAbove(addError.message.search('invalid JUMP'), -1, 'invalid JUMP error must be returned'); }); 

Testing the change in the balance of a smart contract, using the current time in a smart contract


Perhaps the most important function of the smart contract to Congress , which implements the principles of DAO, is the executeProposal function, which starts checking that the proposal received the required number of votes and the discussion of the proposal lasted no less than the minimum required time specified when creating the smart contract, and then transfers the funds to the beneficiary discussed proposal.

Function code executeProposal smart contract
 function executeProposal(uint proposalNumber, bytes transactionBytecode) { Proposal p = proposals[proposalNumber]; /* Check if the proposal can be executed: - Has the voting deadline arrived? - Has it been already executed or is it being executed? - Does the transaction code match the proposal? - Has a minimum quorum? */ if (now < p.votingDeadline || p.executed || p.proposalHash != sha3(p.recipient, p.amount, transactionBytecode) || p.numberOfVotes < minimumQuorum) throw; /* execute result */ /* If difference between support and opposition is larger than margin */ if (p.currentResult > majorityMargin) { // Avoid recursive calling p.executed = true; if (!p.recipient.call.value(p.amount * 1 ether)(transactionBytecode)) { throw; } p.proposalPassed = true; } else { p.proposalPassed = false; } // Fire Events ProposalTallied(proposalNumber, p.currentResult, p.numberOfVotes, p.proposalPassed); } 

To simulate the elapsed time, we use the evm_increaseTime method, which is implemented in testrpc - with its help, you can change the internal blockchain time of the client.

 it("should pay for executed proposal", async function() { const proposalParams = { beneficiary: accounts[9], etherAmount: 1, JobDescription: 'Some job description', transactionBytecode: web3.sha3('some content') }; //    accounts[9]   executeProposal let curAccount9Balance = web3.eth.getBalance(accounts[9]).toNumber(); //  ,         //     accounts[9] await congress.newProposal(...Object.values(proposalParams), { from: accounts[0] //accounts[0]  , ..      }); //  DAO,      //     3    //   3  DAO     0    for (let i of[3, 4, 5]) { await congress.addMember(accounts[i], 'Name for account ' + i); await congress.vote(0, true, 'Some justification text from account ' + i, { from: accounts[i] }); } //    let curProposalState = await congress.proposals(0); //   ,       //  .  //... //    testrpc  10 ,    //       minutesForDebate (5) await new Promise((resolve, reject) => web3.currentProvider.sendAsync({ jsonrpc: "2.0", method: "evm_increaseTime", params: [10 * 600], id: new Date().getTime() }, (error, result) => error ? reject(error) : resolve(result.result)) ); //    -  3  “”, //      await congress.executeProposal(0, proposalParams.transactionBytecode); // ,     accounts[9] //   ,     let newAccount9Balance = web3.eth.getBalance(accounts[9]).toNumber(); assert.equal(web3.fromWei(newAccount9Balance - curAccount9Balance, 'ether'), proposalParams.etherAmount, 'balance of acccounts[9] must increase to proposalParams.etherAmount'); let newProposalState = await congress.proposals(0); assert.isOk(newProposalState[PROPOSAL_PASSED_FIELD]); }); 

Secondary functions


In the process of writing tests useful functions that are conveniently used for frequently used operations when testing, such as checking for an exception, receiving the event log, changing the time of the test client. The functions are placed in the form of a package https://www.npmjs.com/package/solidity-test-util , the code is placed on github . The following is an example of using the testUtil.assertThrow function to check for an exception:

 it("should disallow no owner to add members", async function() { await testUtil.assertThrow(() => congress.addMember(accounts[1], 'Name for account 1', { from: accounts[9] })); }); 

Other examples using solidity-test-util functions can be seen here .

Conclusion


As a result of the development of tests, we get both automated verification of the correctness of smart contracts, as well as the specification and examples of use, in general, everything that can be said about testing any other software code.

Source: https://habr.com/ru/post/321362/


All Articles