$ sudo teach IT
Модуль 6 из 7

Собираем краудфандинг целиком

Все кирпичики изучены - пора строить дом. Соберём единый контракт, пройдём его строка за строкой и развернём в Remix.

Вы узнаете
  • как собрать полный контракт из частей, изученных в модулях 2–5;
  • как автомат состояний (Active / Succeeded / Failed) наводит порядок в логике;
  • как развернуть и проверить контракт в Remix шаг за шагом.

6.1Полный контракт

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

solidity
// 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 - быстро, бесплатно и без риска для настоящих денег.

  1. Создайте Crowdfunding.sol, вставьте контракт, скомпилируйте (версия 0.8.24+).
  2. На вкладке Deploy задайте аргументы конструктора: _goal = 2000000000000000000 (2 эфира в вей) и _durationDays = 7. Нажмите Deploy.
  3. В поле Value вверху выставьте, например, 1 Ether, выберите второй аккаунт в списке Accounts и нажмите contribute. Повторите с третьего аккаунта - так наберётся цель.
  4. Проверьте contributions по своему адресу и contributorsCount - учёт должен сойтись.
  5. Чтобы проверить вывод, нужно «перемотать время». В Remix VM это делается сменой окружения или ожиданием; для учёбы временно уменьшите множитель в конструкторе (например, секунды вместо дней), чтобы дедлайн наступил быстро, затем вызовите withdraw владельцем.
  6. Для проверки возврата разверните новую кампанию, недоберите цель, дождитесь дедлайна и вызовите refund - вкладчик получит деньги назад.
Мини-практикум
  1. Добавьте событие GoalReached и испускайте его в contribute, когда сбор впервые достиг цели.
  2. Сделайте функцию timeLeft(), возвращающую секунды до дедлайна (0, если уже прошёл).
  3. Подумайте: что сломается, если убрать строку contributions[msg.sender] = 0; из refund? Воспроизведите мысленно атаку повторного входа.
Вывод модуля

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

Дальше · модуль 7

Контракт работает. Чем он отличается от того, что держит миллионы в основной сети? Последний модуль - о том, чего мы ещё не касались, и о дороге дальше.