Contract Name:
SonicSuperRain
Contract Source Code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Interface definitions
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
}
interface IUniswapRouter {
function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external;
// Add ETH swap function
function swapExactTokensForETHSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] calldata path,
address to,
uint256 deadline
) external;
}
/**
* @title RainToken
* @dev A simplified ERC20 token with reflection rewards
*/
contract SonicSuperRain {
// Token setup
string private _name = "SonicSuperRain";
string private _symbol = "SSR";
uint8 private constant _decimals = 18;
uint256 private _totalSupply;
// Reward token and router addresses
address public REWARD_TOKEN;
address constant private ROUTER = 0x95a7e403d7cF20F675fF9273D66e94d35ba49fA3; // ROUTER
address public LP_PAIR;
// Ownership
address private _owner;
// Balances and allowances
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
// Fee configuration
uint256 public taxPercentage = 5; // 5% default tax
uint256 public swapThreshold;
// Reward distribution
uint256 public distributionInterval = 300; // 5 minutes (in seconds)
uint256 public batchSize = 50;
uint256 public lastProcessedTime;
uint256 public currentIndex;
// Holder management
address[] private holders;
mapping(address => uint256) public holderIndexes;
struct HolderInfo {
uint256 amount;
uint256 pendingRewards;
uint256 totalExcluded;
}
mapping(address => HolderInfo) public holderInfo;
// Rewards accounting
uint256 public totalShares;
uint256 public totalDividends;
uint256 public dividendsPerShare;
// Special addresses
mapping(address => bool) public excluded;
mapping(address => bool) public exempt;
// Control flags
bool private inSwap;
bool public tradingEnabled;
// Events
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
event SwapAndDistribute(uint256 tokenAmount, uint256 rewardAmount);
event Excluded(address indexed account, bool status);
event TaxUpdated(uint256 newTaxPercentage);
event IntervalUpdated(uint256 newDistributionInterval);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
event RewardsProcessed(uint256 holderCount);
event TotalSharesRecalculated(uint256 previousValue, uint256 newValue);
// Modifiers
modifier lockTheSwap {
inSwap = true;
_;
inSwap = false;
}
modifier onlyOwner() {
require(msg.sender == _owner, "Caller is not the owner");
_;
}
// Constructor
constructor() {
_owner = msg.sender;
// Initial supply
_totalSupply = 100000000 * 10**_decimals;
_balances[msg.sender] = _totalSupply;
// Set default parameters
swapThreshold = _totalSupply * 5 / 10000; // 0.05%
// Set initial reward token address
REWARD_TOKEN = 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38; // WRAPPED_SONIC
// Add owner to holders array
_addHolder(_owner, _balances[_owner]);
// Setup initial shares - FIXED: Don't set totalShares here, it will be set in _addHolder
// totalShares = _totalSupply;
// Approve router for future swaps
_approve(address(this), ROUTER, _totalSupply);
emit Transfer(address(0), msg.sender, _totalSupply);
}
// External functions
function setRewardToken(address newRewardToken) external onlyOwner {
require(newRewardToken != address(0), "Reward token cannot be zero address");
REWARD_TOKEN = newRewardToken;
}
function setLPPair(address newPair) external onlyOwner {
require(newPair != address(0), "LP pair cannot be zero address");
LP_PAIR = newPair;
}
function setTaxPercentage(uint256 newTaxPercentage) external onlyOwner {
taxPercentage = newTaxPercentage;
emit TaxUpdated(newTaxPercentage);
}
function setDistributionInterval(uint256 newInterval) external onlyOwner {
require(newInterval >= 60, "Interval must be at least 60 seconds");
distributionInterval = newInterval;
emit IntervalUpdated(newInterval);
}
function setBatchSize(uint256 newBatchSize) external onlyOwner {
require(newBatchSize >= 5 && newBatchSize <= 100, "Batch size must be between 5 and 100");
batchSize = newBatchSize;
}
function setSwapThreshold(uint256 newThreshold) external onlyOwner {
require(newThreshold > 0, "Threshold must be greater than 0");
swapThreshold = newThreshold;
}
function setExcluded(address account, bool status) external onlyOwner {
if (excluded[account] != status) {
excluded[account] = status;
if (status) {
// If excluding an address, remove it from holders
if (holderInfo[account].amount > 0) {
_removeHolder(account);
}
} else {
// If including an address, add it to holders
_setShare(account, balanceOf(account));
}
}
emit Excluded(account, status);
}
function setExempt(address account, bool status) external onlyOwner {
exempt[account] = status;
}
function enableTrading() external onlyOwner {
require(!tradingEnabled, "Trading already enabled");
tradingEnabled = true;
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "New owner cannot be zero address");
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
/**
* @dev Manually swap a specified amount of tokens for rewards
* @param amount Amount of tokens to swap (0 for custom calculation based on contract balance)
* @return swapped Amount of tokens that were actually swapped
* @return received Amount of reward tokens received from the swap
*/
function manualSwap(uint256 amount) external onlyOwner returns (uint256 swapped, uint256 received) {
uint256 contractBalance = balanceOf(address(this));
require(contractBalance > 0, "No tokens to swap");
// Determine swap amount based on parameters and contract balance
uint256 swapAmount;
if (amount > 0 && amount <= contractBalance) {
// Use specified amount
swapAmount = amount;
} else if (swapThreshold > 0 && contractBalance >= swapThreshold) {
// Use threshold amount
swapAmount = swapThreshold;
} else {
// Use entire balance
swapAmount = contractBalance;
}
// Execute the swap and track results
uint256 receivedAmount = _swapTokensForRewards(swapAmount);
return (swapAmount, receivedAmount);
}
/**
* @dev Returns the contract's current balance of RAIN tokens available for swapping
*/
function getSwappableBalance() external view returns (uint256) {
return balanceOf(address(this));
}
/**
* @dev Returns the contract's current balance of native SONIC available for distribution
*/
function getRewardBalance() external view returns (uint256) {
return address(this).balance;
}
function manualDistributeRewards(uint256 batchCount) external returns (uint256, uint256) {
require(msg.sender == _owner, "Only owner can distribute rewards");
uint256 batchToProcess = batchCount > 0 ? batchCount : batchSize;
if (currentIndex + batchToProcess > holders.length) {
batchToProcess = holders.length - currentIndex;
}
if (batchToProcess == 0) {
currentIndex = 0;
return (0, 0);
}
uint256 startIndex = currentIndex;
for (uint256 i = 0; i < batchToProcess; i++) {
if (currentIndex < holders.length) {
_distributeRewards(holders[currentIndex]);
currentIndex++;
}
}
uint256 processedCount = currentIndex - startIndex;
if (currentIndex >= holders.length) {
currentIndex = 0;
lastProcessedTime = block.timestamp;
emit RewardsProcessed(holders.length);
}
return (processedCount, currentIndex);
}
/**
* @dev Recalculates totalShares based on actual holder balances to fix accounting issues
* @return previousTotalShares The total shares value before recalculation
* @return newTotalShares The corrected total shares value after recalculation
*/
function recalculateTotalShares() external onlyOwner returns (uint256 previousTotalShares, uint256 newTotalShares) {
// Store previous value for reporting
previousTotalShares = totalShares;
// Reset total shares
totalShares = 0;
// Iterate through all holders and sum their actual balances
for (uint256 i = 0; i < holders.length; i++) {
address holder = holders[i];
uint256 holderBalance = _balances[holder];
// Only count meaningful balances (same threshold as in _setShare)
if (holderBalance >= 1 * 10**_decimals) {
// Update the holder info with their actual balance
holderInfo[holder].amount = holderBalance;
// Add to the total shares
totalShares += holderBalance;
} else if (holderBalance == 0) {
// Handle zero balances - these shouldn't be in the holders array
// We'll address this in the next loop to avoid index shifts during iteration
}
}
// Second pass to clean up holders with zero balances
// We iterate backwards to avoid index shifting problems
for (int256 i = int256(holders.length) - 1; i >= 0; i--) {
address holder = holders[uint256(i)];
if (_balances[holder] < 1 * 10**_decimals) {
// Remove this holder using the existing function
_removeHolder(holder);
}
}
// Return the new total shares value
newTotalShares = totalShares;
// Re-calculate dividendsPerShare based on the new totalShares
if (totalShares > 0 && totalDividends > 0) {
dividendsPerShare = totalDividends * 10000 / totalShares;
}
// Emit an event for transparency
emit TotalSharesRecalculated(previousTotalShares, newTotalShares);
}
// ERC20 Standard Functions
function name() public view returns (string memory) {
return _name;
}
function symbol() public view returns (string memory) {
return _symbol;
}
function decimals() public pure returns (uint8) {
return _decimals;
}
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
function transfer(address recipient, uint256 amount) public returns (bool) {
_transfer(msg.sender, recipient, amount);
return true;
}
function allowance(address owner_, address spender) public view returns (uint256) {
return _allowances[owner_][spender];
}
function approve(address spender, uint256 amount) public returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool) {
uint256 currentAllowance = _allowances[sender][msg.sender];
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
_approve(sender, msg.sender, currentAllowance - amount);
}
_transfer(sender, recipient, amount);
return true;
}
// Information functions
function owner() public view returns (address) {
return _owner;
}
function getHolderCount() public view returns (uint256) {
return holders.length;
}
function isRewardsReady() public view returns (bool) {
return block.timestamp >= lastProcessedTime + distributionInterval;
}
function getTimeUntilRewards() public view returns (uint256) {
if (isRewardsReady()) return 0;
return lastProcessedTime + distributionInterval - block.timestamp;
}
function getUnpaidRewards(address account) public view returns (uint256) {
if (holderInfo[account].amount == 0) return 0;
uint256 earnedRewards = getSharesPercentage(account);
uint256 alreadyPaid = holderInfo[account].totalExcluded;
if (earnedRewards <= alreadyPaid) return 0;
return earnedRewards - alreadyPaid;
}
function getDistributionStatus() public view returns (uint256, uint256, uint256) {
if (holders.length == 0) return (0, 0, 100);
uint256 percentage = currentIndex * 100 / holders.length;
return (holders.length, currentIndex, percentage);
}
// Internal functions
function _transfer(address from, address to, uint256 amount) internal {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
require(amount > 0, "Transfer amount must be greater than zero");
// Check if trading is enabled when interacting with LP
if (!tradingEnabled && (from == LP_PAIR || to == LP_PAIR)) {
require(from == _owner || to == _owner, "Trading not enabled yet");
}
// Auto-liquidity and swap
bool canSwap = balanceOf(address(this)) >= swapThreshold;
if (canSwap && !inSwap && from != LP_PAIR && from != _owner && to != _owner) {
uint256 swapAmount = amount > swapThreshold ? swapThreshold : amount;
_swapTokensForRewards(swapAmount);
}
// Fee system
bool takeFee = !exempt[from] && !exempt[to];
uint256 fees = 0;
if (takeFee) {
if (from == LP_PAIR || to == LP_PAIR) {
fees = amount * taxPercentage / 100;
}
if (fees > 0) {
_takeFee(from, address(this), fees);
amount -= fees;
}
}
// Get sender's previous balance for share calculation
uint256 fromPreviousBalance = _balances[from];
// Get recipient's previous balance for share calculation
uint256 toPreviousBalance = _balances[to];
// Standard transfer
_balances[from] -= amount;
_balances[to] += amount;
emit Transfer(from, to, amount);
// Update holder tracking - FIXED: Use actual balances after transfer
if (!excluded[from] && from != LP_PAIR) {
// Use current balance after the transfer
_setShare(from, _balances[from]);
}
if (!excluded[to] && to != LP_PAIR) {
// Use current balance after the transfer
_setShare(to, _balances[to]);
}
}
function _approve(address owner_, address spender, uint256 amount) internal {
require(owner_ != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner_][spender] = amount;
emit Approval(owner_, spender, amount);
}
function _takeFee(address from, address to, uint256 amount) internal {
_balances[to] += amount;
emit Transfer(from, to, amount);
}
// UPDATED: Swap function that correctly uses swapExactTokensForETHSupportingFeeOnTransferTokens
function _swapTokensForRewards(uint256 amount) internal lockTheSwap returns (uint256) {
// Setup swap path
address[] memory path = new address[](2);
path[0] = address(this); // SUPERRAIN token address
path[1] = REWARD_TOKEN; // WRAPPED_SONIC address
// Approve router
_approve(address(this), ROUTER, amount);
// Get initial balance of native SONIC
uint256 initialBalance = address(this).balance;
// Swap tokens for native SONIC
IUniswapRouter(ROUTER).swapExactTokensForETHSupportingFeeOnTransferTokens(
amount,
0,
path,
address(this),
block.timestamp + 1200 // 20 minute deadline
);
// Calculate received native SONIC
uint256 newBalance = address(this).balance;
uint256 receivedSonic = newBalance - initialBalance;
if (receivedSonic > 0 && totalShares > 0) {
// Update dividends per share
totalDividends += receivedSonic;
dividendsPerShare = totalDividends * 10000 / totalShares;
emit SwapAndDistribute(amount, receivedSonic);
}
return receivedSonic;
}
function processRewards() public {
uint256 gas = 300000;
uint256 iterations = 0;
while (gas > 0 && iterations < batchSize && currentIndex < holders.length) {
uint256 gasUsed = gasleft();
_distributeRewards(holders[currentIndex]);
gas -= (gasUsed - gasleft());
currentIndex++;
iterations++;
}
if (currentIndex >= holders.length) {
currentIndex = 0;
}
}
// UPDATED: Distribution function that now sends native SONIC instead of tokens
function _distributeRewards(address holder) internal {
if (holderInfo[holder].amount == 0) return;
uint256 amount = getUnpaidEarnings(holder);
if (amount > 0) {
uint256 contractBalance = address(this).balance;
if (amount <= contractBalance) {
holderInfo[holder].totalExcluded = getSharesPercentage(holder);
holderInfo[holder].pendingRewards = 0;
// Send native SONIC instead of tokens
(bool success, ) = holder.call{value: amount}("");
require(success, "SONIC transfer failed");
}
}
}
function getSharesPercentage(address account) internal view returns (uint256) {
if (totalShares == 0) return 0;
return dividendsPerShare * holderInfo[account].amount / 10000;
}
function getUnpaidEarnings(address account) internal view returns (uint256) {
if (holderInfo[account].amount == 0) return 0;
uint256 earnedDividends = getSharesPercentage(account);
uint256 alreadyPaid = holderInfo[account].totalExcluded;
if (earnedDividends <= alreadyPaid) return 0;
return earnedDividends - alreadyPaid;
}
// FIXED: Completely revised _setShare to correctly handle share accounting
function _setShare(address holder, uint256 amount) internal {
// First, determine if this is a meaningful amount (at least 1 token)
bool isMeaningful = amount >= 1 * 10**_decimals;
// Case 1: Holder already has shares
if (holderInfo[holder].amount > 0) {
uint256 oldAmount = holderInfo[holder].amount;
// Case 1A: Reducing to zero or below minimum
if (!isMeaningful) {
// Remove holder from tracking and adjust totalShares
totalShares -= oldAmount;
_removeHolder(holder);
}
// Case 1B: Balance changed but still meaningful
else if (oldAmount != amount) {
// Adjust totalShares by the difference
totalShares = totalShares - oldAmount + amount;
// Update holder's record
holderInfo[holder].amount = amount;
// Calculate new pending rewards without changing the total excluded
// (which is handled during distribution)
}
// If oldAmount == amount, nothing changes
}
// Case 2: New holder or holder with zero shares
else if (isMeaningful) {
// Add new holder to tracking
_addHolder(holder, amount);
// Update holder's record
holderInfo[holder].amount = amount;
// Add to totalShares
totalShares += amount;
}
// If not meaningful and holder doesn't exist, do nothing
}
function _addHolder(address holder, uint256 amount) internal {
// Verify holder isn't already in the array
if (holderIndexes[holder] > 0 || (holders.length > 0 && holders[0] == holder)) {
return;
}
// Add holder to tracking
holders.push(holder);
holderIndexes[holder] = holders.length - 1;
// Initialize holder info
holderInfo[holder].totalExcluded = getSharesPercentage(holder);
}
function _removeHolder(address holder) internal {
// Make sure holder exists
if (holders.length == 0 ||
(holderIndexes[holder] == 0 && holders[0] != holder) ||
holderIndexes[holder] >= holders.length) {
return;
}
address lastHolder = holders[holders.length - 1];
uint256 holderIndex = holderIndexes[holder];
// Replace the removed holder with the last one
holders[holderIndex] = lastHolder;
holderIndexes[lastHolder] = holderIndex;
// Remove the last element
holders.pop();
delete holderIndexes[holder];
// Clear holder info (totalShares is already adjusted in _setShare)
delete holderInfo[holder];
}
// Rescue functions
function rescueToken(address token, uint256 amount) external onlyOwner {
IERC20(token).transfer(_owner, amount);
}
function withdrawETH() external onlyOwner {
(bool success,) = _owner.call{value: address(this).balance}("");
require(success, "ETH withdrawal failed");
}
// Receive function to accept ETH (native SONIC)
receive() external payable {}
}