Transfering an ERC20 balance of a contract, to an address

Hello,

I'm struggling to get a seemingly simple concept to work.

So far all other aspects of my contracts are testing OK with Truffle (starting game, buying tickets (with transfer of ERC20 from buyer to the contract)). The only issue is the endGame function (transfer from contract to a selected winner address (random address of one of the players)), which results in "Insufficient allowance".

The balance on the contract (after users buy tickets) is as expected, each ticket costs 1 (* 10 ** 18)

Before firing endGame, I have tried setting high approve values of the ERC20 token, for the contract and the sender (account 0, is also the creator of both the ERC20 and game contract)

let approveAmount100K = web3.utils.toBN(100000).mul(web3.utils.toBN(10).pow(decimals));
await token.approve(contract.address, approveAmount100K, {from: accounts[0]});

I have a test ERC20 token, to which a raffle game contract allows users to buy tickets, using the ERC20 (or any other potential ERC20).

I hope someone might be able to help me understand this problem, I've been looking around for answers for weeks, but can't find anything that clearly explains this particular concept of transferring an ERC20 token balance of a contract, to a regular address.

If you want to send all of the remaining ERC20 tokens out of a contract, then that contract needs a function where something like the following is called inside the context of the contract:

function endGame(address recipient) external {
  uint remainingBalance = ERC20(addressOfMyERC20).balanceOf(address(this));
  ERC20(addressOfMyERC20).transfer(recipient, remainingBalance); 
}

You don't need to call approve from an externally owned account, it won't help you because the tokens are owned by the contract address, not an externally owned account address.

2 Likes

Thanks for your response.

I should have mentioned, I had also tried transfer, which ends up in ERC20: transfer amount exceeds balance

I checked the balanceOf the contract before calling endGame to confirm the balance is as expected.
However if the balance were incorrect, it would have been caught at

    uint256 _pot = g.pot;
    uint256 _balance = g.token.balanceOf(address(this));
    require(
      g.pot <= _balance,
      "Not enough of game token in reserve"
    );

Full function

function endGame(
    uint256 _gameNumber
  ) external onlyRole(CALLER_ROLE) {
    Game storage g = games[_gameNumber];

    require(
      g.maxPlayers > 0,
      "Invalid game"
    );
    require(
      g.status == true,
      "Game already ended"
    );

    uint256 _pot = g.pot;
    uint256 _balance = g.token.balanceOf(address(this));
    require(
      g.pot <= _balance,
      "Not enough of game token in reserve"
    );

    // Close game
    g.status = false;

    // Pick winner
    uint256 _rand = _randModulus(100);
    uint256 _total = g.tickets.length - 1;
    uint256 _index = _rand % _total;
    g.winnerAddress = g.tickets[_index];

    // Send pot to winner
    g.token.transfer(g.winnerAddress, _pot);

    // @todo Trim superfluous game data for gas saving
    totalGamesEnded++;

    // Fire `GameEnded` event
    emit GameEnded(
      g.tokenAddress,
      g.winnerAddress,
      g.number,
      _pot
    );
  }

I've included buyTicket to show that pot is increased (it's done this way to allow multiple games to run)

function buyTicket(
    uint256 _gameNumber,
    uint256 _numberOfTickets
  ) external {
    Game storage g = games[_gameNumber];

    require(
      g.maxPlayers >= 0,
      "Invalid game"
    );
    require(
      g.status == true,
      "Game already ended"
    );
    require(
      _numberOfTickets > 0,
      "Buy at least 1 ticket"
    );

    // Ensure player has enough tokens to play
    uint256 _totalCost = g.ticketPrice * _numberOfTickets;
    require(
      g.token.allowance(msg.sender, address(this)) >= _totalCost,
      "Insufficent game token allowance"
    );

    // Marker for new player logic
    bool _isNewPlayer = false;

    // Current number of tickets that this player has
    uint256 _playerTicketCount = g.players[msg.sender];

    // First time player has entered the game
    if (_playerTicketCount == 0) {
      if (g.playerCount == g.maxPlayers) {
        revert("Too many players in game");
      }
      _isNewPlayer = true;
    }
    
    // Check the new player ticket count
    uint256 _playerTicketNextCount = _playerTicketCount + _numberOfTickets;
    require(
      _playerTicketNextCount <= g.maxTicketsPlayer,
      "Exceeds max player tickets, try lower value"
    );

    // Transfer `_totalCost` of `gameToken` from player, this this contract
    // _safeTransferFrom(g.token, msg.sender, address(this), _totalCost);
    g.token.transferFrom(msg.sender, address(this), _totalCost);

    // Add total ticket cost to pot
    g.pot += _totalCost;

    // If a new player (currently has no tickets)
    if (_isNewPlayer) {

      // Increase game total player count
      g.playerCount++;

      // Used for iteration on game player mapping, when resetting game
      g.playersIndex.push(msg.sender);
    }

    // Update number of tickets purchased by player
    g.players[msg.sender] = _playerTicketNextCount;

    // Add each of the tickets to an array, a random index of this array 
    // will be selected as winner.
    uint256 _i;
    while (_i != _numberOfTickets) {
      g.tickets.push(msg.sender);
      _i++;
    }

    // Increase total number of game player tickets
    g.ticketCount += _numberOfTickets;

    // Fire `TicketBought` event
    emit TicketBought(
      msg.sender,
      g.number,
      g.playerCount,
      g.ticketCount
    );
  }

Looking at your two functions here, I can't tell why you're not able to clear the balance, buyTicket and endGame look good to me. What exact error is getting thrown when you call endGame? Did you try clearing your old artifacts, recompiling, and running you unit tests again?

Hi, welcome to the community! :wave:

Yeah, you can have a try as TtheBC01 said above, and if possible, you can share your full code at here, so others can have a try.

Thanks for your support @TtheBC01 and @Skyge !

In case anyone lands here from Google (maybe it will help?). Looks like my odd issues were caused by a couple small things that I hadn't covered;

  • My UI code had doubled wrapped web3.utils.toBN() on the gameNumber param for my endGame call.
  • UI testing of buying only one ticket, then calling endGame, which caused (uint256 _total = g.tickets.length - 1) = 0 and led to a revert on uint256 _index = _rand % _total;
  • Not stepping away from problem sooner :wink: