Английский аукцион English auction for NFT - demonoved/A-trial-run-in-solidity GitHub Wiki

// SPDX-License-Identifier: MIT
// compiler version must be greater than or equal to 0.8.20 and less than 0.9.0
pragma solidity ^0.8.23;
// 1. продавец NFT разворачивает этот контракт
// 2. аукцион длится 7 дней
// 3. участники могут делать ставки, внося свой ETH на сумму, превышающую текущую самую высокую цену
// 4. все участники могут отозвать свою ставку, если она не является текущей самой высокой ставкой
// 5. тот, кто предложил самую высокую цену, становится новым владельцем NFT
// 6. продавец получает самую высокую ставку ETH
interface IERC721 {
    function safeTransferFrom( address from, address to, uint tokenId) external ;
    function transferFrom(address, address, uint) external;
}
contract EnglishAuction{
    event Start();
    event Bid(address indexed sender, uint amount);
    event WithDraw(address indexed bidder, uint amount);
    event End(address winner, uint amount);

    IERC721 public nft;
    uint public nftId;
    address payable public seller;
    uint public endAt;
    bool public started;
    bool public ended;
    address public  highestBidder;
    uint public highestBid;
    mapping(address => uint) public bids;

    constructor(address _nft, uint _nftId, uint _startingBid){
        nft = IERC721(_nft);
        nftId = _nftId;
        seller = payable(msg.sender);
        highestBid = _startingBid;
    }
    function start() external {
        require(!started, "started");
        require(msg.sender == seller, "not seller");
        nft.transferFrom(msg.sender, address(this), nftId);
        started = true;
        endAt = block.timestamp + 7 days;
        emit Start();
    }
    function bid() external payable {
        require(started, "not started");
        require(block.timestamp < endAt, "ended");
        require(msg.value > highestBid, "value < highest");

        if (highestBidder != address(0)){
            bids[highestBidder] += highestBid;
        }
        highestBidder = msg.sender;
        highestBid = msg.value;
        emit Bid(msg.sender, msg.value);
    }
    function withdraw() external {
        uint bal = bids[msg.sender];
        bids[msg.sender] = 0;
        payable (msg.sender).transfer(bal);
        emit WithDraw(msg.sender, bal);
    }
    function end() external {
        require(started, "not started");
        require(block.timestamp >= endAt, "not ended");
        require(!ended, "ended");
        ended = true;
        if (highestBidder != address(0)){
            nft.safeTransferFrom(address(this), highestBidder, nftId);
            seller.transfer(highestBid);
        } else {
            nft.safeTransferFrom(address(this), seller, nftId);
        }
        emit End(highestBidder, highestBid);
    }
}

Этот код представляет собой смарт-контракт на языке программирования Solidity для платформы Ethereum. Он реализует простой аукцион типа "Английский аукцион" для токенов стандарта ERC721, часто используемых для реализации уникальных невзаимозаменяемых токенов (NFT). Вот основные шаги его работы и особенности:

  1. Инициализация
    Смарт-контракт инициализируется с NFT (невзаимозаменяемый токен), его идентификатором и начальной ставкой.

  2. Запуск аукциона
    Продавец NFT (кто первым развернул контракт) начинает аукцион, вызывая функцию start(). Продавец должен передать NFT контракту перед началом аукциона. Аукцион длится 7 дней с момента вызова этой функции.

  3. Ставки
    Участники далее могут делать ставки путем отправки ETH, вызывая функцию bid(). Стоимость отправляемого ETH должна превышать текущую самую высокую цену. Есть логика для возврата предыдущей самой высокой ставке ее ETH.

  4. Отзыв ставки
    Участники могут отозвать свои ставки, если они больше не являются текущей самой высокой ставкой, путем вызова функции withdraw().

  5. Завершение аукциона
    По окончании аукциона (после того, как прошло 7 дней), функция end() позволяет завершить аукцион. Если есть победитель, NFT передается самому высокому участнику ставок, а ETH передается продавцу. Если ставок не было, NFT возвращается продавцу.

В фрагменте кода определены события для логирования важных действий: Start, Bid, Withdraw и End. Каждое из этих событий записывает информацию, когда соответствующие функции вызываются в блокчейне Ethereum.

В контракте также есть проверки условий (например, require), чтобы убедиться, что функции вызываются правильным образом (например, что аукцион не начался дважды, ставки делаются своевременно, а аукцион не может завершиться преждевременно и т.д.).

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

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

Возврат средств напрямую прежнему самому высокому участнику

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

Безопасный перевод NFT

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

Проверка переполнения времени

Добавление безопасной математики для проверки переполнения при вычислении endAt.

Использование модификаторов

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

Обработка граничных случаев

Что произойдет, если начальная ставка равна 0? Это потенциально может привести к путанице. Устанавливая минимальную начальную ставку, мы можем избежать ошибок при начале аукциона без ставок.

Профилактика атаки Reentrancy

Атака Reentrancy может произойти, если вызывается untrusted contract (непроверенный контракт) в withdraw(). Используя шаблон проверяй-эффекты-взаимодействие (check-effects-interaction), мы можем сделать эту функцию безопаснее.

События

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

Вот улучшенный код с учетом вышеупомянутых предложений:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract EnglishAuction is ReentrancyGuard {
    using SafeMath for uint256;

    event Start();
    event Bid(address indexed sender, uint amount);
    event Withdraw(address indexed bidder, uint amount);
    event End(address winner, uint amount);

    IERC721 public nft;
    uint public nftId;
    address payable public seller;
    uint public endAt;
    bool public started;
    bool public ended;
    address public highestBidder;
    uint public highestBid;
    mapping(address => uint) public bids;

    // Modifiers
    modifier auctionNotStarted() {
        require(!started, "Auction already started");
        _;
    }

    modifier onlySeller() {
        require(msg.sender == seller, "Can only be called by the seller");
        _;
    }

    modifier auctionStarted() {
        require(started, "Auction not started");
        _;
    }

    modifier auctionNotEnded() {
        require(block.timestamp < endAt, "Auction already ended");
        _;
    }

    modifier auctionEnded() {
        require(block.timestamp >= endAt, "Auction not yet ended");
        require(!ended, "Auction end already called");
        _;
    }

    constructor(address _nft, uint _nftId, uint _startingBid) {
        require(_startingBid > 0, "Starting bid must be greater than 0");
        nft = IERC721(_nft);
        nftId = _nftId;
        seller = payable(msg.sender);
        highestBid = _startingBid;
    }

    function start() external onlySeller auctionNotStarted {
        nft.transferFrom(msg.sender, address(this), nftId);
        started = true;
        endAt = block.timestamp.add(7 days);
        emit Start();
    }

    function bid() external payable auctionStarted auctionNotEnded nonReentrant {
        require(msg.value > highestBid, "Bid must be higher than current highest");

        if (highestBidder != address(0)) {
            payable(highestBidder).transfer(highestBid);
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
        emit Bid(msg.sender, msg.value);
    }

    function withdraw() external nonReentrant {
        uint bal = bids[msg.sender];
        require(bal > 0, "No funds to withdraw");

        bids[msg.sender] = 0;
        payable(msg.sender).transfer(bal);
        emit Withdraw(msg.sender, bal);
    }

    function end() external auctionStarted auctionEnded nonReentrant {
        ended = true;
        if (highestBidder != address(0)) {
            nft.safeTransferFrom(address(this), highestBidder, nftId);
            seller.transfer(highestBid);
        } else {
            nft.safeTransferFrom(address(this), seller, nftId);
        }
        emit End(highestBidder, highestBid);
    }
}