Transfering tokens from a user to a secondary contract

Original token contract

// contracts/TestToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20 {

    constructor () ERC20("TestToken", "LNR") {
            _mint(msg.sender, 1000000 * (10 ** uint256(decimals())));
            _approve(address(this), msg.sender, totalSupply());
    }
}

The secondary contract, which will have additional functionality.

// contracts/SecondaryContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SecondaryContract is Initializable, AccessControlUpgradeable {

    IERC20 public testToken;

    function initialize (IERC20 _testToken) public initializer {
        __AccessControl_init();
        
        testToken = _testToken;

        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function requestPayFromUser(uint256 _amount) external {
        uint256 amount = _amount;

        testToken.approve(msg.sender, amount);
        testToken.approve(address(this), amount);
        // Only have both here to test approving from both addresses, 
        // while during actual testing I used only one approve call.
        testToken.transferFrom(msg.sender, address(this), amount);
    }
}

Hello, I'm new to developing smart contracts and I have run into an issue while trying to test sending ERC20 minted tokens from a user to a contract. Ideally, what I am trying to do is to make it possible for the user to send tokens to a contract as payment for digital goods. However, I have not been able to successfully create a way where users can do this.

If I am using truffle, I can enter the console and enter

> const TestToken = await TestToken.deployed();
> await TestToken.approve([UserAddressHere], 1);
> await TestToken.transferFrom([UserAddressHere], [SecondaryContractAddress], 1);

Which will provide the transfer successfully. However, running this (seemingly same code; correct me if I'm wrong!) code within the secondary contract does not work and returns the error

"ERC20: transfer amount exceeds allowance". 

I have tried this both through truffle console such as:

> const token = await TestToken.deployed();
> const secondContract = SecondaryContract.deployed();
> await secondContract.initialize(token.address);
> await secondContract.requestPayFromUser(1);

Also I have tried this on remix and achieved the same result.

I am not sure I understand how this works precisely. My assumptions were that while testing through Truffle or Remix, the transactions would auto-pay the gas fees, much like when migrating / deploying contracts or minting tokens. I assume that the user would have to approve this through metamask when initiating this function from the front-end application. Seeing as how this is not working, I guess I am wrong!

I have spent several hours trying to work this out, and I have not been able to find any answers that seem to answer my question exactly. So, I thought I should ask the community for help on this one.

Your understanding of approve is wrong.

  • testToken.approve(msg.sender, amount); inside the requestPayFromUser function is saying "I (second contract) want to approve the spender (msg.sender) to move at most amount tokens"

  • testToken.approve(address(this), amount); inside the requestPayFromUser function is saying "I (second contract) want to approve the spender (address(this) = second contract) to move at most amount tokens".

What you want to do instead is this:

const TestToken = await TestToken.deployed();
const secondContract = SecondaryContract.deployed();
await secondContract.initialize(token.address);
await TestToken.approve(secondContract.address, 1);  // This says I (msg.sender) wants to approve the spender (secondContract) to move 1 token.
await secondContract.requestPayFromUser(1); // In this function is where you call `testToken.transferFrom(msg.sender, address(this), 1);
2 Likes

Thank you! This works and solved my issue.

1 Like