⚙️Smart Contract

Our smart contract is ERC721 compatible and has a couple of additional function to batch send prizes, here is a breakdown of our callable functions:

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

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {Counters} from "@openzeppelin/contracts/utils/Counters.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import {ERC721, ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol";

contract SparkwaveLogicV2 is
    Multicall,
    Ownable2Step,
    ReentrancyGuard,
    EIP712,
    ERC721URIStorage
{
    using Counters for Counters.Counter;
    using ECDSA for bytes32;

    struct MintVoucher {
        string uuid;
        address to;
        string uri;
        bool soulbound;
    }
    struct Fees {
        uint256 disperseFungible;
        uint256 disperseNFT;
    }
    uint256 private _mintFee;
    mapping(address => Fees) private addressFees;
    mapping(uint256 => bool) public isSoulbound;
    Counters.Counter private _tokenIdCounter;
    mapping(uint256 => string) private _tokenURIs;
    mapping(bytes32 => bool) public minted;

    event Minted(address indexed to, string uri);

    constructor()
        EIP712("Sparkwave", "2")
        ERC721("SparkwaveV2", "SPARKWAVE-2")
    {}

    function setMintFee(uint256 fee) external onlyOwner {
        _mintFee = fee;
    }

    function setCustomerFees(
        address sender,
        Fees calldata fees
    ) external onlyOwner {
        addressFees[sender] = fees;
    }

    function typeHash() internal pure returns (bytes32) {
        return
            keccak256(
                "MintVoucher(string uuid,address to,string uri,bool soulbound)"
            );
    }

    function _hash(
        MintVoucher calldata voucher
    ) internal view returns (bytes32) {
        return
            _hashTypedDataV4(
                keccak256(
                    abi.encode(
                        typeHash(),
                        keccak256(bytes(voucher.uuid)),
                        voucher.to,
                        keccak256(bytes(voucher.uri)),
                        voucher.soulbound
                    )
                )
            );
    }

    /**
     * @notice Mint through voucher + signedMessage
     *
     */
    function mint(
        MintVoucher calldata voucher,
        bytes calldata signedMessage
    ) external payable nonReentrant {
        // Ensure not minted

        address signer = _hash(voucher).recover(signedMessage);
        require(signer == owner(), "INVALID_SIGNER");
        // If the receiver is `0x0`, send it to the sender.
        address to = voucher.to == address(0) ? _msgSender() : voucher.to;
        // fee
        payable(owner()).transfer(_mintFee);

        // check minted
        bytes32 uuid = keccak256(abi.encode(voucher.uuid));
        require(!minted[uuid], "DUPLICATE_USE");
        minted[uuid] = true;

        uint256 tokenId = _tokenIdCounter.current();
        _mint(to, tokenId);
        _setTokenURI(tokenId, voucher.uri);
        isSoulbound[tokenId] = voucher.soulbound;
        _tokenIdCounter.increment();

        emit Minted(voucher.to, voucher.uri);
    }

    function _transfer(
        address from,
        address to,
        uint256 tokenId
    ) internal override {
        require(!isSoulbound[tokenId], "SOULBOUND");
        super._transfer(from, to, tokenId);
    }

    function disperseNative(
        address[] calldata recipients,
        uint256[] calldata values
    ) external payable nonReentrant {
        uint256 leftover = msg.value;
        uint256 totalSent;
        for (uint256 i = 0; i < recipients.length; i++) {
            leftover -= values[i];
            totalSent += values[i];
            payable(recipients[i]).transfer(values[i]);
        }
        // fee
        payable(owner()).transfer(
            (totalSent * addressFees[_msgSender()].disperseFungible) / 1e5
        );
        if (leftover > 0) {
            payable(_msgSender()).transfer(leftover);
        }
    }

    function disperseERC20(
        IERC20 token,
        address[] calldata recipients,
        uint256[] calldata values
    ) external nonReentrant {
        uint256 totalSent;
        for (uint256 i = 0; i < recipients.length; i++) {
            token.transferFrom(_msgSender(), recipients[i], values[i]);
            totalSent += values[i];
        }
        // fee
        token.transferFrom(
            _msgSender(),
            owner(),
            (totalSent * addressFees[_msgSender()].disperseFungible) / 1e5
        );
    }

    function disperseERC721(
        IERC721 nftContract,
        address[] calldata recipients,
        uint256[] calldata ids
    ) external nonReentrant {
        require(
            nftContract.isApprovedForAll(_msgSender(), address(this)),
            "MISSING_APPROVAL"
        );
        for (uint256 i = 0; i < recipients.length; i++) {
            nftContract.safeTransferFrom(_msgSender(), recipients[i], ids[i]);
        }
        // fee
        payable(owner()).transfer(
            addressFees[_msgSender()].disperseNFT * recipients.length
        );
    }
}

Last updated