Documentado el Smart Contract
Para la transparencia y la automomia de BNBFund, Presenta el Smart Contract Documentado que hace cada Funcion dentro del codigo.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Interfaz estándar de ERC20: funciones mínimas necesarias para interactuar con tokens ERC20
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool); // Transfiere tokens a otra dirección
function transferFrom(address from, address to, uint256 amount) external returns (bool); // Transfiere tokens de un usuario a otro, usando allowance
function balanceOf(address account) external view returns (uint256); // Consulta el balance de tokens en una dirección
}
// Interfaz para compatibilidad con Chainlink Automation (Keepers), permite comprobar y ejecutar acciones automatizadas
interface AutomationCompatibleInterface {
function checkUpkeep(bytes calldata checkData) external view returns (bool upkeepNeeded, bytes memory performData); // Verifica si se necesita ejecutar mantenimiento
function performUpkeep(bytes calldata performData) external; // Realiza el mantenimiento automatizado
}
// Protección contra ataques de reentrancy (llamadas recursivas maliciosas)
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1; // Estado: no ejecutando función
uint256 private constant _ENTERED = 2; // Estado: ejecutando función
uint256 private _status;
constructor() { _status = _NOT_ENTERED; } // Inicializa el estado
// Modificador para proteger funciones contra reentrancia
modifier nonReentrant() {
require(_status != _ENTERED, "Reentrant call"); // Asegura que no hay una llamada reentrante
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
// Contrato principal: administra un fondo con depósitos, referrers y distribución periódica de recompensas en BUSD-T
contract BNBFund is AutomationCompatibleInterface, ReentrancyGuard {
address public owner; // Propietario del contrato
address public automationCaller; // Dirección autorizada para operaciones automatizadas
IERC20 public immutable busdt; // Instancia del token BUSD-T; dirección definida al desplegar
// Constantes operativas
uint256 public constant MIN_DEPOSIT = 50 * 10**18; // Depósito mínimo requerido (en wei)
uint256 public constant DISTRIBUTION_INTERVAL = 24 hours; // Intervalo entre distribuciones
uint256 public constant DISTRIBUTION_PERCENTAGE = 15; // % del balance a repartir por ciclo
uint256 public constant MAX_DISTRIBUTIONS = 30; // Número máximo de ciclos por usuario
uint256 public constant CLAIM_DEADLINE = 2 hours; // Tiempo límite para reclamar recompensa de cada ciclo
uint256 public constant REFERRAL_BONUS_PERCENT = 10; // % del bonus para referrer
// Estructura de datos por ciclo de usuario
struct UserCycleData {
bool optedIn; // El usuario se registró para el ciclo
bool claimed; // Reclamo realizado
bool referralBonusClaimed; // Bonus de referido reclamado
}
// Datos de cada participante
struct Participant {
uint256 depositedAmount; // Total depositado
uint256 receivedAmount; // Total recibido en recompensas
uint256 lastClaimTimestamp; // Último momento en que reclamó recompensa
uint256 lastDepositTime; // Última vez que depositó
uint256 claimsMade; // Reclamos realizados
bool active; // Usuario activo o no
uint256 referrerId; // ID del referrer
uint256 cyclesOptedIn; // Cantidad de ciclos a los que se unió
}
// Mappings de gestión de participantes y ciclos
mapping(address => Participant) public participants; // Datos de cada usuario
mapping(address => mapping(uint256 => UserCycleData)) public userCycles; // Ciclos por usuario
mapping(address => uint256) public walletToId; // Mapea wallet a ID (para referidos)
mapping(uint256 => address) public idToWallet; // Mapea ID a wallet
mapping(address => uint256) public totalReferralBonusReceived; // Bonus de referidos por usuario
mapping(uint256 => uint256) public participantsByCycle; // Cantidad de participantes por ciclo
mapping(address => address[]) public directReferrals; // Referidos directos
// Variables de estado global
uint256 public nextId = 2; // Siguiente ID libre para usuario
uint256 public totalParticipants; // Total de participantes únicos
uint256 public totalDeposited; // Total depositado en el contrato
uint256 public totalDistributed; // Total distribuido
uint256 public lastDistributionTime; // Última vez que se realizó una distribución
uint256 public currentCycle = 1; // Ciclo actual
uint256 public activeOptIns; // Participantes activos en el ciclo actual
uint256 public lastCycleRewardPerUser; // Recompensa por usuario en el último ciclo
uint256 public lastCycleDeadline; // Deadline límite del ciclo actual para reclamos
// Declaración de eventos para seguimiento fuera de cadena
event Deposit(address indexed participant, uint256 amount, uint256 indexed referrerId); // Nuevo depósito
event OptedIn(address indexed participant, uint256 cycle); // Usuario se integra a un ciclo
event RewardClaimed(address indexed participant, uint256 amount, uint256 claimsMade, uint256 cycle); // Reclamo de recompensa
event ReferralBonusPaid(address indexed referrer, address indexed referred, uint256 bonus, uint256 cycle, uint256 cyclesOptedIn); // Bonus de referido pagado
event DistributionStarted(uint256 rewardPerUser, uint256 deadline, uint256 cycle); // Inicio de distribución
event AutomationCallerSet(address indexed newCaller); // Set nuevo caller automatizado
event CycleAdvanced(uint256 newCycle); // Avanza el ciclo global
event CycleClosed(uint256 oldCycle); // Cierre de ciclo
event ForcedDeactivation(address indexed participant); // Desactivación forzada de user
// Restricción: solo propietario o automationCaller puede ejecutar
modifier onlyOwnerOrAutomation() {
require(msg.sender == owner || msg.sender == automationCaller, "Not authorized");
_;
}
// Restricción: solo propietario
modifier onlyOwner() { require(msg.sender == owner, "Only owner"); _; }
// Restricción: solo EOA (no contratos)
modifier onlyEOA() { require(msg.sender == tx.origin, "No contracts allowed"); _; }
// Constructor: inicializa el contrato, setea owner y primer participante
constructor(address busdtAddress) {
owner = msg.sender;
walletToId[owner] = 1;
idToWallet[1] = owner;
nextId = 2;
totalParticipants = 1;
lastDistributionTime = block.timestamp;
busdt = IERC20(busdtAddress);
}
// Transferencia segura de BUSD-T evitando errores de contratos que devuelvan datos inesperados
function _safeTransfer(address to, uint256 value) internal {
(bool success, bytes memory data) = address(busdt).call(
abi.encodeWithSelector(busdt.transfer.selector, to, value)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "BUSD-T transfer failed");
}
// Transferencia segura de BUSD-T desde un usuario usando allowance
function _safeTransferFrom(address from, address to, uint256 value) internal {
(bool success, bytes memory data) = address(busdt).call(
abi.encodeWithSelector(busdt.transferFrom.selector, from, to, value)
);
require(success && (data.length == 0 || abi.decode(data, (bool))), "BUSD-T transferFrom failed");
}
// Previene loops en los referidos (referirse a sí mismo o cadenas circulares)
function _hasReferralLoop(address user, uint256 referrerId) internal view returns (bool) {
address refAddr = idToWallet[referrerId];
if (refAddr == address(0) || refAddr == user) return true;
if (participants[refAddr].referrerId != 0) {
address ref2 = idToWallet[participants[refAddr].referrerId];
if (ref2 == user) return true;
}
return false;
}
// Permite al owner definir o cambiar el address que ejecuta acciones automatizadas
function setAutomationCaller(address _automationCaller) external onlyOwner {
require(_automationCaller != address(0), "Invalid address");
automationCaller = _automationCaller;
emit AutomationCallerSet(_automationCaller);
}
// Depósito inicial, define referidor y activa el usuario
function deposit(uint256 referrerId, uint256 amount) external onlyEOA nonReentrant {
Participant storage p = participants[msg.sender];
require(!p.active, "Finish cycles first"); // Solo permitido si el usuario no está activo
require(amount >= MIN_DEPOSIT, "Insufficient deposit");
_safeTransferFrom(msg.sender, address(this), amount);
p.cyclesOptedIn = 0;
p.claimsMade = 0;
p.active = true;
// Si es primer depósito del usuario: asigna ID y referidor
bool firstDeposit = (walletToId[msg.sender] == 0);
if (firstDeposit) {
if (
referrerId == 0 ||
idToWallet[referrerId] == address(0) ||
idToWallet[referrerId] == msg.sender ||
_hasReferralLoop(msg.sender, referrerId)
) {
referrerId = 1; // Owner por defecto si referrer inválido
}
totalParticipants++;
walletToId[msg.sender] = nextId;
idToWallet[nextId] = msg.sender;
nextId++;
p.referrerId = referrerId;
address referrerAddr = idToWallet[referrerId];
if (referrerAddr != address(0) && referrerAddr != msg.sender) {
directReferrals[referrerAddr].push(msg.sender); // Registra referido directo
}
}
// Actualiza estado
p.depositedAmount += amount;
p.lastDepositTime = block.timestamp;
totalDeposited += amount;
emit Deposit(msg.sender, amount, p.referrerId);
}
// El usuario participa en el ciclo actual, activa para siguiente distribución
function optIn() external onlyEOA {
Participant storage p = participants[msg.sender];
require(p.active, "Not registered");
require(!userCycles[msg.sender][currentCycle].optedIn, "Already opted-in");
require(p.cyclesOptedIn < MAX_DISTRIBUTIONS, "Max opt-in cycles reached");
require(lastCycleRewardPerUser == 0, "Active distribution");
userCycles[msg.sender][currentCycle].optedIn = true;
userCycles[msg.sender][currentCycle].claimed = false;
userCycles[msg.sender][currentCycle].referralBonusClaimed = false;
p.cyclesOptedIn++;
participantsByCycle[currentCycle]++;
activeOptIns++;
emit OptedIn(msg.sender, currentCycle);
}
// Inicia una nueva distribución periódica, solo ejecutable por owner o automatización autorizada
function startDistribution() public onlyOwnerOrAutomation {
require(block.timestamp >= lastDistributionTime + DISTRIBUTION_INTERVAL, "Not time yet");
require(participantsByCycle[currentCycle] > 0, "No active participants");
uint256 contractBalance = busdt.balanceOf(address(this));
require(contractBalance > 0, "No BUSD-T funds");
uint256 cycleReward = (contractBalance * DISTRIBUTION_PERCENTAGE) / 100;
uint256 rewardPerUser = cycleReward / participantsByCycle[currentCycle];
require(rewardPerUser >= 1, "Reward too small");
lastDistributionTime = block.timestamp;
lastCycleRewardPerUser = rewardPerUser;
lastCycleDeadline = block.timestamp + CLAIM_DEADLINE;
emit DistributionStarted(rewardPerUser, lastCycleDeadline, currentCycle);
}
/// Claim de recompensas: permite al usuario reclamar su parte en la distribución y paga bonus de referido cuando corresponde
function claimDistribution() external onlyEOA nonReentrant {
Participant storage p = participants[msg.sender];
require(p.active, "Not registered");
require(userCycles[msg.sender][currentCycle].optedIn, "Did not opt-in");
require(!userCycles[msg.sender][currentCycle].claimed, "Already claimed");
require(lastCycleRewardPerUser > 0, "No distribution running");
require(block.timestamp <= lastCycleDeadline, "Claim expired");
require(p.cyclesOptedIn <= MAX_DISTRIBUTIONS, "Max claims");
uint256 reward = lastCycleRewardPerUser;
uint256 referralBonus = 0;
// Bonus de referido en ciclos específicos
bool cicloBonus = (
p.cyclesOptedIn == 1 || p.cyclesOptedIn == 3 || p.cyclesOptedIn == 6 ||
p.cyclesOptedIn == 9 || p.cyclesOptedIn == 12 || p.cyclesOptedIn == 15 ||
p.cyclesOptedIn == 18 || p.cyclesOptedIn == 21 || p.cyclesOptedIn == 24 ||
p.cyclesOptedIn == 27 || p.cyclesOptedIn == 30
);
if (
cicloBonus &&
!userCycles[msg.sender][currentCycle].referralBonusClaimed &&
p.referrerId >= 1
) {
address referrer = idToWallet[p.referrerId];
uint256 discount = (reward * REFERRAL_BONUS_PERCENT) / 100;
if (referrer != address(0) && participants[referrer].active) {
// Referrer activo: se paga el bonus
referralBonus = discount;
reward = reward - referralBonus;
userCycles[msg.sender][currentCycle].referralBonusClaimed = true;
totalReferralBonusReceived[referrer] += referralBonus;
_safeTransfer(referrer, referralBonus);
emit ReferralBonusPaid(referrer, msg.sender, referralBonus, currentCycle, p.cyclesOptedIn);
} else {
// Referrer inactivo: se descuenta igual pero el fondo lo retiene el contrato
reward = reward - discount;
// Sin evento ni transferencia del bonus
}
}
userCycles[msg.sender][currentCycle].claimed = true;
p.receivedAmount += (reward + referralBonus);
p.claimsMade++;
p.lastClaimTimestamp = block.timestamp;
totalDistributed += reward;
if (p.cyclesOptedIn == MAX_DISTRIBUTIONS) {
p.active = false;
}
_safeTransfer(msg.sender, reward);
emit RewardClaimed(msg.sender, reward, p.claimsMade, currentCycle);
}
// Acción automatizada: avanza ciclos y/o dispara distribución según corresponda
function performUpkeep(bytes calldata) external override onlyOwnerOrAutomation {
bool needsAdvance = (block.timestamp > lastCycleDeadline && lastCycleDeadline != 0);
bool needsPayOut = (
!needsAdvance &&
block.timestamp >= lastDistributionTime + DISTRIBUTION_INTERVAL &&
participantsByCycle[currentCycle] > 0 &&
busdt.balanceOf(address(this)) > 0
);
if (needsAdvance) {
emit CycleClosed(currentCycle);
currentCycle++;
lastCycleRewardPerUser = 0;
lastCycleDeadline = 0;
activeOptIns = 0;
emit CycleAdvanced(currentCycle);
}
if (needsPayOut) {
startDistribution();
}
}
// Check automatizado: usado por Chainlink para consultar si hace falta mantenimiento o payout
function checkUpkeep(bytes calldata) external view override returns (bool upkeepNeeded, bytes memory) {
bool needsAdvance = (block.timestamp > lastCycleDeadline && lastCycleDeadline != 0);
bool needsPayOut = (
!needsAdvance &&
block.timestamp >= lastDistributionTime + DISTRIBUTION_INTERVAL &&
participantsByCycle[currentCycle] > 0 &&
busdt.balanceOf(address(this)) > 0
);
upkeepNeeded = needsAdvance || needsPayOut;
return (upkeepNeeded, "");
}
// Obtiene datos completos de un participante
function getParticipant(address addr) external view returns (
uint256 depositedAmount,
uint256 receivedAmount,
uint256 lastClaimTimestamp,
uint256 lastDepositTime,
uint256 claimsMade,
bool active,
uint256 referrerId,
uint256 cyclesOptedIn
) {
Participant storage p = participants[addr];
return (
p.depositedAmount, p.receivedAmount, p.lastClaimTimestamp, p.lastDepositTime,
p.claimsMade, p.active, p.referrerId, p.cyclesOptedIn
);
}
// Cálculo del total realmente recibido descontando posibles bonus de referido (útil para estadísticas)
function getRealReceivedAmount(address addr) public view returns (uint256 realReceived) {
Participant storage p = participants[addr];
uint256[11] memory bonusCycles = [uint256(1),3,6,9,12,15,18,21,24,27,30];
uint256 totalReferralBonusDiscount = 0;
uint256 rewardPerBonusCycle = lastCycleRewardPerUser;
for (uint256 i = 0; i < 11; ++i) {
uint256 cycle = bonusCycles[i];
if (p.cyclesOptedIn >= cycle) {
totalReferralBonusDiscount += (rewardPerBonusCycle * REFERRAL_BONUS_PERCENT) / 100;
}
}
if (p.receivedAmount > totalReferralBonusDiscount) {
return p.receivedAmount - totalReferralBonusDiscount;
} else {
return 0;
}
}
// Obtiene la lista de referidos directos y su estado
function getReferrals(address addr) external view returns (
address[] memory referrals,
bool[] memory activeStatus,
bool[] memory eligibleForBonus
) {
uint256 num = directReferrals[addr].length;
referrals = new address[](num);
activeStatus = new bool[](num);
eligibleForBonus = new bool[](num);
for (uint256 i = 0; i < num; i++) {
address ref = directReferrals[addr][i];
Participant storage p = participants[ref];
referrals[i] = ref;
activeStatus[i] = p.active;
eligibleForBonus[i] =
p.active &&
(p.cyclesOptedIn == 1 || p.cyclesOptedIn == 3 || p.cyclesOptedIn == 6 ||
p.cyclesOptedIn == 9 || p.cyclesOptedIn == 12 || p.cyclesOptedIn == 15 ||
p.cyclesOptedIn == 18 || p.cyclesOptedIn == 21 || p.cyclesOptedIn == 24 ||
p.cyclesOptedIn == 27 || p.cyclesOptedIn == 30);
}
return (referrals, activeStatus, eligibleForBonus);
}
// Funciones auxiliares de consulta rápidas
function getParticipantId(address addr) external view returns (uint256) { return walletToId[addr]; }
function getAddressById(uint256 id) external view returns (address) { return idToWallet[id]; }
function getTotalParticipants() external view returns (uint256) { return totalParticipants; }
function getTotalDeposited() external view returns (uint256) { return totalDeposited; }
function getTotalDistributed() external view returns (uint256) { return totalDistributed; }
function getContractBalance() external view returns (uint256) { return busdt.balanceOf(address(this)); }
function getCurrentCycle() external view returns (uint256) { return currentCycle; }
/// Permite a usuarios desbloquear su estado si no reclamaron, completando todos los ciclos pero con el último pendiente
function forceDeactivate(address user) public {
require(msg.sender == user, "Solo puedes destrabarte a ti mismo");
Participant storage p = participants[user];
if (!p.active) return;
if (
p.cyclesOptedIn >= MAX_DISTRIBUTIONS &&
userCycles[user][currentCycle - 1].optedIn &&
!userCycles[user][currentCycle - 1].claimed
) {
p.active = false;
emit ForcedDeactivation(user);
}
}
// Fallbacks: el contrato no acepta BNB directamente
receive() external payable { revert("Use BUSD-T deposit"); }
fallback() external payable { revert("Invalid call"); }
}PreviousMitos y Verdades de BNBFund.ioNextChecklist de Seguridad y Buenas Prácticas de Auditoría — BNBFund
Last updated


