Собираем краудфандинг целиком
Все кирпичики изучены - пора строить дом. Соберём единый контракт, пройдём его строка за строкой и развернём в Remix.
- как собрать полный контракт из частей, изученных в модулях 2–5;
- как автомат состояний (Active / Succeeded / Failed) наводит порядок в логике;
- как развернуть и проверить контракт в Remix шаг за шагом.
6.1Полный контракт
Вот итог курса - цельный контракт краудфандинга. Если что-то в нём покажется незнакомым, это повод вернуться назад: ни одной новой конструкции здесь нет.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract Crowdfunding { // --- неизменные параметры кампании --- address public immutable owner; uint256 public immutable goal; uint256 public immutable deadline; uint256 public constant MIN_CONTRIBUTION = 0.01 ether; // --- учёт --- mapping(address => uint256) public contributions; address[] public contributors; uint256 public totalRaised; // сколько собрано вкладами bool public withdrawn; // --- события --- event Contributed(address indexed who, uint256 amount, uint256 total); event Withdrawn(uint256 amount); event Refunded(address indexed who, uint256 amount); // --- ошибки --- error NotOwner(); error CampaignEnded(); error TooSmall(); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } constructor(uint256 _goal, uint256 _durationDays) { require(_goal > 0, "goal must be > 0"); owner = msg.sender; goal = _goal; deadline = block.timestamp + _durationDays * 1 days; } // --- вклад --- function contribute() public payable { if (block.timestamp >= deadline) revert CampaignEnded(); if (msg.value < MIN_CONTRIBUTION) revert TooSmall(); if (contributions[msg.sender] == 0) contributors.push(msg.sender); contributions[msg.sender] += msg.value; totalRaised += msg.value; emit Contributed(msg.sender, msg.value, totalRaised); } receive() external payable { contribute(); } // --- вывод создателем при успехе --- function withdraw() external onlyOwner { require(block.timestamp >= deadline, "not finished"); require(totalRaised >= goal, "goal not reached"); require(!withdrawn, "already withdrawn"); withdrawn = true; // эффект uint256 amount = address(this).balance; (bool ok, ) = owner.call{value: amount}(""); // взаимодействие require(ok, "withdraw failed"); emit Withdrawn(amount); } // --- возврат вкладчику при провале (pull) --- function refund() external { require(block.timestamp >= deadline, "not finished"); require(totalRaised < goal, "goal reached"); uint256 amount = contributions[msg.sender]; require(amount > 0, "nothing to refund"); contributions[msg.sender] = 0; // эффект до взаимодействия! (bool ok, ) = msg.sender.call{value: amount}(""); require(ok, "refund failed"); emit Refunded(msg.sender, amount); } // --- вспомогательные чтения --- function isActive() external view returns (bool) { return block.timestamp < deadline; } function contributorsCount() external view returns (uint256) { return contributors.length; } }
6.2Как читать этот контракт
Пробегитесь по нему сверху вниз и сыграйте в «узнай приём»: каждая строка вам уже встречалась.
| Фрагмент | Откуда приём |
|---|---|
immutable / constant параметры |
Модуль 2 - значения, которые не меняются |
mapping + массив contributors |
Модуль 4 - учёт «кто сколько внёс» |
Конструктор, onlyOwner , custom errors |
Модуль 3 - запуск и защита функций |
events и emit |
Модуль 4 - сообщения наружу |
call , pull-возврат, порядок «эффект → взаимодействие» |
Модуль 5 - безопасная работа с деньгами |
Контракт рабочий и безопасный для учебных целей, но ему есть куда расти: наш onlyOwner стоит заменить проверенным Ownable из OpenZeppelin, к refund добавить замок nonReentrant, а состояние кампании вынести в явный enum State. Всем этим займётся продвинутый курс.
- Раз он рабочий, почему бы не запустить его в основной сети прямо сейчас?
- Потому что «работает в Remix» и «выдерживает людей, которые профессионально ищут, где бы поживиться» - две разные планки. До второй мы дойдём в следующих курсах.
6.3Разворачиваем и проверяем в Remix
Проверим контракт целиком в Remix VM - быстро, бесплатно и без риска для настоящих денег.
- Создайте
Crowdfunding.sol, вставьте контракт, скомпилируйте (версия 0.8.24+). - На вкладке Deploy задайте аргументы конструктора:
_goal=2000000000000000000(2 эфира в вей) и_durationDays=7. Нажмите Deploy. - В поле Value вверху выставьте, например, 1 Ether, выберите второй аккаунт в списке Accounts и нажмите
contribute. Повторите с третьего аккаунта - так наберётся цель. - Проверьте
contributionsпо своему адресу иcontributorsCount- учёт должен сойтись. - Чтобы проверить вывод, нужно «перемотать время». В Remix VM это делается сменой окружения или ожиданием; для учёбы временно уменьшите множитель в конструкторе (например, секунды вместо дней), чтобы дедлайн наступил быстро, затем вызовите
withdrawвладельцем. - Для проверки возврата разверните новую кампанию, недоберите цель, дождитесь дедлайна и вызовите
refund- вкладчик получит деньги назад.
- Добавьте событие
GoalReachedи испускайте его вcontribute, когда сбор впервые достиг цели. - Сделайте функцию
timeLeft(), возвращающую секунды до дедлайна (0, если уже прошёл). - Подумайте: что сломается, если убрать строку
contributions[msg.sender] = 0;изrefund? Воспроизведите мысленно атаку повторного входа.
Вы прошли путь от первой строки до цельного контракта, который принимает деньги, ведёт учёт, отдаёт сбор при успехе и возвращает вклады при провале - и делает это безопасно. Небольшая, но настоящая децентрализованная логика без единого посредника. Можно собой гордиться.
Контракт работает. Чем он отличается от того, что держит миллионы в основной сети? Последний модуль - о том, чего мы ещё не касались, и о дороге дальше.