Статьи

Создание приложений Ethereum: создание белых списков и тестирование истории DAO

В третьей части этой серии руководств по созданию DApp-приложений с помощью Ethereum мы создали и развернули наш токен в тестовой сети Ethereum Rinkeby. В этой части мы начнем писать код Story DAO.

Мы будем использовать условия, изложенные во введении, чтобы направлять нас.

План контракта

Давайте создадим новый контракт, StoryDao.sol , с этим скелетом:

 pragma solidity ^0.4.24; import "../node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol"; import "../node_modules/openzeppelin-solidity/contracts/ownership/Ownable.sol"; contract StoryDao is Ownable { using SafeMath for uint256; mapping(address => bool) whitelist; uint256 public whitelistedNumber = 0; mapping(address => bool) blacklist; event Whitelisted(address addr, bool status); event Blacklisted(address addr, bool status); uint256 public daofee = 100; // hundredths of a percent, ie 100 is 1% uint256 public whitelistfee = 10000000000000000; // in Wei, this is 0.01 ether event SubmissionCommissionChanged(uint256 newFee); event WhitelistFeeChanged(uint256 newFee); uint256 public durationDays = 21; // duration of story's chapter in days uint256 public durationSubmissions = 1000; // duration of story's chapter in entries function changedaofee(uint256 _fee) onlyOwner external { require(_fee < daofee, "New fee must be lower than old fee."); daofee = _fee; emit SubmissionCommissionChanged(_fee); } function changewhitelistfee(uint256 _fee) onlyOwner external { require(_fee < whitelistfee, "New fee must be lower than old fee."); whitelistfee = _fee; emit WhitelistFeeChanged(_fee); } function lowerSubmissionFee(uint256 _fee) onlyOwner external { require(_fee < submissionZeroFee, "New fee must be lower than old fee."); submissionZeroFee = _fee; emit SubmissionFeeChanged(_fee); } function changeDurationDays(uint256 _days) onlyOwner external { require(_days >= 1); durationDays = _days; } function changeDurationSubmissions(uint256 _subs) onlyOwner external { require(_subs > 99); durationSubmissions = _subs; } } 

Мы импортируем SafeMath, чтобы снова выполнить безопасные вычисления, но на этот раз мы также используем Ownable контракт Zeppelin, который позволяет кому-то «владеть» историей и выполнять определенные функции только для администратора. Достаточно просто сказать, что наш StoryDao is Ownable ; не стесняйтесь проверять контракт, чтобы увидеть, как он работает.

Мы также используем модификатор onlyOwner из этого контракта. Модификаторы функций — это в основном расширения, плагины для функций. Модификатор onlyOwner выглядит так:

 modifier onlyOwner() { require(msg.sender == owner); _; } 

Когда в onlyOwner добавляется onlyOwner , тело этой функции вставляется в часть, где _; часть есть, и все, прежде чем она выполняется в первую очередь. Таким образом, с помощью этого модификатора функция автоматически проверяет, является ли отправитель сообщения также владельцем договора, а затем продолжает работу в обычном режиме, если это так. Если нет, он падает.

Используя модификатор onlyOwner для функций, которые изменяют сборы и другие параметры нашей истории DAO, мы гарантируем, что только администратор может внести эти изменения.

тестирование

Давайте проверим начальные функции.

Создайте test папки, если он не существует. Затем внутри него создайте файлы TestStoryDao.sol и TestStoryDao.js . Поскольку в Truffle нет собственного способа проверки исключений, также создайте helpers/expectThrow.js с содержимым:

 export default async promise => { try { await promise; } catch (error) { const invalidOpcode = error.message.search('invalid opcode') >= 0; const outOfGas = error.message.search('out of gas') >= 0; const revert = error.message.search('revert') >= 0; assert( invalidOpcode || outOfGas || revert, 'Expected throw, got \'' + error + '\' instead', ); return; } assert.fail('Expected throw not received'); }; 

Примечание. Тесты на устойчивость обычно используются для тестирования низкоуровневых функций, основанных на контракте, внутренних элементов смарт-контракта. Тесты JS обычно используются для проверки правильности взаимодействия с контрактом извне, что будут делать наши конечные пользователи.

В TestStoryDao.sol поместите следующее содержимое:

 pragma solidity ^0.4.24; import "truffle/Assert.sol"; import "truffle/DeployedAddresses.sol"; import "../contracts/StoryDao.sol"; contract TestStoryDao { function testDeploymentIsFine() public { StoryDao sd = StoryDao(DeployedAddresses.StoryDao()); uint256 daofee = 100; // hundredths of a percent, ie 100 is 1% uint256 whitelistfee = 10000000000000000; // in Wei, this is 0.01 ether uint256 durationDays = 21; // duration of story's chapter in days uint256 durationSubmissions = 1000; // duration of story's chapter in entries Assert.equal(sd.daofee(), daofee, "Initial DAO fee should be 100"); Assert.equal(sd.whitelistfee(), whitelistfee, "Initial whitelisting fee should be 0.01 ether"); Assert.equal(sd.durationDays(), durationDays, "Initial day duration should be set to 3 weeks"); Assert.equal(sd.durationSubmissions(), durationSubmissions, "Initial submission duration should be set to 1000 entries"); } } 

Это проверяет, что контракт StoryDao правильно развернут с правильными номерами для сборов и продолжительности. Первая строка проверяет его развертывание, считывая его из списка развернутых адресов, а в последнем разделе делается несколько утверждений — проверяется, является ли утверждение истинным или ложным. В нашем случае мы сравниваем числа с начальными значениями развернутого контракта. Всякий раз, когда это «правда», часть Assert.equals будет Assert.equals событие, которое говорит «True», и это то, что Truffle Assert.equals при тестировании.

В TestStoryDao.js поместите следующий контент:

 import expectThrow from './helpers/expectThrow'; const StoryDao = artifacts.require("StoryDao"); contract('StoryDao Test', async (accounts) => { it("should make sure environment is OK by checking that the first 3 accounts have over 20 eth", async () =>{ assert.equal(web3.eth.getBalance(accounts[0]).toNumber() > 2e+19, true, "Account 0 has more than 20 eth"); assert.equal(web3.eth.getBalance(accounts[1]).toNumber() > 2e+19, true, "Account 1 has more than 20 eth"); assert.equal(web3.eth.getBalance(accounts[2]).toNumber() > 2e+19, true, "Account 2 has more than 20 eth"); }); it("should make the deployer the owner", async () => { let instance = await StoryDao.deployed(); assert.equal(await instance.owner(), accounts[0]); }); it("should let owner change fee and duration", async () => { let instance = await StoryDao.deployed(); let newDaoFee = 50; let newWhitelistFee = 1e+10; // 1 ether let newDayDuration = 42; let newSubsDuration = 1500; instance.changedaofee(newDaoFee, {from: accounts[0]}); instance.changewhitelistfee(newWhitelistFee, {from: accounts[0]}); instance.changedurationdays(newDayDuration, {from: accounts[0]}); instance.changedurationsubmissions(newSubsDuration, {from: accounts[0]}); assert.equal(await instance.daofee(), newDaoFee); assert.equal(await instance.whitelistfee(), newWhitelistFee); assert.equal(await instance.durationDays(), newDayDuration); assert.equal(await instance.durationSubmissions(), newSubsDuration); }); it("should forbid non-owners from changing fee and duration", async () => { let instance = await StoryDao.deployed(); let newDaoFee = 50; let newWhitelistFee = 1e+10; // 1 ether let newDayDuration = 42; let newSubsDuration = 1500; await expectThrow(instance.changedaofee(newDaoFee, {from: accounts[1]})); await expectThrow(instance.changewhitelistfee(newWhitelistFee, {from: accounts[1]})); await expectThrow(instance.changedurationdays(newDayDuration, {from: accounts[1]})); await expectThrow(instance.changedurationsubmissions(newSubsDuration, {from: accounts[1]})); }); it("should make sure the owner can only change fees and duration to valid values", async () =>{ let instance = await StoryDao.deployed(); let invalidDaoFee = 20000; let invalidDayDuration = 0; let invalidSubsDuration = 98; await expectThrow(instance.changedaofee(invalidDaoFee, {from: accounts[0]})); await expectThrow(instance.changedurationdays(invalidDayDuration, {from: accounts[0]})); await expectThrow(instance.changedurationsubmissions(invalidSubsDuration, {from: accounts[0]})); }) }); 

Чтобы наши тесты были успешно выполнены, мы также должны сказать Truffle, что мы хотим развернуть StoryDao, потому что он не собирается делать это за нас. Итак, давайте создадим 3_deploy_storydao.js в migrations с содержимым, практически идентичным предыдущей миграции, которую мы написали:

 var Migrations = artifacts.require("./Migrations.sol"); var StoryDao = artifacts.require("./StoryDao.sol"); module.exports = function(deployer, network, accounts) { if (network == "development") { deployer.deploy(StoryDao, {from: accounts[0]}); } else { deployer.deploy(StoryDao); } }; 

На этом этапе нам также следует обновить (или создать, если он отсутствует) файл package.json в корне папки нашего проекта с учетом зависимостей, которые нам были нужны до сих пор и могут понадобиться в ближайшем будущем:

 { "name": "storydao", "devDependencies": { "babel-preset-es2015": "^6.18.0", "babel-preset-stage-2": "^6.24.1", "babel-preset-stage-3": "^6.17.0", "babel-polyfill": "^6.26.0", "babel-register": "^6.23.0", "dotenv": "^6.0.0", "truffle": "^4.1.12", "openzeppelin-solidity": "^1.10.0", "openzeppelin-solidity-metadata": "^1.2.0", "openzeppelin-zos": "", "truffle-wallet-provider": "^0.0.5", "ethereumjs-wallet": "^0.6.0", "web3": "^1.0.0-beta.34", "truffle-assertions": "^0.3.1" } } 

И файл .babelrc с содержанием:

 { "presets": ["es2015", "stage-2", "stage-3"] } 

Нам также необходимо указать Babel в нашей конфигурации Truffle, чтобы он знал, что должен использовать его при компиляции тестов.

Примечание. Babel — это надстройка для NodeJS, которая позволяет нам использовать JavaScript следующего поколения в NodeJS текущего поколения, поэтому мы можем писать такие вещи, как import и т. Д. Если вам это не понятно, просто проигнорируйте это и просто вставьте это дословно. Вам, вероятно, никогда не придется иметь дело с этим снова после его установки таким образом.

 require('dotenv').config(); ================== ADD THESE TWO LINES ================ require('babel-register'); require('babel-polyfill'); ======================================================= const WalletProvider = require("truffle-wallet-provider"); const Wallet = require('ethereumjs-wallet'); // ... 

Теперь, наконец, запустите truffle test . Вывод должен быть похож на этот:

Успешный тест

Для получения дополнительной информации о тестировании см. Этот учебник , который мы подготовили специально для тестирования умных контрактов.

В последующих частях этого курса мы пропустим тесты, поскольку их вывод из-за этого приведет к тому, что учебники станут слишком длинными, но, пожалуйста, обратитесь к окончательному исходному коду проекта, чтобы просмотреть их все. Процесс, который мы только что прошли, настроил среду для тестирования, так что вы можете просто написать тесты без дополнительной настройки.

Whitelist

Давайте теперь создадим механизм белых списков, который позволит пользователям участвовать в создании истории. Добавьте следующие скелеты функций в StoryDao.sol :

 function whitelistAddress(address _add) public payable { // whitelist sender if enough money was sent } function() external payable { // if not whitelisted, whitelist if paid enough // if whitelisted, but X tokens at X price for amount } 

Неименованная функция function() называется резервной функцией , и это функция, которая вызывается, когда деньги отправляются в этот контракт без специальной инструкции (т.е. без специального вызова другой функции). Это позволяет людям присоединиться к StoryDao, просто отправляя Ether в DAO и мгновенно получая белый список, или покупая токены, в зависимости от того, внесены они в белый список или нет.

Функция whitelistSender предназначена для белых списков и может вызываться напрямую, но мы позаботимся о том, чтобы резервная функция автоматически вызывала ее при получении какого-либо эфира, если отправитель еще не был добавлен в белый список. Функция whitelistAddress объявлена public поскольку она должна вызываться и из других контрактов, а резервная функция является external поскольку деньги будут поступать на этот адрес только с внешних адресов. Контракты, вызывающие этот контракт, могут легко вызывать необходимые функции напрямую.

Давайте сначала разберемся с резервной функцией.

 function() external payable { if (!whitelist[msg.sender]) { whitelistAddress(msg.sender); } else { // buyTokens(msg.sender, msg.value); } } 

Мы проверяем, отсутствует ли отправитель в белом списке, и делегируем вызов функции whitelistAddress . Обратите внимание, что мы закомментировали нашу функцию buyTokens потому что у нас ее еще нет.

Далее, давайте обработаем белый список.

 function whitelistAddress(address _add) public payable { require(!whitelist[_add], "Candidate must not be whitelisted."); require(!blacklist[_add], "Candidate must not be blacklisted."); require(msg.value >= whitelistfee, "Sender must send enough ether to cover the whitelisting fee."); whitelist[_add] = true; whitelistedNumber++; emit Whitelisted(_add, true); if (msg.value > whitelistfee) { // buyTokens(_add, msg.value.sub(whitelistfee)); } } 

Обратите внимание, что эта функция принимает адрес в качестве параметра и не извлекает его из сообщения (из транзакции). Это дает дополнительное преимущество тем, что люди могут вносить в белый список других людей, если кто-то не может позволить себе присоединиться к DAO, например.

Мы начинаем функцию с некоторых проверок работоспособности: отправитель уже не должен быть в белом или черном списке (запрещен) и должен был отправить достаточно, чтобы покрыть плату. Если эти условия являются удовлетворительными, адрес добавляется в белый список, генерируется событие из белого списка, и, наконец, если количество отправленного эфира превышает количество эфира, необходимое для покрытия платы за внесение в белый список, остаток используется для купить токены.

Примечание: мы используем sub вместо - для вычитания, потому что это функция SafeMath для безопасных вычислений.

Теперь пользователи могут получить себя или других пользователей из белого списка, если они отправляют 0,01 эфира или более в контракт StoryDao.

Вывод

Мы создали начальную часть нашего DAO в этом уроке, но предстоит еще много работы. Оставайтесь с нами: в следующей части мы разберемся с добавлением контента в историю!