1

I have recently deployed a contract on the BSC mainnet that got drained of all the funds in it, but I can't seem to find where in my contract the loophole is to know how it got drained so it doesn't happen again. Please I need help.

Here is what my contract looks like:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

contract BUSDMinerProtocol is Ownable, ReentrancyGuard {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    address public immutable BUSD;
    address public adminAddress = // some address here;
    mapping(address => UserInfo) public userInfo;

    uint256 public contractInitializedAt;
    bool public emergencyWidthdrawal = false;

    uint256 public dailyReturnsInBPS = 300;
    uint256 public totalInvestments;
    uint256 public totalParticipants;
    uint256 public totalPayouts;
    uint256 public adminFee = 5000000000000000000; // 5 dollar
    uint256 public withdrawalFeeInBPS = 250;
    uint256 public minCompoundingAmount = 10000000000000000000;
    uint256 public minInvestment = 50000000000000000000;
    uint256 public stakingDuration = 30 days;
    uint256 maxGenerations = 1;

    mapping(address => ReferrerInfo) public referrers;
    mapping(address => UserReferralInfo[]) public userReferrals;
    mapping(address => uint256) public referralsCount;
    mapping(address => uint256) public totalReferralCommissions;
    uint256 public referralCommisionInBPS = 1000;
    LeadershipInfo[] public leadershipPositionsReward;

    struct LeadershipInfo {
        uint256 sales;
        uint256 reward;
    }

    struct ReferrerInfo {
        address referrer;
        bool initialReward;
        uint256 totalEarnings;
    }

    struct UserReferralInfo {
        address user;
        int256 debt;
    }

    struct UserInfo {
        uint256 currentLeadershipPosition; // leadership position 1 - 7
        uint256 totalInvestments;
        uint256 lastWithdrawn;
        uint256 amount;
        uint256 debt;
        uint256 referralDebt;
        uint256 initialTime;
        uint256 totalWithdrawal;
        uint256 withdrawnAt;
        uint256 reinvestmentDeadline;
        uint256 lockEndTime;
        uint256 leadershipScore;
    }

    event ReferralRecorded(address indexed user, address indexed referrer);
    event ReferralCommissionPaid(
        address indexed user,
        address indexed referrer,
        uint256 commissionAmount
    );
    event ReferralCommissionRecorded(
        address indexed referrer,
        uint256 commission
    );

    constructor(address _busd) {
        BUSD = _busd;
        contractInitializedAt = block.timestamp;
        leadershipPositionsReward.push(
            LeadershipInfo(20000000000000000000000, 200000000000000000000)
        );
        leadershipPositionsReward.push(
            LeadershipInfo(50000000000000000000000, 1000000000000000000000)
        );
        leadershipPositionsReward.push(
            LeadershipInfo(120000000000000000000000, 2500000000000000000000)
        );
        leadershipPositionsReward.push(
            LeadershipInfo(250000000000000000000000, 5000000000000000000000)
        );
        leadershipPositionsReward.push(
            LeadershipInfo(500000000000000000000000, 10000000000000000000000)
        );
        leadershipPositionsReward.push(
            LeadershipInfo(750000000000000000000000, 15000000000000000000000)
        );
        leadershipPositionsReward.push(
            LeadershipInfo(1000000000000000000000000, 20000000000000000000000)
        );
    }

    function clearPreviousStaking(address _account) internal {
        UserInfo memory user = userInfo[_account];
        uint256 _debtAmount = user.debt;
        user.withdrawnAt = 0;
        user.lastWithdrawn = 0;
        user.initialTime = block.timestamp;
        user.lockEndTime = user.initialTime + stakingDuration;
        user.debt = 0;
        user.referralDebt = 0;
        userInfo[_account] = user;

        if (_debtAmount > 0) {
            totalPayouts = totalPayouts.add(_debtAmount);
            IERC20(BUSD).transfer(_account, _debtAmount);
        }
    }

    function getUserDetails(address _account)
        external
        view
        returns (UserInfo memory, uint256)
    {
        uint256 reward = getRewards(_account);
        UserInfo memory user = userInfo[_account];
        return (user, reward);
    }

    function getUserReferrals(address _user)
        public
        view
        returns (UserReferralInfo[] memory)
    {
        return userReferrals[_user];
    }

    function getRewards(address _account) public view returns (uint256) {
        uint256 pendingReward = 0;
        UserInfo memory user = userInfo[_account];
        if (user.lastWithdrawn > 0) {
            if (user.reinvestmentDeadline < block.timestamp) {
                return 0;
            } else {
                return user.debt;
            }
        }
        if (user.amount > 0) {
            uint256 stakeAmount = user.amount;
            uint256 timeDiff;
            unchecked {
                timeDiff = block.timestamp - user.initialTime;
            }
            if (timeDiff >= stakingDuration) {
                uint256 stakingDurationInNum = 30;
                return
                    stakeAmount.mul(dailyReturnsInBPS).div(10000).mul(
                        stakingDurationInNum
                    );
            }
            uint256 returnsIn30days = dailyReturnsInBPS * 30;
            uint256 rewardAmount = (((stakeAmount * returnsIn30days) / 10000) *
                timeDiff) / stakingDuration;
            pendingReward = rewardAmount;
        }

        uint256 pending = user.debt.add(pendingReward);
        return pending;
    }

    function setDailyReturns(uint256 _dailyReturnsInBPS) public {
        dailyReturnsInBPS = _dailyReturnsInBPS;
    }

    function getReferralRewards(address _account)
        public
        view
        returns (uint256)
    {
        int256 pendingReward = 0;
        for (uint256 i = 0; i < userReferrals[_account].length; i++) {
            pendingReward = pendingReward + userReferrals[_account][i].debt;
            uint256 userRewards = getRewards(userReferrals[_account][i].user);
            uint256 rewardsPercentage = 15;
            pendingReward =
                pendingReward +
                (int256(userRewards.mul(rewardsPercentage).div(100)));
        }

        return uint256(pendingReward);
    }

    function addReferralDebt(address _account) internal {
        ReferrerInfo memory _referrer = getReferrer(_account);
        if (_referrer.referrer != address(0)) {
            uint256 userReward = getRewards(_account);
            UserReferralInfo memory referredUser;
            uint256 index;

            for (
                uint256 i = 0;
                i < userReferrals[_referrer.referrer].length;
                i++
            ) {
                if (userReferrals[_referrer.referrer][i].user == _account) {
                    index = i;
                    referredUser = userReferrals[_referrer.referrer][i];
                    break;
                }
            }

            if (referredUser.user != address(0)) {
                uint256 rewardsPercentage = 15;
                referredUser.debt =
                    referredUser.debt +
                    int256(userReward.mul(rewardsPercentage).div(100));
                userReferrals[_referrer.referrer][index] = referredUser;
            }
        }
    }

    function invest(uint256 _amount) external nonReentrant {
        require(adminFee < _amount, "Incorrect request!");

        UserInfo memory user = userInfo[msg.sender];
        uint256 investment = _amount - adminFee;

        if (user.totalInvestments > 0) {
            if (user.lastWithdrawn > 0) {
                if (user.reinvestmentDeadline < block.timestamp) {
                    user.debt = 0;
                } else {
                    uint256 reinvestmentPercent = 50;
                    uint256 _minimumInvestment = user
                        .lastWithdrawn
                        .mul(reinvestmentPercent)
                        .div(100);
                    require(
                        investment >= _minimumInvestment,
                        "Invest at least 50% of your previous earning"
                    );
                }
                addReferralDebt(msg.sender);
                clearPreviousStaking(msg.sender);
            } else {
                if (user.debt > 0 || user.amount > 0) {
                    require(
                        investment >= minCompoundingAmount,
                        "Minimum compounding is 10 busd"
                    );
                } else {
                    require(
                        investment >= minInvestment,
                        "Minimum investment is 50 busd"
                    );
                }
            }
        } else {
            require(
                investment >= minInvestment,
                "Minimum investment is 50 busd"
            );
        }

        IERC20(BUSD).transferFrom(msg.sender, address(this), _amount);
        IERC20(BUSD).transfer(adminAddress, adminFee);

        if (user.totalInvestments < 1) {
            totalParticipants = totalParticipants.add(1);
            user.initialTime = block.timestamp;
            user.lockEndTime = user.initialTime + stakingDuration;
        }

        user.totalInvestments = user.totalInvestments.add(investment);
        user.amount = user.amount.add(investment);
        totalInvestments = totalInvestments.add(investment);

        userInfo[msg.sender] = user;

        payReferrerCommission(msg.sender, investment);
    }

    function clearReferralDebt(address _account) internal {
        for (uint256 i = 0; i < userReferrals[_account].length; i++) {
            UserReferralInfo memory usr = userReferrals[_account][i];
            uint256 userRewards = getRewards(usr.user);
            uint256 rewardsPercentage = 15;
            usr.debt = 0 - int256(userRewards.mul(rewardsPercentage).div(100));
            userReferrals[_account][i] = usr;
        }
    }

    function withdraw() external nonReentrant {
        if (emergencyWidthdrawal) {
            UserInfo memory user = userInfo[msg.sender];
            uint256 _withdrawalAmount = user.amount;
            user.amount = 0;
            user.debt = 0;
            user.referralDebt = 0;
            user.lastWithdrawn = 0;
            user.lastWithdrawn = _withdrawalAmount;
            user.totalWithdrawal = user.totalWithdrawal.add(_withdrawalAmount);
            user.withdrawnAt = block.timestamp;

            userInfo[msg.sender] = user;

            IERC20(BUSD).transfer(msg.sender, _withdrawalAmount);
        } else {
            UserInfo memory user = userInfo[msg.sender];
            uint256 totalBalance = getRewards(msg.sender) +
                getReferralRewards(msg.sender) +
                user.amount + user.referralDebt;

            require(totalBalance > 0, "withdraw: insufficient amount");
            uint256 _withdrawalAmount = totalBalance;

            if (user.lockEndTime > block.timestamp) {
                user.amount = 0;
                user.debt = 0;
                user.referralDebt = 0;
                _withdrawalAmount = _withdrawalAmount.div(2);
                totalPayouts = totalPayouts.add(_withdrawalAmount);
                user.lastWithdrawn = 0;
            } else {
                _withdrawalAmount = _withdrawalAmount.mul(70).div(100);
                totalPayouts = totalPayouts.add(_withdrawalAmount);
                user.debt = totalBalance.sub(_withdrawalAmount);
                user.referralDebt = 0;
                user.amount = 0;
                user.lastWithdrawn = _withdrawalAmount;
                user.reinvestmentDeadline = block.timestamp + 1 days;
            }

            user.totalWithdrawal = user.totalWithdrawal.add(_withdrawalAmount);
            user.withdrawnAt = block.timestamp;

            userInfo[msg.sender] = user;
            addReferralDebt(msg.sender);
            clearReferralDebt(msg.sender);

            IERC20(BUSD).transfer(
                msg.sender,
                _withdrawalAmount.sub(
                    _withdrawalAmount.mul(withdrawalFeeInBPS).div(10000)
                )
            );
        }
    }

    function harvest() external nonReentrant {
        UserInfo memory user = userInfo[msg.sender];
        uint256 refReward = getReferralRewards(msg.sender);
        uint256 rewardAmount = getRewards(msg.sender) + refReward + user.referralDebt;
        require(rewardAmount >= 0, "harvest: not enough funds");

        if (refReward > 0) {
            clearReferralDebt(msg.sender);
        }
        addReferralDebt(msg.sender);

        user.debt = 0;
        user.referralDebt = 0;
        user.initialTime = block.timestamp;
        user.lockEndTime = user.initialTime + stakingDuration;
        user.totalWithdrawal = user.totalWithdrawal.add(rewardAmount);
        user.withdrawnAt = block.timestamp;
        userInfo[msg.sender] = user;

        totalPayouts = totalPayouts.add(rewardAmount);

        IERC20(BUSD).transfer(
            msg.sender,
            rewardAmount.sub(rewardAmount.mul(withdrawalFeeInBPS).div(10000))
        );
    }

    function recordReferral(address _user, address _referrer) public {
        if (
            _user != address(0) &&
            _referrer != address(0) &&
            _user != _referrer &&
            referrers[_user].referrer == address(0)
        ) {
            referrers[_user].referrer = _referrer;
            referralsCount[_referrer] += 1;
            userReferrals[_referrer].push(UserReferralInfo(_user, 0));
            emit ReferralRecorded(_user, _referrer);
        }
    }

    function getReferrer(address _user)
        public
        view
        returns (ReferrerInfo memory)
    {
        return referrers[_user];
    }

    function calcReferralReward(uint256 _amount)
        private
        view
        returns (uint256)
    {
        return _amount.mul(referralCommisionInBPS).div(10000);
    }

    function payReferrerCommission(address _user, uint256 _transactionAmount)
        internal
    {
        ReferrerInfo memory referrerInfo = getReferrer(_user);
        if (referrerInfo.referrer != address(0)) {
            UserInfo memory referrerUserInfo = userInfo[referrerInfo.referrer];
            referrerUserInfo.leadershipScore = referrerUserInfo
                .leadershipScore
                .add(_transactionAmount);
            uint256 currentPosition = referrerUserInfo
                .currentLeadershipPosition;
            uint256 points = 0;
            for (
                uint256 i = currentPosition;
                i < leadershipPositionsReward.length;
                i++
            ) {
                LeadershipInfo memory pos = leadershipPositionsReward[i];
                if (referrerUserInfo.leadershipScore < pos.sales) {
                    break;
                }
                points = points.add(pos.reward);
                currentPosition = currentPosition.add(1);
            }
            referrerUserInfo.currentLeadershipPosition = currentPosition;
            referrerUserInfo.referralDebt = referrerUserInfo.referralDebt.add(points);
            userInfo[referrerInfo.referrer] = referrerUserInfo;
        }
        if (
            referrerInfo.referrer != address(0) &&
            referrerInfo.initialReward == false
        ) {
            uint256 commision = calcReferralReward(_transactionAmount);
            if (commision > 0) {
                totalReferralCommissions[referrerInfo.referrer] += commision;
                referrerInfo.initialReward = true;
                referrers[_user] = referrerInfo;

                UserInfo memory referrerUserInfo = userInfo[
                    referrerInfo.referrer
                ];
                referrerUserInfo.referralDebt = referrerUserInfo.referralDebt.add(commision);
                userInfo[referrerInfo.referrer] = referrerUserInfo;

                emit ReferralCommissionRecorded(
                    referrerInfo.referrer,
                    commision
                );
                emit ReferralCommissionPaid(
                    _user,
                    referrerInfo.referrer,
                    commision
                );
            }
        }
    }

    function enableEmergencyWithdrawal(bool _enable) public onlyOwner {
        emergencyWidthdrawal = _enable;
    }
}

