BotGains - Audit Report


Botgains Audit Report BotGains is a crypto trading pool that intends to allow users to deposit ETH and have the team trade with the funds with the goal of making profits and returning those profits to investors. Botgains intends to do this by utilizing varius types of trading bots to execute profitable trades.

We audited contracts that the BotGains team provided to us. Please note we have not reviewed Botgains trading strategies.
  • BotGainsProtocol is deployed at 0x7ad46de120fbb86d00a877b45de07be4c4376013.
  • BotGainsProtocolStorage is deployed at 0x5ca7d64069ffa52e65017e31ae4583d9cd4e035a.
  • Findings on the Contracts:
    • BotGains allows users to deposit ETH that the Botgains will use to trade with and provide back a portion of the profits to depositers.
    • Depositing with BotGains requires committing to a lockup period, currently set at 30 days. Each month rewards will be added to the contract and available for distribution.
    • After each period, users will have the option of withdrawing funds or reinvesting in the platform.
    • The fees associated with investing are a 2% fee on deposits, reinvests, and dividends paid (collected by the admin address); a 20% management fee on profits earned; and an additional .25% fee on profits earned for the funding of a future “Loyalty Bonus”.

    • The owner can also change a number of variables in the smart contract. These include: The minimum/maximum amount ETH to invest, the maximum pool size, lock/unlock times for deposits, and wallet addresses for the following uses: Bot, Owner, Admin, Management, Divs, Bonus, & trading.
    • Each of these addresses have different uses. Only the address set to be the Bot is meant to withdraw ETH under normal circumstances; and only the Bonus address can send bonuses to the contract properly.

    • Usage of ReentrancyGuard in applicable functions to prevent re-entrancy attacks.
    • Utilization of SafeMath and SafeERC20 to prevent overflows and ensure safe transfers.

    • No security issues from outside attackers were identified.
    • Investing requires placing substantial trust in the team as they must send the ETH back to the investment contract manually.
    • Date: December 29th, 2020.
    • Update Date: February 21st, 2021 - Review V1.5 code.

    BotGains includes the following disclaimer on their site: No Guarantee of Profits. Funds given as a donation to BotGains admins to trade with. No losses to be represented nor paid back. The Pool Trader is in no respects making any guarantee of profits or of protections against loss, but is undertaking to use reasonable best efforts to trade profitably on behalf of the user pool.

    Vulnerability Category Notes Result
    Arbitrary Storage Write N/A PASS
    Arbitrary Jump N/A PASS
    Delegate Call to Untrusted Contract N/A PASS
    Dependence on Predictable Variables N/A PASS
    Deprecated Opcodes N/A PASS
    Ether Thief N/A PASS
    Exceptions N/A PASS
    External Calls N/A PASS
    Flash Loans N/A PASS
    Integer Over/Underflow N/A PASS
    Multiple Sends N/A PASS
    Oracles N/A PASS
    Suicide N/A PASS
    State Change External Calls N/A Pass
    Unchecked Retval N/A PASS
    User Supplied Assertion N/A PASS
    Critical Solidity Compiler N/A PASS
    Overall Contract Safety   PASS

    Smart Contract Graph

    Contract Inheritance

     ($) = payable function
     # = non-constant function
     Int = Internal
     Ext = External
     Pub = Public
     +  Context 
        - [Int]  #
        - [Int] _msgSender
        - [Int] _msgData
     + [Lib] SafeMath 
        - [Int] add
        - [Int] sub
        - [Int] sub
        - [Int] mul
        - [Int] div
        - [Int] div
        - [Int] mod
        - [Int] mod
     +  Ownable (Context)
        - [Int]  #
        - [Pub] owner
        - [Pub] isOwner
        - [Pub] renounceOwnership #
           - modifiers: onlyOwner
        - [Pub] transferOwnership #
           - modifiers: onlyOwner
        - [Int] _transferOwnership #
     +  BotGainsProtocolStorage (Ownable)
        - [Pub]  #
        - [Ext] changeCycleLength #
           - modifiers: onlyOwner
        - [Pub] _cycleLength
        - [Ext] changeLockTimer #
           - modifiers: onlyOwner
        - [Ext] changeUnlockTimer #
           - modifiers: onlyOwner
        - [Pub] _lockTimer
        - [Pub] _unlockTimer
        - [Ext] changeMinETH #
           - modifiers: onlyOwner
        - [Ext] changeMaxETH #
           - modifiers: onlyOwner
        - [Ext] changeMaxPoolSize #
           - modifiers: onlyOwner
        - [Pub] _minETH
        - [Pub] _maxETH
        - [Pub] _maxPoolSize
        - [Ext] changeAdminFeeWallet #
           - modifiers: onlyOwner
        - [Ext] changeManagementFeeWallet #
           - modifiers: onlyOwner
        - [Ext] changeDivsFeeWallet #
           - modifiers: onlyOwner
        - [Ext] changeBonusWallet #
           - modifiers: onlyOwner
        - [Ext] changeTradingWallet #
           - modifiers: onlyOwner
        - [Pub] _tradingWallet
        - [Pub] _adminFeeWallet
        - [Pub] _managementFeeWallet
        - [Pub] _divsFeeWallet
        - [Pub] _bonusWallet
     +  ReentrancyGuard 
        - [Int]  #
     +  BotGainsProtocol (Ownable, ReentrancyGuard)
        - [Pub]  #
        - [Ext]  ($)
        - [Pub] userDeposit ($)
           - modifiers: isUnlocked,nonReentrant
        - [Pub] userWithdrawFundsOnCycle #
           - modifiers: nonReentrant,isUnlocked
        - [Ext] BOTwithdraw #
           - modifiers: onlyBot,isUnlocked
        - [Ext] BOTdeposit ($)
           - modifiers: onlyBot,isNotUnlocked
        - [Int] incrementUser #
        - [Int] transferToDivs #
        - [Int] transferToAdmin #
        - [Int] transferToManagement #
        - [Int] checkUserLimit
        - [Int] calculateEth
        - [Ext] viewFundsAvailable
        - [Ext] viewCurrentlyInvested
        - [Ext] viewPool

    Click here to download the source code as a .sol file.

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.5.0;
    import "./BotGainsProtocolStorage.sol";
    import "@openzeppelin/contracts/math/SafeMath.sol";
    import "@openzeppelin/contracts/ownership/Ownable.sol";
    import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
    contract BotGainsProtocol is Ownable,ReentrancyGuard {
        using SafeMath for uint256;
        BotGainsProtocolStorage private _protocol_storage;
        //state variables
        bool public locked = false;
        uint256 public currentCycle;
        bool public loss;
        uint256 private fragsPerETH;
        uint256 public timeLocked;
        uint256 public timeUnlocked;
        //cycle mappings
        mapping(uint256 => mapping(address => bool)) public userExistOnCycle;
        mapping(uint256 => uint256) private TOTAL_FRAGS_ON_CYCLE; //tracks total frags on a given cycle
        mapping(uint256 => uint256) private FRAGS_PER_ETH_ON_CYCLE;
        mapping(uint256 => mapping(address => uint256)) private USER_FRAGS_ON_CYCLE; //tracks capital on a given cycle\
        mapping(uint256 => uint256) public USERS_ON_CYCLE;
        mapping(uint256 => uint256) public POOL_ON_CYCLE;
        mapping(uint256 => bool) public PROFIT_ON_CYCLE;
        modifier cycleHappend(){
            require(currentCycle > 0, "no cycles have occured yet");
        modifier isUnlocked() {
            require(!locked, "The bot is current trading!");
        modifier isNotUnlocked() {
            require(locked, "The bot is currently trading!");
        modifier onlyBot() {
            require(msg.sender == _protocol_storage._tradingWallet(), "Not the trader!");
        modifier onlyBonus() {
            require(msg.sender == _protocol_storage._bonusWallet(), "Not the Bonus Wallet");
        event BotWithdraw(uint256 poolAmount, uint256 users, uint256 atTime, uint256 onCycle);
        event BotDeposit(uint256 depositAmount, uint256 forCycle, bool loss, uint256 atTime);
        event UserDeposit(uint256 depositAmount, uint256 onCycle, uint256 atTime);
        event UserWithdraw(uint256 poolAmount, uint256 onCycle, uint256 atTime);
        constructor (address _storage) public {
            _protocol_storage = BotGainsProtocolStorage(_storage);
            //assign state variables
            currentCycle = 0;
            fragsPerETH = 1e30;
            FRAGS_PER_ETH_ON_CYCLE[0] = fragsPerETH;
            timeUnlocked = now;
            USERS_ON_CYCLE[currentCycle] = 0;
        //revert payable fallback
        function() payable external {
            revert("Use userDeposit");
        ************* USER FUNCTIONS **************
        function userDeposit() public payable isUnlocked nonReentrant {
            require(_protocol_storage._minETH() <= msg.value, "Minimum not met"); //good
            incrementUser(_msgSender()); //good
            uint256 userETHamount = msg.value.mul(97750).div(1e5); //97.75%
            uint256 userFeeAmount = msg.value.mul(2000).div(1e5); //2%
            uint256 userDivFeeAmount = msg.value.mul(250).div(1e5); //.25%
            //check user deposit into current pool & check that this deposit does not exceed pool limit
            checkUserLimit(_msgSender(), userETHamount);
            //update this user's balance for this investment cycle
            uint256 fragAmount = (userETHamount).mul(FRAGS_PER_ETH_ON_CYCLE[currentCycle]); //user frag balance -- good
            //if multiple deposits have been made this step makes sure frags have been updates correctly with additional deposit
            USER_FRAGS_ON_CYCLE[currentCycle][_msgSender()] = USER_FRAGS_ON_CYCLE[currentCycle][_msgSender()].add(fragAmount);
            //keep track of total frags on this cycle
            TOTAL_FRAGS_ON_CYCLE[currentCycle] = TOTAL_FRAGS_ON_CYCLE[currentCycle].add(fragAmount); // good -- track totals
            //add liquidity to pool for this round of investment
            POOL_ON_CYCLE[currentCycle] = POOL_ON_CYCLE[currentCycle].add(userETHamount); //good -- updates total ETH alongside FRAGS
            userExistOnCycle[currentCycle][_msgSender()] = true;
            //transfer fees
            transferToAdmin(userFeeAmount); // good
            transferToDivs(userDivFeeAmount); // good
            emit UserDeposit(userETHamount, currentCycle, block.timestamp);
        function userWithdrawFundsOnCycle(uint256 _cycle) public nonReentrant isUnlocked{
            //get user frags on this cycle
            uint256 fragAmount = USER_FRAGS_ON_CYCLE[_cycle][_msgSender()]; // good
            if(USER_FRAGS_ON_CYCLE[_cycle][_msgSender()] == 0) {
                revert("BotGains: Nothing to Withdraw");
            //convert these frags to eth amount 
            uint256 ETHamount = calculateEth(fragAmount, _cycle); // good
            //send to user
            _msgSender().transfer(ETHamount); // good
            //zero them out
            USER_FRAGS_ON_CYCLE[_cycle][_msgSender()] = 0; // good
            //subtract FRAGS from TOTAL on this cycle
            TOTAL_FRAGS_ON_CYCLE[_cycle] = TOTAL_FRAGS_ON_CYCLE[_cycle].sub(fragAmount); // good
            //update the current cycle pool stats
            if(_cycle == currentCycle){
                POOL_ON_CYCLE[_cycle] = POOL_ON_CYCLE[_cycle].sub(ETHamount); // good        
                //update user count
                USERS_ON_CYCLE[_cycle] = USERS_ON_CYCLE[_cycle].sub(1); // good    
                userExistOnCycle[_cycle][_msgSender()] = false;
            emit UserWithdraw(ETHamount, _cycle, block.timestamp);
        * BOT FUNCTIONS * 
        function BOTwithdraw() external onlyBot isUnlocked {
            //withdraw the pool amount for this investment cycle
            uint256 poolAmount = POOL_ON_CYCLE[currentCycle];
            //lock the user FUNCTIONS
            locked = true;
            timeLocked = now;
            //emit event to show the amount withdrawn, users, and time
            emit BotWithdraw(poolAmount, USERS_ON_CYCLE[currentCycle], block.timestamp, currentCycle);
        function BOTdeposit() payable onlyBot isNotUnlocked external {
            uint256 ETHamount = msg.value;
            if(ETHamount == 0){            
                //if the eth amount here is 0 the previous cycle pool amount can NOT be 0 as well
                if(POOL_ON_CYCLE[currentCycle] != 0)  {
                    revert("deposit and pool funds must both be zero or both be non zero");
            } else{
                //previous pool cycle CANNOT BE ZERO if deposit is not zero!
                if(POOL_ON_CYCLE[currentCycle] == 0){
                    revert("deposit and pool funds must both be zero or both be non zero");
                if (POOL_ON_CYCLE[currentCycle] <= ETHamount){ // WIN!
                    //subtract eth amount from pool amount to get profit amount and take 20% 
                    uint256 profit = ETHamount.sub(POOL_ON_CYCLE[currentCycle]);
                    uint256 forManagement = profit.mul(20).div(100);
                    uint256 ethForPool = ETHamount.sub(forManagement);
                    //transfer 20% of profit to management
                    //caluclate the new fragsPerEth on this cycle
                    FRAGS_PER_ETH_ON_CYCLE[currentCycle] = TOTAL_FRAGS_ON_CYCLE[currentCycle].div(ethForPool); // good
                    FRAGS_PER_ETH_ON_CYCLE[currentCycle.add(1)] = fragsPerETH;
                    loss = false;
                } else{ // LOSS!
                    //caluclate the new fragsPerEth on this cycle
                    FRAGS_PER_ETH_ON_CYCLE[currentCycle] = TOTAL_FRAGS_ON_CYCLE[currentCycle].div(ETHamount); // good
                    FRAGS_PER_ETH_ON_CYCLE[currentCycle.add(1)] = fragsPerETH;
                    loss = true;
            //increment cycle
            currentCycle = currentCycle.add(1);
            //init users
            USERS_ON_CYCLE[currentCycle] = 0;
            locked = false;
            timeUnlocked = now;
            emit BotDeposit(ETHamount, currentCycle.sub(1), loss, block.timestamp);
        /*internal utils*/
        function incrementUser(address _user) internal {
                userExistOnCycle[currentCycle][_user] = true;
                USERS_ON_CYCLE[currentCycle] = USERS_ON_CYCLE[currentCycle].add(1);
        function transferToDivs(uint256 amount) internal{
        function transferToAdmin(uint256 amount) internal{
        function transferToManagement(uint256 amount) internal{
        function checkUserLimit(address _user, uint256 amount) view internal{
            if((USER_FRAGS_ON_CYCLE[currentCycle][_user].div(FRAGS_PER_ETH_ON_CYCLE[currentCycle])).add(amount) > _protocol_storage._maxETH()){
                revert("This user has reached the maximum limit for deposits!");
            if(POOL_ON_CYCLE[currentCycle].add(amount) > _protocol_storage._maxPoolSize()){
                revert("Maximum amount of ETH reached");
        function calculateEth(uint256 fragAmount, uint256 _cycle) internal view returns(uint256){
            return fragAmount.div(FRAGS_PER_ETH_ON_CYCLE[_cycle]);    
        function viewFundsAvailable(uint256 _cycle, address _user) external view returns(uint256) {
            uint256 _userFrags = USER_FRAGS_ON_CYCLE[_cycle][_user];
            if(_userFrags == 0){
                return 0;
            uint256 _fragsPerETH = FRAGS_PER_ETH_ON_CYCLE[_cycle];
            return _userFrags.div(_fragsPerETH);
        function viewCurrentlyInvested(address _user) external view returns(uint256) {
            if(USER_FRAGS_ON_CYCLE[currentCycle][_user] == 0){
                return 0;
            return USER_FRAGS_ON_CYCLE[currentCycle][_user].div(FRAGS_PER_ETH_ON_CYCLE[currentCycle]);
        function viewPool(uint256 _cycle) external view returns(uint256){
            return POOL_ON_CYCLE[_cycle];

    Click here to download the source code as a .sol file.

    // SPDX-License-Identifier: ISC
    pragma solidity ^0.5.0;
    import "hardhat/console.sol";
    import "@openzeppelin/contracts/math/SafeMath.sol";
    import "@openzeppelin/contracts/ownership/Ownable.sol";
    contract BotGainsProtocolStorage is Ownable{
        using SafeMath for uint256;
        //addressstate variable
        address payable botWallet;
        address payable adminFeeWallet;
        address payable managementFeeWallet;
        address payable divsFeeWallet;
        address payable bonusWallet;
        uint256 public minETH = 1e17;
        uint256 public maxETH = 1e21; //1000 eth
        uint256 public maxPoolSize = 500e18;
        uint256 public cycleLength;
        uint256 private lockTimer;
        uint256 private unlockTimer;
        constructor (address payable _tradingWallet,address payable _adminFeeWallet,address payable _managementFeeWallet,address payable _divsFeeWallet, address payable _bonusWallet) public {
            //assign wallet addresses
            botWallet = _tradingWallet;
            adminFeeWallet = _adminFeeWallet;
            managementFeeWallet = _managementFeeWallet;
            divsFeeWallet = _divsFeeWallet;
            bonusWallet = _bonusWallet;
            cycleLength = 30;
            unlockTimer= 30 minutes;
            lockTimer= 30 minutes;
        function changeCycleLength(uint256 _days) onlyOwner external {
            require(_days > 1, "must be greater than a single day");
            cycleLength = _days;
        function _cycleLength() public view returns(uint256) {
            return cycleLength;
        function changeLockTimer(uint256 _minutes) onlyOwner external {
            lockTimer = _minutes.mul(1 minutes);
        function changeUnlockTimer(uint256 _minutes) onlyOwner external {
            unlockTimer = _minutes.mul(1 minutes);
        function _lockTimer() public view returns(uint256){
            return lockTimer;
        function _unlockTimer() public view returns(uint256){
            return unlockTimer;
        function changeMinETH(uint256 amount) onlyOwner external {
            require(amount > 1e14, "Min ETH can be no lower than 0.001 ETH, Make sure to update the minETH in wei");
            minETH = amount;
        function changeMaxETH(uint256 amount) onlyOwner external {
            require(amount > 1e18, "Max ETH can be no lower than 1 ETH, Make sure to update the maxETH in wei");
            maxETH = amount;
        function changeMaxPoolSize(uint256 amount) onlyOwner external {
            require(amount > 1e18, "Max ETH can be no lower than 1 ETH, Make sure to update the maxPoolSize in wei");
            maxPoolSize = amount;
        function _minETH() public view returns(uint256){
            return minETH;
        function _maxETH() public view returns(uint256){
            return maxETH;
        function _maxPoolSize() public view returns(uint256){
            return maxPoolSize;
        function changeAdminFeeWallet(address payable _admin) onlyOwner external{
            adminFeeWallet = _admin;
        function changeManagementFeeWallet(address payable _manage) onlyOwner external{
            managementFeeWallet = _manage;
        function changeDivsFeeWallet(address payable _divs) onlyOwner external{
            divsFeeWallet = _divs;
        function changeBonusWallet(address payable _bonus) onlyOwner external{
            bonusWallet = _bonus;
        function changeTradingWallet(address payable _tradingWallet) onlyOwner external{
            botWallet = _tradingWallet;
        function _tradingWallet() public view returns(address payable){
            return botWallet;
        function _adminFeeWallet() public view returns(address payable){
            return adminFeeWallet;
        function _managementFeeWallet() public view returns(address payable){
            return managementFeeWallet;
        function _divsFeeWallet() public view returns(address payable){
            return divsFeeWallet;
        function _bonusWallet() public view returns(address payable){
            return bonusWallet;