Статьи

Ethereum DApps: кросс-контрактная коммуникация и продажа токенов

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

Добавление токенов

Чтобы контракт мог взаимодействовать с другим контрактом, ему необходимо знать интерфейс этого другого контракта — доступные ему функции. Поскольку наш токен TNS имеет довольно простой интерфейс, мы можем включить его как таковой в контракт нашего DAO, выше декларации contract StoryDao и в наших операторах import :

 contract LockableToken is Ownable { function totalSupply() public view returns (uint256); function balanceOf(address who) public view returns (uint256); function transfer(address to, uint256 value) public returns (bool); event Transfer(address indexed from, address indexed to, uint256 value); function allowance(address owner, address spender) public view returns (uint256); function transferFrom(address from, address to, uint256 value) public returns (bool); function approve(address spender, uint256 value) public returns (bool); event Approval(address indexed owner, address indexed spender, uint256 value); function approveAndCall(address _spender, uint256 _value, bytes _data) public payable returns (bool); function transferAndCall(address _to, uint256 _value, bytes _data) public payable returns (bool); function transferFromAndCall(address _from, address _to, uint256 _value, bytes _data) public payable returns (bool); function increaseLockedAmount(address _owner, uint256 _amount) public returns (uint256); function decreaseLockedAmount(address _owner, uint256 _amount) public returns (uint256); function getLockedAmount(address _owner) view public returns (uint256); function getUnlockedAmount(address _owner) view public returns (uint256); } 

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

Теперь мы можем использовать эти функции в договоре DAO. План следующий:

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

Но как DAO узнает, по какому адресу развернут токен? Мы говорим это.

Сначала мы добавляем новую переменную в начало контракта DAO:

 LockableToken public token; 

Затем мы добавим несколько функций:

 constructor(address _token) public { require(_token != address(0), "Token address cannot be null-address"); token = LockableToken(_token); } 

Конструктор — это функция, которая вызывается автоматически при развертывании контракта. Это полезно для инициализации значений, таких как связанные контракты, значения по умолчанию и т. Д. В нашем случае мы будем использовать его для использования и сохранения адреса токена TNS. require проверка, чтобы убедиться, что адрес токена действителен.

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

 event TokenAddressChange(address token); function daoTokenBalance() public view returns (uint256) { return token.balanceOf(address(this)); } function changeTokenAddress(address _token) onlyOwner public { require(_token != address(0), "Token address cannot be null-address"); token = LockableToken(_token); emit TokenAddressChange(_token); } 

Первая функция настроена на view потому что она не меняет состояние блокчейна; это не меняет никаких значений. Это означает, что это бесплатный, доступный только для чтения вызов функции для блокчейна: для него не требуется платная транзакция. Он также возвращает остаток токенов в виде числа, поэтому его необходимо объявить в сигнатуре функции с помощью returns (uint256) . Токен имеет функцию balanceOf (см. Интерфейс, который мы вставили выше), и он принимает один параметр — адрес, чей баланс проверять. Мы проверяем баланс нашего (этого) DAO, поэтому «это», и мы превращаем «это» в адрес с address() .

Функция изменения адреса токена позволяет владельцу (администратору) изменять контракт токена. Это идентично логике конструктора.

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

Покупка токенов

Как и в предыдущей части серии, пользователи могут покупать токены:

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

Однако есть одна оговорка. Когда кто-то вызывает функцию buyTokens извне, мы хотим, чтобы она не buyTokens если в DAO недостаточно токенов для продажи. Но когда кто-то покупает токены с помощью функции белого списка, отправляя слишком много в первой попытке создания белого списка, мы не хотим, чтобы он потерпел неудачу, потому что тогда процесс создания белого списка будет отменен, поскольку все сразу выходит из строя. Транзакции в Ethereum являются атомарными: либо все должно преуспеть, либо ничего. Итак, мы сделаем две функции buyTokens .

 // This goes at the top of the contract with other properties uint256 public tokenToWeiRatio = 10000; function buyTokensThrow(address _buyer, uint256 _wei) external { require(whitelist[_buyer], "Candidate must be whitelisted."); require(!blacklist[_buyer], "Candidate must not be blacklisted."); uint256 tokens = _wei * tokenToWeiRatio; require(daoTokenBalance() >= tokens, "DAO must have enough tokens for sale"); token.transfer(_buyer, tokens); } function buyTokensInternal(address _buyer, uint256 _wei) internal { require(!blacklist[_buyer], "Candidate must not be blacklisted."); uint256 tokens = _wei * tokenToWeiRatio; if (daoTokenBalance() < tokens) { msg.sender.transfer(_wei); } else { token.transfer(_buyer, tokens); } } 

