Untitled

 avatar
unknown
plain_text
a year ago
8.4 kB
17
Indexable
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * RenovaPresale – Final Version with beneficiary in voucher
 * - Multichain presale (ETH HUB with claim, BSC SPOKE without claim)
 * - Accepts native currency (ETH/BNB) or USDT (adaptive decimals)
 * - On ETH: claim via off-chain EIP-712 voucher (beneficiary required, must equal msg.sender)
 */

import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

interface IERC20Metadata is IERC20 {
    function decimals() external view returns (uint8);
}

contract RenovaPresale is Ownable, ReentrancyGuard, EIP712 {
    using SafeERC20 for IERC20;

    bool    public immutable isHub;                 // true on ETH (claim enabled), false on BSC
    IERC20  public rvaToken;                        // $RVA on ETH; zero on BSC
    IERC20  public usdt;                            // USDT on the current chain
    AggregatorV3Interface public priceFeed;         // native/USD Chainlink feed
    uint256 public pricePerRVAUSD;                  // RVA price in USD (1e18)
    bool    public isPresaleActive;
    bool    public isClaimEnabled;

    uint256 public tokensSold;                      // total tokens sold across this chain

    address public voucherSigner;                   // backend address signing vouchers
    mapping(bytes32 => bool) public usedNonce;      // prevent replay

    // Claim typehash includes beneficiary
    bytes32 internal constant CLAIM_TYPEHASH =
        keccak256("Claim(address beneficiary,uint256 amount,uint256 deadline,bytes32 nonce)");

    enum PayKind { NATIVE, USDT }

    struct Purchase {
        uint256 tokens;       // in 1e18
        uint256 usdPaid;      // in 1e18
        bool    paidWithUSDT;
        uint256 rawAmount;    // wei or USDT units
        uint256 timestamp;
    }
    mapping(address => Purchase[]) public purchases;

    event PresaleStarted();
    event PresaleEnded();
    event ClaimEnabled();
    event TokensPurchased(
        address indexed payer,
        PayKind payKind,
        address payToken,
        uint256 rawAmount,
        uint256 usdValue1e18,
        uint256 tokens1e18
    );
    event TokensClaimed(address indexed claimer, uint256 amount1e18);

    constructor(
        bool _isHub,
        IERC20 _rvaToken,
        IERC20 _usdt,
        AggregatorV3Interface _priceFeed,
        uint256 _pricePerRVAUSD,
        address _voucherSigner
    ) Ownable(msg.sender) EIP712("RenovaPresale", "1") {
        isHub = _isHub;
        rvaToken = _rvaToken;      // zero on BSC
        usdt = _usdt;
        priceFeed = _priceFeed;
        require(_pricePerRVAUSD > 0, "price=0");
        pricePerRVAUSD = _pricePerRVAUSD;
        voucherSigner = _voucherSigner;
    }

    // --- Admin ---
    function startPresale() external onlyOwner {
        require(!isPresaleActive, "already active");
        isPresaleActive = true;
        emit PresaleStarted();
    }

    function endPresale() external onlyOwner {
        require(isPresaleActive, "already ended");
        isPresaleActive = false;
        emit PresaleEnded();
    }

    function enableClaim() external onlyOwner {
        require(isHub, "hub only");
        require(!isPresaleActive, "presale active");
        isClaimEnabled = true;
        emit ClaimEnabled();
    }

    function setToken(IERC20 token) external onlyOwner {
        rvaToken = token;
    }

    function setFeeds(AggregatorV3Interface _priceFeed, IERC20 _usdt) external onlyOwner {
        priceFeed = _priceFeed;
        usdt = _usdt;
    }

    function setPricePerRVAUSD(uint256 _price) external onlyOwner {
        require(_price > 0, "price=0");
        pricePerRVAUSD = _price;
    }

    function rescueNative(address payable to, uint256 amount) external onlyOwner {
        (bool ok, ) = to.call{value: amount}("");
        require(ok, "native transfer failed");
    }

    function rescueERC20(IERC20 token, address to, uint256 amount) external onlyOwner {
        token.safeTransfer(to, amount);
    }

    // --- Helpers ---
    function _normalizeTo1e18(int256 v, uint8 d) internal pure returns (uint256) {
        require(v > 0, "bad price");
        if (d == 18) return uint256(v);
        if (d < 18)  return uint256(v) * (10 ** (18 - d));
        return uint256(v) / (10 ** (d - 18));
    }

    function _scaleTo1e18(uint256 amount, uint8 decimals_) internal pure returns (uint256) {
        if (decimals_ == 18) return amount;
        if (decimals_ < 18)  return amount * (10 ** (18 - decimals_));
        return amount / (10 ** (decimals_ - 18));
    }

    function getLatestNativeUsd1e18() public view returns (uint256) {
        (, int256 px,,,) = priceFeed.latestRoundData();
        uint8 d = priceFeed.decimals();
        return _normalizeTo1e18(px, d);
    }

    modifier saleIsActive() {
        require(isPresaleActive, "not active");
        _;
    }

    // --- Buy with ETH/BNB ---
    function buyWithNative() external payable nonReentrant saleIsActive {
        require(msg.value > 0, "no value");

        uint256 nativeUsd = getLatestNativeUsd1e18();
        uint256 usd       = (msg.value * nativeUsd) / 1e18;
        uint256 tokens    = (usd * 1e18) / pricePerRVAUSD;

        require(tokens > 0, "too small");

        purchases[msg.sender].push(Purchase({
            tokens: tokens,
            usdPaid: usd,
            paidWithUSDT: false,
            rawAmount: msg.value,
            timestamp: block.timestamp
        }));
        tokensSold += tokens;

        emit TokensPurchased(msg.sender, PayKind.NATIVE, address(0), msg.value, usd, tokens);
    }

    // --- Buy with USDT ---
    function buyWithUSDT(uint256 usdtAmount) external nonReentrant saleIsActive {
        require(usdtAmount > 0, "amt=0");

        uint8 d;
        try IERC20Metadata(address(usdt)).decimals() returns (uint8 dec) {
            d = dec;
        } catch {
            d = 6;
        }

        uint256 usd    = _scaleTo1e18(usdtAmount, d);
        uint256 tokens = (usd * 1e18) / pricePerRVAUSD;

        require(tokens > 0, "too small");

        purchases[msg.sender].push(Purchase({
            tokens: tokens,
            usdPaid: usd,
            paidWithUSDT: true,
            rawAmount: usdtAmount,
            timestamp: block.timestamp
        }));
        tokensSold += tokens;

        emit TokensPurchased(msg.sender, PayKind.USDT, address(usdt), usdtAmount, usd, tokens);
    }

    // --- Claim on ETH HUB ---
    function claimWithSig(
        address beneficiary,
        uint256 amount,
        uint256 deadline,
        bytes32 nonce,
        bytes calldata signature
    ) external nonReentrant {
        require(isHub, "hub only");
        require(isClaimEnabled, "claim disabled");
        require(address(rvaToken) != address(0), "token unset");
        require(block.timestamp <= deadline, "voucher expired");
        require(!usedNonce[nonce], "nonce used");

        // Build struct hash including beneficiary
        bytes32 structHash = keccak256(abi.encode(
            CLAIM_TYPEHASH,
            beneficiary,
            amount,
            deadline,
            nonce
        ));
        bytes32 digest = _hashTypedDataV4(structHash);
        address recovered = ECDSA.recover(digest, signature);
        require(recovered == voucherSigner, "bad signer");

        // Only beneficiary can claim
        require(msg.sender == beneficiary, "not beneficiary");

        usedNonce[nonce] = true;
        rvaToken.safeTransfer(msg.sender, amount);

        emit TokensClaimed(msg.sender, amount);
    }

    // --- Views ---
    function purchasesLength(address payer) external view returns (uint256) {
        return purchases[payer].length;
    }

    receive() external payable {}
}
Editor is loading...
Leave a Comment