Статьи

Построение приложений Эфириум: голосование с использованием пользовательских токенов

В пятой части этой серии руководств по созданию DApps с помощью Ethereum мы рассматривали добавление контента в историю, рассматривая, как добавить участникам возможность покупать токены в DAO и добавлять материалы в историю. Теперь пришло время для окончательной формы DAO: голосование, внесение в черный / черный список, а также распределение и снятие дивидендов. Мы добавим некоторые дополнительные вспомогательные функции для хорошей меры.

Если вы потерялись во всем этом, полный исходный код доступен в репозитории .

Голоса и предложения

Мы будем выдавать предложения и голосовать с помощью голосов. Нам нужны две новые структуры:

struct Proposal {
    string description;
    bool executed;
    int256 currentResult;
    uint8 typeFlag; // 1 = delete
    bytes32 target; // ID of the proposal target. I.e. flag 1, target XXXXXX (hash) means proposal to delete submissions[hash]
    uint256 creationDate;
    uint256 deadline;
    mapping (address => bool) voters;
    Vote[] votes;
    address submitter;
}

Proposal[] public proposals;
uint256 proposalCount = 0;
event ProposalAdded(uint256 id, uint8 typeFlag, bytes32 hash, string description, address submitter);
event ProposalExecuted(uint256 id);
event Voted(address voter, bool vote, uint256 power, string justification);

struct Vote {
    bool inSupport;
    address voter;
    string justification;
    uint256 power;
}

Предложение будет иметь отображение избирателей, чтобы люди не могли голосовать по предложению дважды, и некоторые другие метаданные, которые должны быть понятны. Голосование будет либо голосованием «да», либо «нет», и запомнит избирателя вместе с его обоснованием для голосования определенным образом, а право голоса — количество жетонов, которое они хотят посвятить голосованию за это предложение. Мы также добавили массив предложений, чтобы их можно было где-то хранить, и счетчик для подсчета количества предложений.

Давайте создадим их сопутствующие функции сейчас, начиная с функции голосования:

 modifier tokenHoldersOnly() {
    require(token.balanceOf(msg.sender) >= 10**token.decimals());
    _;
}

function vote(uint256 _proposalId, bool _vote, string _description, uint256 _votePower) tokenHoldersOnly public returns (int256) {

    require(_votePower > 0, "At least some power must be given to the vote.");
    require(uint256(_votePower) <= token.balanceOf(msg.sender), "Voter must have enough tokens to cover the power cost.");

    Proposal storage p = proposals[_proposalId];

    require(p.executed == false, "Proposal must not have been executed already.");
    require(p.deadline > now, "Proposal must not have expired.");
    require(p.voters[msg.sender] == false, "User must not have already voted.");

    uint256 voteid = p.votes.length++;
    Vote storage pvote = p.votes[voteid];
    pvote.inSupport = _vote;
    pvote.justification = _description;
    pvote.voter = msg.sender;
    pvote.power = _votePower;

    p.voters[msg.sender] = true;

    p.currentResult = (_vote) ? p.currentResult + int256(_votePower) : p.currentResult - int256(_votePower);
    token.increaseLockedAmount(msg.sender, _votePower);

    emit Voted(msg.sender, _vote, _votePower, _description);
    return p.currentResult;
}

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

Функция голосования выполняет некоторые проверки работоспособности, такие как право голоса положительное, у избирателя достаточно токенов, чтобы фактически проголосовать, и т. Д. Затем мы извлекаем предложение из хранилища и следим, чтобы оно не истекло и не было выполнено. Не имеет смысла голосовать за предложение, которое уже сделано. Мы также должны убедиться, что этот человек еще не проголосовал. Мы могли бы позволить изменить силу голосования, но это открывает DAO для некоторых уязвимостей, таких как люди, снимающие свои голоса в последнюю минуту и ​​т.д.

Затем мы регистрируем новое Голосование в предложении, меняем текущий результат для удобного просмотра результатов и, наконец, отправляем событие Voted. Но что такое token.increaseLockedAmount

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

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

Голосование за удаление и черный список

Как установлено в части 1 этой серии, у нас запланированы три функции удаления записей:

  1. Удалить запись : при подтверждении голосованием целевая запись удаляется. Время голосования: 48 часов .
  2. Запись об аварийном удалении [Только владелец] : может быть активирован только владельцем. При подтверждении голосованием целевая запись удаляется. Время голосования: 24 часа .
  3. Аварийное удаление изображения [Только владелец] : применяется только к записям изображения. Может быть вызвано только владельцем. При подтверждении голосованием целевая запись удаляется. Время голосования: 4 часа .