Итак, существует 100 миллионов токенов TNS. Если мы установим цену 10000 токенов за один эфир, это снизится до 4–5 центов за токен, что является приемлемым.

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

Часть token.transfer(_buyer, tokens) — это мы, использующие контракт токена TNS, чтобы инициировать передачу из текущего местоположения (DAO) в конечный _buyer для количества tokens .

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

Структуры и представления

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

 uint256 public submissionZeroFee = 0.0001 ether; uint256 public nonDeletedSubmissions = 0; function calculateSubmissionFee() view internal returns (uint256) { return submissionZeroFee * nonDeletedSubmissions; } 

Примечание: твердость имеет встроенные единицы времени и эфира. Подробнее о них читайте здесь .

Эта плата может быть изменена только владельцем, но только снижена. Для увеличения нужно голосование. Напишем функцию уменьшения:

 function lowerSubmissionFee(uint256 _fee) onlyOwner external { require(_fee < submissionZeroFee, "New fee must be lower than old fee."); submissionZeroFee = _fee; emit SubmissionFeeChanged(_fee); } 

Мы отправляем событие, чтобы уведомить всех наблюдающих клиентов о том, что плата была изменена, поэтому давайте объявим это событие:

 event SubmissionFeeChanged(uint256 newFee); 

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

 struct Submission { bytes content; bool image; uint256 index; address submitter; bool exists; } 

Это похоже на «тип объекта» в нашем умном контракте. Объект имеет свойства разных типов. content является значением типа bytes . Свойство image является логическим значением, обозначающим, является ли оно изображением (true / false). index представляет собой число, равное обычному номеру представления; его индекс в списке всех представленных материалов (0, 1, 2, 3…). submitter — это адрес учетной записи, которая отправила запись, и там exists флаг существует, потому что в отображениях все значения всех ключей инициализируются значениями по умолчанию (false), даже если ключи еще не существуют.

Другими словами, если у вас есть сопоставление address => bool , то в этом сопоставлении все адреса в мире будут иметь значение «false». Так работает Ethereum. Таким образом, проверяя, существует ли отправка с определенным хешем, мы получим «да», тогда как отправка может вообще не быть. Флаг слияния помогает в этом. Это позволяет нам проверить, что отправка есть и существует, то есть была отправлена, а не просто неявно добавлена ​​EVM. Кроме того, это значительно облегчает последующее «удаление» записей.

Примечание: технически мы также можем проверить, чтобы убедиться, что адрес отправителя не является нулевым адресом.

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

 event SubmissionCreated(uint256 index, bytes content, bool image, address submitter); event SubmissionDeleted(uint256 index, bytes content, bool image, address submitter); 

Хотя есть проблема. Отображения в Ethereum не повторяемы: мы не можем перебрать их без значительного взлома .

Чтобы просмотреть их все, мы создадим массив идентификаторов для этих представлений, в котором ключи массива будут индексом представления, а значения будут уникальными хешами, которые мы будем генерировать для каждой отправки. Солидность предоставляет нам алгоритм хэширования keccak256 для генерации хешей из произвольных значений, и мы можем использовать его в тандеме с текущим номером блока, чтобы убедиться, что запись не дублируется в одном и том же блоке и получить некоторую степень уникальности для каждой записи. Мы используем это так: keccak256(abi.encodePacked(_content, block.number)); , Нам нужно encodePacked в encodePacked переменные, переданные алгоритму, потому что он требует от нас одного параметра. Вот что делает эта функция.

Нам также нужно где-то хранить представления, поэтому давайте определим еще две переменные контракта.

 mapping (bytes32 => Submission) public submissions; bytes32[] public submissionIndex; 

