åæ£åèªåŸçµç¹ïŒDAOïŒ
2016幎ã®å€ãTHE DAOãšã®ç©èªã¯ã»ã³ã»ãŒã·ã§ãã«ã§ãããæ»æè ã¯ããããå€é¡ã®è³éãçã¿ãŸãã ã DAOã¯çµç¹ãšããŠäœçœ®ä»ããããã¹ããŒããªå¥çŽã§ããããã®ãã¹ãŠã®ããã»ã¹ã¯ãããã¯ãã§ãŒã³ç°å¢ã§åäœããã³ãŒãã«ãã£ãŠèšè¿°ãããŸãããæ³äººã§ã¯ãªãããã¹ãŠã®æè³å®¶ã«ãã£ãŠéåçã«ç®¡çãããŸãã 3æã«ãDAOéçºè ã¯ãã¹ãã®éèŠæ§ã匷調ãã PythonãšJavascriptãæ··åãããã¬ãŒã ã¯ãŒã¯ã䜿çšãããã¹ãã§ã¹ããŒãã³ã³ãã©ã¯ããã«ããŒããŸããããæ®å¿µãªãããã¹ãã¯åŸã®è匱æ§ãéããŸããã§ããã
ããšãã°ã DAOã¹ããŒãã³ã³ãã©ã¯ãã³ãŒãã¯å€§ããããããããã¹ããªããžã§ã¯ããšããŠã httpïŒ //ethereum.orgã®ãããã¯ãã§ãŒã³ã«æ°äž»äž»çŸ©ãæ§ç¯ããæ¹æ³ã®èšäºã§èª¬æãããŠããDAOã®ååãå®è£ ããè°äŒã¹ããŒãã³ã³ãã©ã¯ããåãäžããŸãã å°æ¥çã«ã¯ãã¹ããŒãã³ã³ãã©ã¯ãã®éçºã®åºæ¬ååã«ç²ŸéããŠããããšãåæãšãªããŸãã
ã¹ããŒãã³ã³ãã©ã¯ãã®ãã¹ãæ¹æ³
äžè¬çãªååã¯ãä»ã®ã³ãŒãã®ãã¹ãã«äŒŒãŠããŸã-äžé£ã®åç §ã¡ãœããåŒã³åºããäºåå®çŸ©ãããç°å¢ã§äœæãããçµæã¹ããŒãã¡ã³ããæžã蟌ãŸããŸãã ãã¹ãã«ã¯ãBDD-Behavior Driven Development Practicesã䜿çšãããšäŸ¿å©ã§ãããã¹ããšäžç·ã«ãããã¥ã¡ã³ããšäœ¿çšäŸãäœæã§ããŸãã
ãã¹ãããŒã«
å€ãã®ã€ãŒãµãªã¢ã ã¹ããŒãã³ã³ãã©ã¯ããã¹ããã¬ãŒã ã¯ãŒã¯ãšã©ã€ãã©ãªãéçºãããŸããã
ããªã¥ã
Truffle v.2ã§ã¯ãMochaãã¬ãŒã ã¯ãŒã¯ãšChaiã©ã€ãã©ãªã䜿çšããŠãJavaScriptã§ãã¹ããéçºãããŠããŸãã ããŒãžã§ã³3ã§ã¯ã Solidityã®ãã¹ããäœæããæ©èœãè¿œå ãããŠããŸã ã
ãŸã ã
DAppleã§ã¯ãç¹å¥ã«éçºãããåºæ¬çãªã¹ããŒãã³ã³ãã©ã¯ãã®æ¹æ³ã䜿çšããŠãSolidityã«ãã¹ããå®è£ ãããŸãã
Embarkjs
EmbarkJSã§ã¯ãã¢ãããŒãã¯Truffleã«äŒŒãŠããããã¹ãã¯Javascriptã§èšè¿°ãããMochaãã¬ãŒã ã¯ãŒã¯ã䜿çšãããŸãã
Solidityã®ãã¹ãã®éçºã¯ãã®èšèªã®æ©èœã«ãã£ãŠéåžžã«å¶éãããŠãããããJavascriptã䜿çšããŸãããã¹ãŠã®äŸã§ã¯Truffle Frameworkã䜿çšããŸãã ãŸãã truffle-contractãtruffle-artifactorãªã©ã®Truffle Frameworkã³ã³ããŒãã³ãã䜿çšããŠãã¹ããŒãã³ã³ãã©ã¯ããšå¯Ÿè©±ããããã®ã«ã¹ã¿ã ãœãªã¥ãŒã·ã§ã³ãäœæã§ããŸãã
ãã¹ãã¯ã©ã€ã¢ã³ã
ãããã¯ãã§ãŒã³ã·ã¹ãã ãç¹ã«ã€ãŒãµãªã¢ã ã¯éåžžã«é«éã«åäœããªãããããã¹ãã«ã¯ããã¹ãããããã¯ãã§ãŒã³ã¯ã©ã€ã¢ã³ãã䜿çšããŸããããšãã°ã ã€ãŒãµãªã¢ã ã¯ã©ã€ã¢ã³ãã®JSON RPC APIãã»ãŒå®å šã«ãšãã¥ã¬ãŒãããTestRPCã§ãã TestRPCã¯ãæšæºã®ã¡ãœããã«å ããŠããã¹ãæã«evm_increaseTime ã evm_mineãªã©ã®äœ¿çšã«äŸ¿å©ãªããã€ãã®è¿œå ã¡ãœãããå®è£ ããŸãã
å¥ã®æ¹æ³ãšããŠãæšæºã¯ã©ã€ã¢ã³ãã®1ã€ã䜿çšããæ¹æ³ããããŸããããšãã°ãããªãã£ã¯devã¢ãŒãã§åäœã ããã©ã³ã¶ã¯ã·ã§ã³ã¯å³åº§ã«ç¢ºèªãããŸãã ãããªãäŸã§ã¯ãTestRPCã䜿çšãããŸãã
ç°å¢èšå®
ãã¹ãã¯ã©ã€ã¢ã³ã
npmãä»ããã€ã³ã¹ããŒã«ïŒ
npm install -g ethereumjs-testrpc
TestRPCã¯å¥ã®ã¿ãŒããã«ã§å®è¡ããå¿ èŠããããŸãã ãã¹ããéå§ãããã³ã«ããã¹ãã¯ã©ã€ã¢ã³ãã¯10åã®æ°ããã¢ã«ãŠã³ããçæããŸããåã¢ã«ãŠã³ãã«ã¯æ¢ã«è³éãé 眮ãããŠããŸãã
ããªã¥ããã¬ãŒã ã¯ãŒã¯
npmãä»ããã€ã³ã¹ããŒã«ïŒ
npm install -g truffle
ãããžã§ã¯ãæ§é ãäœæããã«ã¯ãtruffle initã³ãã³ããå®è¡ããå¿ èŠããããŸã
$ mkdir solidity-test-example $ cd solidity-test-example/ $ truffle init
ã³ã³ãã©ã¯ãã¯ã³ã³ãã©ã¯ã/ãã£ã¬ã¯ããªã«é 眮ããå¿ èŠããããŸããã³ã³ãã©ã¯ããã³ã³ãã€ã«ãããšããTruffle Frameworkã¯åã³ã³ãã©ã¯ããåå¥ã®ãã¡ã€ã«ã«é 眮ãããããšãæåŸ ããã³ã³ãã©ã¯ãã®ååã¯ãã¡ã€ã«ã®ååãšçãããªããŸãã ãã¹ãã¯test /ãã£ã¬ã¯ããªã«é 眮ãããŸãã truffle initã³ãã³ããå®è¡ããããš ãMetacoin et alããã¹ãã³ã³ãã©ã¯ããäœæãããŸãã
ãããªãäŸã§ã¯ããããžã§ã¯ãhttps://github.com/vitiko/solidity-test-exampleã䜿çšãããŸããããã«ã¯ã è°äŒã®ã¹ããŒãå¥çŽã³ãŒããšãã®ãã¹ããå«ãŸããŠããŸãã ãã¹ãã¯Truffle v.2ç°å¢ã§å®è¡ãããŸããæè¿ãªãªãŒã¹ãããv.3ããŒãžã§ã³ã§ã¯ãTruffleã«ãã£ãŠçæãããã³ãŒãã®æ¥ç¶ãšãç¶æ å€æŽã¡ãœãããåŒã³åºããåŸã«è¿ããããã©ã³ã¶ã¯ã·ã§ã³ããŒã¿åœ¢åŒã«ããããªéãããããŸãã
Truffleãã¬ãŒã ã¯ãŒã¯ã«åºã¥ãããã¹ãéçº
ãã¹ãçµç¹
ãã¹ãã§ã¯JavaScriptãªããžã§ã¯ãã䜿çšããŸããããã¯ãã³ã³ãã©ã¯ãã®æäœããªããžã§ã¯ãã®æäœãšEthereumã¯ã©ã€ã¢ã³ãã¡ãœããã®JSON RPCåŒã³åºãéã®ãããã³ã°ã®æœè±¡åã§ãã ãããã®ãªããžã§ã¯ãã¯ããœãŒã¹ã³ãŒã* .solãã¡ã€ã«ã®ã³ã³ãã€ã«æã«èªåçã«äœæãããŸãã ãã¹ãŠã®ã¡ãœããã®åŒã³åºãã¯éåæã§ãããPromiseãè¿ããŸããããã«ããããã©ã³ã¶ã¯ã·ã§ã³ç¢ºèªã®è¿œè·¡ãå¿é ããå¿ èŠããªããªãããã¹ãŠãTruffleã³ã³ããŒãã³ãã®ãå éšãã§å®è£ ãããŸãã
Truffle Frameworkãµã€ãã®äŸã§ã¯ã.thenïŒïŒãã§ãŒã³ã䜿çšããèšè¿°ã¹ã¿ã€ã«ã䜿çšããŠããŸãã 倧èŠæš¡ãªã·ããªãªã説æããå Žåããã¹ãã³ãŒãã¯éåžžã«èšå€§ã§ãã async / awaitã䜿çšãããã¹ãã³ãŒãã¯ã¯ããã«ç°¡æœã§èªã¿ãããããããã®ã¹ã¿ã€ã«ã®ãã¹ãã䜿çšããŸãã ãŸããéçºè ã®ãµã€ãã®äŸã§ã¯ãã¹ããŒãã³ã³ãã©ã¯ãã®ã€ã³ã¹ã¿ã³ã¹ã䜿çšãããŠããããã®ãããã€ã¯ç§»è¡ã§èŠå®ãããŠããŸã ã 移è¡ãšãã¹ãã€ã³ã¹ã¿ã³ã¹ã®äœæãæ··åšãããªãããã«ããã¹ãã§æ瀺çã«äœæããæ¹ã䟿å©ã§ãããã®ãããåãã¹ãé¢æ°ãåŒã³åºãåã«ãã³ã³ãã©ã¯ãã®æ°ããã€ã³ã¹ã¿ã³ã¹ãäœæããã³ãŒãã䜿çšã§ããŸãã 以äžã®äŸã¯ã Congressãªããžã§ã¯ãã®ã€ã³ã¹ã¿ã³ã¹ãäœæãããbeforeEaché¢æ°ã瀺ããŠããŸãã
ã³ã³ã°ã¬ã¹ã¹ããŒãã³ã³ãã©ã¯ããã¶ã€ããŒ
/* 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)); });
ã¹ããŒãã³ã³ãã©ã¯ãã®ã¹ããŒã¿ã¹ããã¹ããã
ãŸããã¹ããŒãã³ã³ãã©ã¯ãã®ç¶æ ãå€æŽããaddMemberã¡ãœããããã¹ãããŠã¿ãŸãããããã®ã¡ãœããã¯ã ã¡ã³ããŒæ§é ã®é åã«DAOã¡ã³ããŒã«é¢ããæ å ±ãæžã蟌ãå¿ èŠããããŸãã
ã¹ããŒãã³ã³ãã©ã¯ãã®addMemberé¢æ°ã³ãŒã
/*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); }
ãã¹ãã§ã¯ããã¹ãã¢ã«ãŠã³ãã§é åã䜿çšããŠãåå è ãå¥çŽã«è¿œå ããŸãã 次ã«ãmembersé¢æ°ïŒ membersæ§é äœã®é åã®ã²ãã¿ãŒïŒãå ¥åãããããŒã¿ãè¿ãããšã確èªããŸãã addMemberã¡ãœãããåŒã³åºããã³ã«ããã©ã³ã¶ã¯ã·ã§ã³ãäœæããããããã¯ãã§ãŒã³ã®ç¶æ ãå€åããããšã«æ³šæããŠãã ããã æ å ±ã¯åæ£ã¬ãžã¹ããªã«èšé²ãããŸãã
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); } });
ã€ãã³ããã¹ã
ã€ãŒãµãªã¢ã ã®ã€ãã³ãã«ã¯ãããªãæ®éçãªã¢ããªã±ãŒã·ã§ã³ãããã䜿çšã§ããŸãã
- 以äžãå«ããå¥çŽã®ç¶æ ãå€æŽããã¡ãœããããå€ãè¿ã ãŠãŒã¶ãŒã€ã³ã¿ãŒãã§ãŒã¹ãž
- ã¹ããŒãã³ã³ãã©ã¯ãã®ã¹ããŒã¿ã¹ã®å±¥æŽãäœæããã«ã¯
- æ å ±ã®å®äŸ¡ãªã¹ãã¬ãŒãžãšããŠïŒã€ãã³ããã°ã«ããŒã¿ãé 眮ããã³ã¹ãã¯ãå¥çŽã®ç¶æ ãããã¯ããã«å®äŸ¡ã§ãïŒ
次ã®äŸã§ã¯ã è°äŒã®å¥çŽã«ææ¡ãè¿œå ããnewProposalã¡ãœãããåŒã³åºããšãã«ã Proposal Addedã€ãã³ãã®ã¬ã³ãŒããäœæãããããšã確èªããŸã
æ©èœã³ãŒãnewProposalã¹ããŒãã³ã³ãã©ã¯ã
/* 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; }
ãããè¡ãã«ã¯ããã¹ãã§æåã«DAOã¡ã³ããŒãäœæãã圌ã«ä»£ãã£ãŠææ¡ãäœæããŸãã 次ã«ã ProposalAddedã€ãã³ãã®ãµãã¹ã¯ã©ã€ããŒãäœæãã newProposalã¡ãœããã®åŒã³åºãåŸã«ã€ãã³ããçºçãããã®å±æ§ãéä¿¡ãããããŒã¿ã«å¯Ÿå¿ããããšã確èªããŸãã
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); }); });
ãšã©ãŒãã¹ããšã¡ãã»ãŒãžéä¿¡è ã®æ€èšŒ
ã³ã³ãã©ã¯ãã¡ãœããã®æäœãäžæããæšæºçãªã¡ãœããã¯ã throwã¹ããŒãã¡ã³ãã䜿çšããŠäœæã§ããäŸå€ã§ãã ã¡ãœãããžã®ã¢ã¯ã»ã¹ãå¶éããå¿ èŠãããå Žåãªã©ãäŸå€ãå¿ èŠã«ãªãå ŽåããããŸãã ãããè¡ãããã«ãã¡ãœãããåŒã³åºããã¢ã«ãŠã³ãã®ã¢ãã¬ã¹ããã§ãã¯ãã修食åãå®è£ ãããæ¡ä»¶ãæºãããªãå ŽåãäŸå€ãã¹ããŒãããŸãã ããšãã°ãã³ã³ãã©ã¯ãã®ææè ãaddMemberã¡ãœãããåŒã³åºããå Žåã«äŸå€ãã¹ããŒãããããšã確èªãããã¹ããäœæããŸãããã 以äžã®ã³ãŒãã§ã¯ã è°äŒã®å¥çŽãã¢ã«ãŠã³ã[0]ã«ä»£ãã£ãŠäœæããã次ã«addMemberã¡ãœãããå¥ã®ã¢ã«ãŠã³ãã«ä»£ãã£ãŠåŒã³åºãããŸãã
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'); });
ã¹ããŒãã³ã³ãã©ã¯ãã®çŸåšã®æå»ã䜿çšããŠãã¹ããŒãã³ã³ãã©ã¯ãã®ãã©ã³ã¹ã®å€åããã¹ããã
ãããããDAOã®ååãå®è£ ããè°äŒã®ã¹ããŒãã³ã³ãã©ã¯ãã®æãéèŠãªæ©èœã¯ã executeProposalæ©èœã§ãããã®æ©èœã¯ãææ¡ãå¿ èŠãªæ祚æ°ãåãåã£ãããšã確èªããææ¡ã®è°è«ãã¹ããŒãã³ã³ãã©ã¯ãã®äœææã«èšå®ãããæäœå¿ èŠæé以äžç¶ããåŸãåçè ã«ééããŸãè°è«äžã®ææ¡ã
æ©èœã³ãŒãexecuteProposalã¹ããŒãã³ã³ãã©ã¯ã
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); }
çµéæéãã·ãã¥ã¬ãŒãããããã«ã testrpcã« å®è£ ãããŠããevm_increaseTimeã¡ãœããã䜿çšããŸããããã䜿çšããŠãã¯ã©ã€ã¢ã³ããããã¯ãã§ãŒã³ã®å éšæéãå€æŽã§ããŸãã
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]); });
ãã«ããŒé¢æ°
ãã¹ãã®èšè¿°ããã»ã¹ã§ã¯ãäŸå€ã®ãã§ãã¯ãã€ãã³ããã°ã®åä¿¡ããã¹ãã¯ã©ã€ã¢ã³ãã®æå»ã®å€æŽãªã©ããã¹ãäžã«é »ç¹ã«äœ¿çšãããæäœã«äŸ¿å©ãªæ©èœã圹ç«ã¡ãŸããã é¢æ°ã¯ããã±ãŒãžhttps://www.npmjs.com/package/solidity-test-utilã§ãã¹ããããã³ãŒãã¯githubã§ãã¹ããããŸãã 以äžã¯ã testUtil.assertThrowé¢æ°ã䜿çšããŠäŸå€ããã§ãã¯ããäŸã§ãã
it("should disallow no owner to add members", async function() { await testUtil.assertThrow(() => congress.addMember(accounts[1], 'Name for account 1', { from: accounts[9] })); });
solidity-test-utilé¢æ°ã䜿çšããä»ã®äŸã«ã€ããŠã¯ã ãã¡ããã芧ãã ãã ã
ãããã«
ãã¹ãã®éçºã®çµæãã¹ããŒãã³ã³ãã©ã¯ãã®äœæ¥ã®æ£ç¢ºæ§ã®èªåæ€èšŒãšãäžè¬ã«ãä»ã®ããã°ã©ã ã³ãŒãã®ãã¹ãã«ã€ããŠèšãããã¹ãŠã®ä»æ§ãšäœ¿çšäŸã®äž¡æ¹ãåŸãããŸãã