I noticed the hacker(person who exploited the contract) first of all made an investment with a contract(The contract used is not verified on BSCScan so I can't read it) instead of an address and after that he was able to drain the contract of all the funds.

EDIT Here is the link to the contract I deployed and here are the transaction hashes of the hack: The hacker's initial investment: https://bscscan.com/tx/0xe1eafbde6bbce8afb8f74a93f07d2dc67da92cb6d20cd07a7338305d7e559ece The first hack attempt: https://bscscan.com/tx/0xaa6ddde1a4c60b7888076b2e347fd2edbefe91fdf9af152456d0b90f0b29eec3 The second: https://bscscan.com/tx/0xc67fe4570ba2bcef018cf047922d64e613d9674e412ab8e3201a8549f33e72f4 The third: https://bscscan.com/tx/0x89359fbdc183938eec8a5f82aa48f8ba8fe59772dd32dbf057ac8074ea118492

8
  • I haven't been able to figure out where the vulnerability is. But accepting contracts to interact with your contract is a risky thing. I recommend you check if the address that is investing is not a contract but an EOA account. For that, you can do something like this in the invest method: require(msg.sender.code.length == 0, "Contracts not allowed."); I'll continue to check where the vulnerability is... Commented Aug 3, 2022 at 17:39
  • Please share the address or link to the explorer so we can see the last methods called before the funds draining themselves and have a better way to now what to look at
    – Julissa DC
    Commented Aug 3, 2022 at 17:52
  • 1
    Exactly, @JulissaDC, I think that more information and details will help up figure out how the hacker launched the attack and how to find the vulnerability. Commented Aug 3, 2022 at 18:12
  • 2
    You can not restrict access to your contract to EOAs only (solidity-by-example.org/hacks/contract-size) Commented Aug 4, 2022 at 12:41
  • 1
    Thanks @ikijong, that was a good one. Commented Aug 4, 2022 at 14:07

1 Answer 1

3

If you look at the transaction on tenderly, the exploiter changed the dailyReturnsInBPS to a large number before they drained your contract.

This is possible because the function setDailyReturns has no access control, so it can be called by anyone, including the hacker.

Looking at your contract, it is unclear to me how the parameter stakingDuration is supposed to be used. Since you can claim rewards even if the stakingDuration has not yet expired. See the following lines of code in the getRewards() function:

uint256 stakeAmount = user.amount;
uint256 timeDiff;
unchecked {
    timeDiff = block.timestamp - user.initialTime;
}
if (timeDiff >= stakingDuration) {
    uint256 stakingDurationInNum = 30;
    return
        stakeAmount.mul(dailyReturnsInBPS).div(10000).mul(
            stakingDurationInNum
        );
}
uint256 returnsIn30days = dailyReturnsInBPS * 30;
uint256 rewardAmount = (((stakeAmount * returnsIn30days) / 10000) *
    timeDiff) / stakingDuration;
pendingReward = rewardAmount;

I would recommend using access control for critical functions. And you should define variables as constants when they are not supposed to be changed.

2
  • Thanks so much. I can't believe I overlooked something like that. I'm truly grateful.
    – Starbody
    Commented Aug 4, 2022 at 14:18
  • No worries. Hope you recover that soon! Commented Aug 4, 2022 at 18:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.