Хорошо, давайте сейчас попробуем создать функцию createSubmission .

 function createSubmission(bytes _content, bool _image) external payable { uint256 fee = calculateSubmissionFee(); require(msg.value >= fee, "Fee for submitting an entry must be sufficient."); bytes32 hash = keccak256(abi.encodePacked(_content, block.number)); require(!submissions[hash].exists, "Submission must not already exist in same block!"); submissions[hash] = Submission( _content, _image, submissionIndex.push(hash), msg.sender, true ); emit SubmissionCreated( submissions[hash].index, submissions[hash].content, submissions[hash].image, submissions[hash].submitter ); nonDeletedSubmissions += 1; } 

Давайте пройдемся по этой строке построчно:

 function createSubmission(bytes _content, bool _image) external payable { 

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

 uint256 fee = calculateSubmissionFee(); require(msg.value >= fee, "Fee for submitting an entry must be sufficient."); 

Затем мы рассчитываем, сколько стоит отправить новую запись, а затем проверяем, равно ли значение, переданное вместе с транзакцией, комиссии или больше.

 bytes32 hash = keccak256(abi.encodePacked(_content, block.number)); require(!submissions[hash].exists, "Submission must not already exist in same block!"); 

Затем мы вычисляем хеш этой записи ( bytes32 — это массив фиксированного размера, bytes32 из 32 байтов, то есть 32 символа, что также является длиной вывода keccak256 ). Мы используем этот хеш, чтобы выяснить, существует ли уже отправка с этим хешем, и отменим все, если это произойдет.

 submissions[hash] = Submission( _content, _image, submissionIndex.push(hash), msg.sender, true ); 

Эта часть создает новую отправку в месте хэша в сопоставлении submissions . Он просто передает значения через новую структуру, как определено выше в контракте. Обратите внимание, что, хотя вы можете привыкнуть к new ключевому слову из других языков, здесь это не обязательно (или не разрешено). Затем мы генерируем событие (не nonDeletedSubmissions += 1; пояснений) и, наконец, есть nonDeletedSubmissions += 1; : это то, что увеличивает плату за следующую отправку (см. calculateSubmissionFee ).

Но здесь не хватает логики. Нам все еще нужно:

  • аккаунт для изображений, и
  • проверьте наличие белого / черного списка и владение 1 токеном TNS при отправке аккаунтов.

Давайте сделаем изображения в первую очередь. Наш первоначальный план сказал, что изображение может быть представлено только каждые 50 текстов. Нам понадобятся еще два свойства контракта:

 uint256 public imageGapMin = 50; uint256 public imageGap = 0; 

Конечно, вы уже можете предположить, как мы собираемся справиться с этим? Давайте добавим следующее в наш метод createSubmission , непосредственно перед созданием нового представления с submissions[hash] = ...

 if (_image) { require(imageGap >= imageGapMin, "Image can only be submitted if more than {imageGapMin} texts precede it."); imageGap = 0; } else { imageGap += 1; } 

Чрезвычайно просто: если предполагается, что запись является изображением, то сначала проверьте, что промежуток между изображениями больше 49, и установите значение 0, если оно есть. В противном случае увеличьте разрыв на единицу. Точно так же, каждая 50-я (или более) заявка теперь может быть изображением.

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

 require(token.balanceOf(msg.sender) >= 10**token.decimals()); require(whitelist[msg.sender], "Must be whitelisted"); require(!blacklist[msg.sender], "Must not be blacklisted"); 

В первой строке проверяется, имеет ли отправитель сообщения больше токенов, чем 10, в степени числа десятичных знаков в контракте токена (поскольку мы можем изменить адрес токена, поэтому возможно, что другой токен займет место нашего токена позже, и этот не имеет 18 десятичных знаков!). Другими словами, 10**token.decimals — это, в нашем случае, 10**18 , что составляет 1 000 000 000 000 000 000 или 1, за которым следуют 18 нулей. Если в нашем токене 18 знаков после запятой, это 1.000000000000000000 или один (1) токен TNS. Обратите внимание, что ваш компилятор или линтер могут дать вам некоторые предупреждения при анализе этого кода. Это связано с тем, что свойство decimals токена является открытым, поэтому его функция получения генерируется автоматически ( decimals() ), но его явно нет в интерфейсе токена, который мы указали в начале контракта. Чтобы обойти это, мы можем изменить интерфейс, добавив следующую строку:

 function decimals() public view returns (uint256); 

Еще одна вещь: поскольку за использование договора в настоящее время взимается плата в размере 1%, давайте отложим сумму, которую владелец может снять, и оставшуюся часть оставим в DAO. Самый простой способ сделать это — отследить, сколько владелец может снять, и увеличить это число после создания каждого представления. Давайте добавим новое свойство в контракт:

 uint256 public withdrawableByOwner = 0; 

А затем добавьте это в конец нашей функции createSubmission :

 withdrawableByOwner += fee.div(daofee); 

Мы можем позволить владельцу снять с помощью функции, подобной этой:

 function withdrawToOwner() public { owner.transfer(withdrawableByOwner); withdrawableByOwner = 0; } 

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

 function withdrawAmountToOwner(uint256 _amount) public { uint256 withdraw = _amount; if (withdraw > withdrawableByOwner) { withdraw = withdrawableByOwner; } owner.transfer(withdraw); withdrawableByOwner = withdrawableByOwner.sub(withdraw); } 

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

 function submissionExists(bytes32 hash) public view returns (bool) { return submissions[hash].exists; } 

Некоторые другие вспомогательные функции для чтения представлений также будут необходимы:

 function getSubmission(bytes32 hash) public view returns (bytes content, bool image, address submitter) { return (submissions[hash].content, submissions[hash].image, submissions[hash].submitter); } function getAllSubmissionHashes() public view returns (bytes32[]) { return submissionIndex; } function getSubmissionCount() public view returns (uint256) { return submissionIndex.length; } 

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

Полная функция createSubmission теперь выглядит так:

 function createSubmission(bytes _content, bool _image) storyActive external payable { require(token.balanceOf(msg.sender) >= 10**token.decimals()); require(whitelist[msg.sender], "Must be whitelisted"); require(!blacklist[msg.sender], "Must not be blacklisted"); uint256 fee = calculateSubmissionFee(); require(msg.value >= fee, "Fee for submitting an entry must be sufficient."); bytes32 hash = keccak256(abi.encodePacked(_content, block.number)); require(!submissionExists(hash), "Submission must not already exist in same block!"); if (_image) { require(imageGap >= imageGapMin, "Image can only be submitted if more than {imageGapMin} texts precede it."); imageGap = 0; } else { imageGap += 1; } submissions[hash] = Submission( _content, _image, submissionIndex.push(hash), msg.sender, true ); emit SubmissionCreated( submissions[hash].index, submissions[hash].content, submissions[hash].image, submissions[hash].submitter ); nonDeletedSubmissions += 1; withdrawableByOwner += fee.div(daofee); } 

Удаление

Так что насчет удаления представлений? Это достаточно просто: мы просто переключаем флаг exists на false !

 function deleteSubmission(bytes32 hash) internal { require(submissionExists(hash), "Submission must exist to be deletable."); Submission storage sub = submissions[hash]; sub.exists = false; deletions[submissions[hash].submitter] += 1; emit SubmissionDeleted( sub.index, sub.content, sub.image, sub.submitter ); nonDeletedSubmissions -= 1; } 

Во-первых, мы проверяем, что отправка существует и еще не удалена; затем мы извлекаем его из хранилища. Затем мы устанавливаем для флага его exists значение false, увеличиваем количество удалений в DAO для этого адреса на 1 (полезно при отслеживании того, сколько записей пользователь удалил для них позже; это может привести к черному списку!) испустить событие удаления.

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

 mapping (address => uint256) public deletions; 

Развертывания становятся более сложными

Теперь, когда мы используем токены в другом контракте, нам нужно обновить скрипт развертывания ( 3_deploy_storydao ), чтобы передать адрес токена в конструктор StoryDao, например, так:

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

Подробнее о настройке развертываний читайте здесь .

Вывод

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