You actually don't need to create/manage private keys for users or pay for their transactions when receiving tokens: you just need a contract that receives tokens (or ethers) so that you can manage afterwards what to do with them.
Below is an example of a simple smart contract that registers ERC20 token addresses and receives either ERC20 tokens or ethers:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol";
contract Example {
address public owner;
address private ZERO_ADDRESS = 0x0000000000000000000000000000000000000000;
mapping(string => address) public contractAddressERC20;
constructor() public {
owner = msg.sender;
}
//Register the contract address of an ERC20 Token
function registerTokenERC20(string memory tokenName, address tokenContractAddress) external onlyAdmin() isToken(tokenName) {
require(contractAddressERC20[tokenName] == ZERO_ADDRESS,
"token address is already registered");
require(bytes(tokenName).length < 25,
"token name too long");
contractAddressERC20[tokenName] = tokenContractAddress;
}
// Unregister the contract address of an ERC20 Token
function unRegisterTokenERC20(string memory tokenName) external onlyAdmin() isToken(tokenName) {
require(contractAddressERC20[tokenName] != ZERO_ADDRESS,
"token address is not registered yet");
contractAddressERC20[tokenName] = ZERO_ADDRESS;
}
// Transfer ERC20 token from sender address to contract address
function depositERC20Token(string memory tokenName, uint256 amount) external {
require(contractAddressERC20[tokenName] != ZERO_ADDRESS,
"token is not registered into the platform");
require(IERC20(contractAddressERC20[tokenName]).allowance(msg.sender, address(this)) >= amount,
"token amount to be transferred is not yet approved by User");
IERC20(contractAddressERC20[tokenName]).transferFrom(msg.sender, address(this), amount);
}
// Transfer ether from sender address to contract address
function depositEther() external payable {
require(msg.value > 0, "amount must be greater than 0 ethers");
}
// Modifier to require that 'tokenName' is not empty
modifier isToken(string memory tokenName) {
bytes memory bytesTokenName = bytes(tokenName);
require(bytesTokenName.length != 0, "tokenName can't be empty");
_;
}
// Modifier to require that only the contract owner is allowed
modifier onlyAdmin() {
require(msg.sender == owner, "only admin");
_;
}
// Get contract balance in weis
function getBalance() public view returns (uint256) {
return payable(address(this)).balance;
}
}
Some considerations:
You need to register first the address of the ERC20 smart contract that you will accept. For instance, if you are in Ropsten network:
"DAI", "0xad6d458402f60fd3bd25163575031acdce07538d"
"UNI", "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"
"WETH", "0xc778417e063141139fce010982780140aa0cd5ab"
...
You need to add withdrawal functions (admin only) to retrieve the deposited tokens or ethers and do whatever with them. Here you will have to pay the gas fees.
Users have to approve first the transfer of their ERC20 tokens to this smart contract from their accounts (from a UI-perspective, check out how Uniswap does when swapping tokens).
Again, this is just a basic example to start from, but you can add much more features on top of this depending on your requirements.