Пять удалений записей одного адреса приводят к черному списку.

Давайте посмотрим, как мы можем сделать это сейчас. Прежде всего, функции удаления:

 modifier memberOnly() {
    require(whitelist[msg.sender]);
    require(!blacklist[msg.sender]);
    _;
}

function proposeDeletion(bytes32 _hash, string _description) memberOnly public {

    require(submissionExists(_hash), "Submission must exist to be deletable");

    uint256 proposalId = proposals.length++;
    Proposal storage p = proposals[proposalId];
    p.description = _description;
    p.executed = false;
    p.creationDate = now;
    p.submitter = msg.sender;
    p.typeFlag = 1;
    p.target = _hash;

    p.deadline = now + 2 days;

    emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender);
    proposalCount = proposalId + 1;
}

function proposeDeletionUrgent(bytes32 _hash, string _description) onlyOwner public {

    require(submissionExists(_hash), "Submission must exist to be deletable");

    uint256 proposalId = proposals.length++;
    Proposal storage p = proposals[proposalId];
    p.description = _description;
    p.executed = false;
    p.creationDate = now;
    p.submitter = msg.sender;
    p.typeFlag = 1;
    p.target = _hash;

    p.deadline = now + 12 hours;

    emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender);
    proposalCount = proposalId + 1;
}    

function proposeDeletionUrgentImage(bytes32 _hash, string _description) onlyOwner public {

    require(submissions[_hash].image == true, "Submission must be existing image");

    uint256 proposalId = proposals.length++;
    Proposal storage p = proposals[proposalId];
    p.description = _description;
    p.executed = false;
    p.creationDate = now;
    p.submitter = msg.sender;
    p.typeFlag = 1;
    p.target = _hash;

    p.deadline = now + 4 hours;

    emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender);
    proposalCount = proposalId + 1;
}

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

Далее давайте посмотрим, как выполнить предложение. Чтобы быть исполняемым, предложение должно иметь достаточное количество голосов и должно быть в срок. Функция выполнения примет идентификатор предложения для выполнения. Нет простого способа заставить EVM выполнить все ожидающие предложения одновременно. Возможно, слишком многие ожидают выполнения и что они внесут большие изменения в данные в DAO, которые могут превысить газовый лимит блоков Ethereum, что приведет к сбою транзакции. Намного проще создать функцию ручного выполнения, которую могут вызывать все, у кого есть четко определенные правила, поэтому сообщество может следить за предложениями, которые необходимо выполнить.

 function executeProposal(uint256 _id) public {
    Proposal storage p = proposals[_id];
    require(now >= p.deadline && !p.executed);

    if (p.typeFlag == 1 && p.currentResult > 0) {
        assert(deleteSubmission(p.target));
    }

    uint256 len = p.votes.length;
    for (uint i = 0; i < len; i++) {
        token.decreaseLockedAmount(p.votes[i].voter, p.votes[i].power);
    }

    p.executed = true;
    emit ProposalExecuted(_id);
}

Мы забираем предложение по его идентификатору, проверяем, чтобы оно соответствовало требованиям о том, что оно не было выполнено, а срок истек, а затем, если тип предложения — предложение об удалении, а результат голосования положительный, мы используем уже написанную функцию удаления, наконец, добавив новое событие, которое мы добавили (добавьте его в начало контракта). Вызов assertrequire Require используется для предварительных условий. Функционально они идентичны, с той разницей, что операторы assert Функция заканчивается разблокировкой токенов для всех голосов в этом предложении.

Мы можем использовать этот же подход для добавления предложений других типов, но сначала давайте обновим функцию deleteSubmission Давайте обновим функцию deleteSubmission

 function deleteSubmission(bytes32 hash) internal returns (bool) {
    require(submissionExists(hash), "Submission must exist to be deletable.");
    Submission storage sub = submissions[hash];

    sub.exists = false;
    deletions[submissions[hash].submitter] += 1;
    if (deletions[submissions[hash].submitter] >= 5) {
        blacklistAddress(submissions[hash].submitter);
    }

    emit SubmissionDeleted(
        sub.index,
        sub.content,
        sub.image,
        sub.submitter
    );

    nonDeletedSubmissions -= 1;
    return true;
}

