There are two ways this is done today:
1. Normal Accounts
Generate wallet keys / addresses that your users transfer to, and you have to send gas to those addresses in order to move funds.
Downsides:
- collecting funds requires you to send gas to these addresses which will ultimately result in dust accumulating in these
- storing and managing the account private keys can become a challenge
2. Smart Contract Accounts
Deploy a smart contract factory that generates a "receiving address" that the main "account can control".
In essence you have a smart contract that creates new addresses that store received funds, and you have an "owner" address that can call a method to transfer funds.

There are multiple ways you can implement this:
The Factory contract, can implement ownership validation and control the receivers, or each receiver could do the validation itself.
Best implementation, security and performance wise i can think of is the following:
Factory contract:
- has an owner ( the owner can be changed )
- the owner can create new "receivers"
- the factory indexes receivers in a mapping ( uint => address )
- the owner can collect or return funds from receiver contracts
- accepts a batch call in order to collect / refund from multiple receivers
Batching is important if you want to lower gas usage and implement hardware signing
Think about having a separate server that does the signing, you could even have an external hardware wallet plugged into this, or an HSM ( Hardware signing module ).
The awesome part here is that you can even implement this in such a way that you can collect tokens from any Ethereum based smart contract / token tracker ( BNB / DAI / etc. ) or any token standard using a custom caller interface ( you can call arbitrary methods at any address you want ).
No need to send gas to these receivers as the owner is the one "sending the transaction" and the "receiver contract is the owner of the tokens"
Cheap gas for deploying and interacting with receivers, since you can do it when the network is not congested, as well as the ability to integrate with GasToken ( gastoken.io ) to lower your price even more.
Hope this helps.
Sample Starting Code
pragma solidity ^0.4.25;
/*
Just the interface so solidity can compile properly
We could skip this if we use generic call creation or abi.encodeWithSelector
*/
contract ERC20 {
function totalSupply() public constant returns (uint);
function balanceOf(address tokenOwner) public constant returns (uint balance);
function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
/*
Generic Receiver Contract
*/
contract Receiver {
address public owner;
constructor() public {
/*
Deployer's address ( Factory in our case )
do not pass this as a constructor argument because
etherscan will have issues displaying our validated source code
*/
owner = msg.sender;
}
/*
@notice Send funds owned by this contract to another address
@param tracker - ERC20 token tracker ( DAI / MKR / etc. )
@param amount - Amount of tokens to send
@param receiver - Address we're sending these tokens to
@return true if transfer succeeded, false otherwise
*/
function sendFundsTo( address tracker, uint256 amount, address receiver) public returns ( bool ) {
// callable only by the owner, not using modifiers to improve readability
require(msg.sender == owner);
// Transfer tokens from this address to the receiver
return ERC20(tracker).transfer(receiver, amount);
}
// depending on your system, you probably want to suicide this at some
// point in the future, or reuse it for other clients
}
/*
Factory Contract
*/
contract Factory {
address public owner;
mapping ( uint256 => address ) public receiversMap;
uint256 receiverCount = 0;
constructor() public {
/*
Deployer's address ( Factory in our case )
do not pass this as a constructor argument because
etherscan will have issues displaying our validated source code
*/
owner = msg.sender;
}
/*
@notice Create a number of receiver contracts
@param number - 0-255
*/
function createReceivers( uint8 number ) public {
require(msg.sender == owner);
for(uint8 i = 0; i < number; i++) {
// Create and index our new receiver
receiversMap[++receiverCount] = new Receiver();
}
// add event here if you need it
}
/*
@notice Send funds in a receiver to another address
@param ID - Receiver indexed ID
@param tracker - ERC20 token tracker ( DAI / MKR / etc. )
@param amount - Amount of tokens to send
@param receiver - Address we're sending tokens to
@return true if transfer succeeded, false otherwise
*/
function sendFundsFromReceiverTo( uint256 ID, address tracker, uint256 amount, address receiver ) public returns (bool) {
require(msg.sender == owner);
return Receiver( receiversMap[ID] ).sendFundsTo( tracker, amount, receiver);
}
/*
Batch Collection - Should support a few hundred transansfers
@param tracker - ERC20 token tracker ( DAI / MKR / etc. )
@param receiver - Address we're sending tokens to
@param contractAddresses - we send an array of addresses instead of ids, so we don't need to read them ( lower gas cost )
@param amounts - array of amounts
*/
function batchCollect( address tracker, address receiver, address[] contractAddresses, uint256[] amounts ) public {
require(msg.sender == owner);
for(uint256 i = 0; i < contractAddresses.length; i++) {
// add exception handling
Receiver( contractAddresses[i] ).sendFundsTo( tracker, amounts[i], receiver);
}
}
}
TODO
- add ownership change
- add view that reads balance of a receiver for a specified tracker
- add events where needed
- add exception handling in batchCollect
Flow:
- 1.Deploy Factory contract
- 2.Call createReceivers on the Factory contract
- 3.Send some test tokens to a receiver mapped in the receiversMap
- 4.Call sendFundsFromReceiverTo on the Factory to move said tokens to a new address