11

I'm building a dApp that involves creating unique accounts for individual users for ERC20 deposits [I keep the addresses and private keys in a centralized database], for popular ERC20 tokens like BNB, 0x, etc. However, to function, my dApp needs to collect all these balances into a central wallet. Since these are ERC20 deposits and not ETH deposits, the user wallets have no ETH deposited in them and therefore will have no way to pay for the gas to send their ERC20 balances to the central wallet.

Does anyone know an efficient solution to this problem? My naive solution is to just send ETH into a user's wallet every time I detect an ERC20 deposit into that wallet, so that said wallet can pay for the gas to transfer, but this is ugly and seems like a lot of overhead in gas.

Thanks!

1

2 Answers 2

16

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.

Factory Smart Contract

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
8
  • Hey, thanks so much Micky. Do you by any chance have an example of a Smart Contract Factory? Dec 28, 2018 at 2:44
  • Sure, give me a few and i'll add a simple example :) Dec 28, 2018 at 2:47
  • 1
    There you go, the sample code and todo / flow sections should get you on your way :). Do remember that i haven't fully tested the code and you should write tests for this as well as decide how you want to handle exceptions in the batch caller. Dec 28, 2018 at 3:47
  • @MickySocaci You're answer has been really helpful. I've opened up a question for bounty(once available), if you could post this with the functions needed I'd be happy to accept this as an answer. Sep 18, 2019 at 11:52
  • @micky-socaci Hello! Could you please explain, how I can make transfer from Receiver contract? I thought it is possible only if you have private key from address that holds token, but contract has no private key at all. I am trying to test your code locally with USDT based on ERC20 and transfer fails.
    – Oroffe
    Apr 8, 2020 at 23:23
0

I can't comment yet, but it would be helpful if you posted some code.

Transferring all balances to a central wallet would not be possible unless you controlled the keys of every user wallet. This would require an off-chain data store to manage key implementations and could prove a security risk say those keys were ever divulged.

Technically, in the case of an ERC20 token, the tokens never move from the contract, but rather the balances are updated within the mapping of balances in the contract itself. So if you wanted to centrally control them, you could add an additional function to the ERC20 contract, which would make it no longer standard compliant, but would allow you to add in admin privileges allowing you to alter the token balances as you needed to.

2
  • Hi, thanks. Yes, I store the keys of every user wallet in a centralized db, and I'm aware of the security risks involved in that. The ERC20 contracts are not mine but rather they are popular ERC20 contracts like BNB, USDT, and so on. This isn't really a code-level problem but more of a design/implementation problem, I think. Dec 28, 2018 at 2:09
  • Since you have control of the user wallets, why not just transfer all the tokens to that wallet instead of going through the user wallets, and then just keep an up to date list of the balances using event listeners?
    – shkfnly
    Dec 28, 2018 at 2:40

Your Answer

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

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