Page cover

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"); }
}

Last updated