Untitled
unknown
plain_text
a year ago
8.4 kB
16
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