Так-то лучше. Авто-черный список на пять удаляет. Хотя было бы несправедливо не дать адресам из черного списка возможность выкупить себя. Нам также нужно определить саму функцию внесения в черный список. Давайте сделаем обе эти вещи и установим плату за снятие черного списка, например, 0,05 эфира.

 function blacklistAddress(address _offender) internal {
    require(blacklist[_offender] == false, "Can't blacklist a blacklisted user :/");
    blacklist[_offender] == true;
    token.increaseLockedAmount(_offender, token.getUnlockedAmount(_offender));
    emit Blacklisted(_offender, true);
}

function unblacklistMe() payable public {
    unblacklistAddress(msg.sender);
}

function unblacklistAddress(address _offender) payable public {
    require(msg.value >= 0.05 ether, "Unblacklisting fee");
    require(blacklist[_offender] == true, "Can't unblacklist a non-blacklisted user :/");
    require(notVoting(_offender), "Offender must not be involved in a vote.");
    withdrawableByOwner = withdrawableByOwner.add(msg.value);
    blacklist[_offender] = false;
    token.decreaseLockedAmount(_offender, token.balanceOf(_offender));
    emit Blacklisted(_offender, false);
}

function notVoting(address _voter) internal view returns (bool) {
    for (uint256 i = 0; i < proposalCount; i++) {
        if (proposals[i].executed == false && proposals[i].voters[_voter] == true) {
            return false;
        }
    }
    return true;
}

Обратите внимание, что токены аккаунта, помещенные в черный список, блокируются до тех пор, пока они не отправят плату за снятие черного списка.

Другие типы голосов

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

Конец главы

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

 bool public active = true;
event StoryEnded();

Тогда давайте построим функцию:

 function endStory() storyActive external {
    withdrawToOwner();
    active = false;
    emit StoryEnded();
}

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

 modifier storyActive() {
    require(active == true);
    _;
}

Затем мы добавим этот модификатор ко всем функциям, за исключением withdrawToOwner

 function whitelistAddress(address _add) storyActive public payable {

В случае, если в DAO остались какие-либо токены, давайте возьмем их обратно и возьмем на себя владение этими токенами, чтобы потом использовать их в другой истории:

 function withdrawLeftoverTokens() external onlyOwner {
    require(active == false);
    token.transfer(msg.sender, token.balanceOf(address(this)));
    token.transferOwnership(msg.sender);
}

function unlockMyTokens() external {
    require(active == false);
    require(token.getLockedAmount(msg.sender) > 0);

    token.decreaseLockedAmount(msg.sender, token.getLockedAmount(msg.sender));
}

Функция unlockMyTokens Этого не должно быть, и эту функцию следует удалить с помощью большого количества тестов.

Распределение и снятие дивидендов

Теперь, когда история закончилась, сборы, полученные за представления, должны быть распределены среди всех обладателей токенов. Мы можем повторно использовать наш белый список, чтобы отметить всех, кто снял сборы:

 function withdrawDividend() memberOnly external {
    require(active == false);
    uint256 owed = address(this).balance.div(whitelistedNumber);
    msg.sender.transfer(owed);
    whitelist[msg.sender] = false;
    whitelistedNumber--;
}

Если эти дивиденды не отозваны в течение определенного периода времени, владелец может получить остальные:

 function withdrawEverythingPostDeadline() external onlyOwner {
    require(active == false);
    require(now > deadline + 14 days);
    owner.transfer(address(this).balance);
}

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

Проблемы развертывания

Учитывая, что наш контракт сейчас довольно большой, его развертывание и / или тестирование может превысить газовый лимит блоков Эфириума. Это то, что ограничивает развертывание больших приложений в сети Ethereum. В любом случае, чтобы развернуть его, попробуйте использовать оптимизатор кода во время компиляции, изменив файл truffle.jssolc

 // ...

module.exports = {
  solc: {
    optimizer: {
      enabled: true,
      runs: 200
    }
  },
  networks: {
    development: {
// ...

Это позволит запустить оптимизатор в коде 200 раз, чтобы найти области, которые можно минимизировать, удалить или абстрагировать до развертывания, что должно значительно снизить стоимость развертывания.

Вывод

На этом мы заканчиваем нашу исчерпывающую разработку DAO — но курс еще не закончен! Нам все еще нужно создать и развернуть пользовательский интерфейс для этой истории. К счастью, с полностью размещенной задней частью на блокчейне, сборка передней части намного менее сложна. Давайте посмотрим на это в нашей предпоследней части этой серии.