1

I made a vesting vault with one beneficiary set on construction and a one time fund function which can be only called by the owner. The fund function funds the contract with ERC-20 tokens and sets an unlock time. The problem comes when testing the fund function with ERC20: insufficient allowance error even though I have used the approve function.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

error BeneficiaryZeroAddressError(address beneficiary);
error AlreadyFundedError(bool funded);
error TokenZeroAddressError(address token);
error InsufficientFundAmountError(uint256 amount);
error InsufficientLockedTimeError(uint256 lockedTime);
error WithdrawerNotBeneficiaryError(address beneficiary);
error VaultNotFundedError(bool funded);
error UnlockTimeNotPassedError();

contract VestingVault is Ownable{
    event ERC20Funded(address token, uint256 amount);
    event ERC20Withdrawn(address token, uint256 amount);

    address private immutable _beneficiary;
    address public tokenVestedAddress;
    uint256 public amountVested;
    uint256 public unlockTime;
    bool public funded = false;
    IERC20 public token;

    constructor(address beneficiary) {
        if(beneficiary == address(0)) revert BeneficiaryZeroAddressError(beneficiary);
        _beneficiary = beneficiary;
    }

    // public functions

    function fund(address tokenAddress, uint256 amount, uint256 lockedTime) public onlyOwner {
        if(funded) revert AlreadyFundedError(funded);
        if(tokenAddress == address(0)) revert TokenZeroAddressError(address(tokenAddress));
        if(amount <= 0) revert InsufficientFundAmountError(amount);
        if(lockedTime <= 0) revert InsufficientLockedTimeError(lockedTime);

        token = IERC20(tokenAddress);
        token.transferFrom(msg.sender, address(this), amount);
        emit ERC20Funded(tokenAddress, amount);

        funded = true;
        tokenVestedAddress = tokenAddress;
        amountVested = amount;
        unlockTime = block.timestamp + lockedTime;
    }

    function withdraw() public {
        if(msg.sender != getBeneficiary()) revert WithdrawerNotBeneficiaryError(getBeneficiary());
        if(!funded) revert VaultNotFundedError(funded);
        if(block.timestamp < unlockTime) revert UnlockTimeNotPassedError();

        token = IERC20(tokenVestedAddress);
        token.transfer(getBeneficiary(), amountVested);
        emit ERC20Withdrawn(tokenVestedAddress, amountVested);
    }

    // view functions

    function getBeneficiary() public view returns (address) {
        return _beneficiary;
    }

    function getAmountVested() public view returns (uint256) {
        return amountVested;
    }

    function getUnlockTime() public view returns (uint256) {
        return unlockTime;
    }

    function getTimeUntilUnlocked() public view returns (uint256) {
        return unlockTime - block.timestamp;
    }
}

This is the test:

from brownie import VestingVault, TestERC20, accounts
import pytest

@pytest.fixture
def vaultContract():
    vaultContract = VestingVault.deploy(accounts[1], {"from": accounts[0]})
    return vaultContract

@pytest.fixture
def tokenContract():
    tokenContract = TestERC20.deploy(100_000 * (10**18), {"from": accounts[2]})
    return tokenContract

# fund function
def test_fund(vaultContract, tokenContract):
    assert vaultContract.funded() == False
    tokenContract.transfer(accounts[0], 10000, {"from": accounts[2]})
    tokenContract.approve(accounts[0], 10000, {"from": accounts[2]})
    vaultContract.fund(tokenContract.address, 100, 10, {"from": accounts[0]})
  • accounts[0] is the owner of the VestingVault contract
  • accounts[1] is the beneficiary of the VestingVault contract
  • accounts[2] is the owner of the TestERC20 contract

I have tried the transfer function in fund() in VestingVault.sol and I get the ERC20: transfer amount exceeds balance error even though I have transfered the tokens to the account. Also I tried to use tokenContract.approve(vaultContract.address, 10000, {"from": accounts[2]}) but still get the same error.

I have also used the OpenZeppelin contract ERC20PresetFixedSupply.sol for testing but got the same errors. This is the current ERC20 contract that I am using for testing.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract TestERC20 is ERC20 {
    constructor(uint256 initialSupply) ERC20("TestERC20", "TERC20") {
        _mint(msg.sender, initialSupply);
    }
}

This is the full error message:

    def test_fund(vaultContract, tokenContract):
        assert vaultContract.funded() == False
        tokenContract.transfer(accounts[0], 10000, {"from": accounts[2]})
        tokenContract.approve(accounts[0], 10000, {"from": accounts[2]})
>       vaultContract.fund(tokenContract.address, 100, 10, {"from": accounts[0]})
E       brownie.exceptions.VirtualMachineError: revert: ERC20: insufficient allowance
E       Trace step -1, program counter 827:
E         File "contracts/VestingVault.sol", line 43, in VestingVault.fund:    
E           if(lockedTime <= 0) revert InsufficientLockedTimeError(lockedTime);
E       
E           token = IERC20(tokenAddress);
E           **token.transferFrom(msg.sender, address(this), amount);**
E           emit ERC20Funded(tokenAddress, amount);
E        
E           funded = true;

tests/test_vestingVault.py:25: VirtualMachineError
5
  • As you are transferring the tokens from the msg.sender to the contract, at that particular interface call the msg.sender for token contract approve function will be the contract from which you are sending the interface call. So VestingVault contract should be allowed to spend the tokens. The approve function should go like tokenContract.approve(VestingVaultaddress , 10000, {"from": accounts[2]}) Commented Nov 9, 2022 at 9:52
  • I have already tried that and it did not work. I get the same error. My bad I forgot to write that, I will do it now.
    – Proxy
    Commented Nov 9, 2022 at 10:19
  • ACTUALLY YOU HAVE TO APPROVE THE CONTRACT FROM THE OWNER OF THE VESTING VAULT CONTRACT. tokenContract.approve(VestingVaultaddress , 10000, {"from": accounts[0]}) Because you are sending tokens from the account[0] not account[2] Commented Nov 9, 2022 at 16:49
  • This passes the test, but I'm not 100% sure why. Could you explain a bit more please. I'm not exactly sure why do I need to use the VestingVault address in approve.
    – Proxy
    Commented Nov 9, 2022 at 19:31
  • Hey sorry for the delayed response. Actually when you call any function in a contract say contract A from another contract B through interface, at that particular call the msg.sender for the contract A is the address of contract B. As you called the transferFrom method, the msg.sender is contract B means contract B's address is signing the transaction, so it needs to be approved. The contract A doesn't know anything about which wallet you used to trigger the function in contract B. The msg.sender will always be contract B's address. Commented Nov 12, 2022 at 15:29

1 Answer 1

0

You are doing transferFrom(msg.sender, address(this), amount). But, transferFrom performs like so:

 function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

You shouldn't call vaultContract.fund with accounts[0].

Try this instead

tokenContract.approve(VestingVaultaddress, 10000, {"from": accounts[2]})
vaultContract.fund(tokenContract.address, 100, 10, {"from": accounts[2]})

Let me know if that works

2
  • That can't work because vaultContract.fund(tokenContract.address, 100, 10, {"from": accounts[2]}) must be called with accounts[0] which is the owner since fund() function can only be called by the owner
    – Proxy
    Commented Nov 9, 2022 at 19:23
  • Then use tokenContract.approve(VestingVaultaddress, 10000, {"from": accounts[0]}) vaultContract.fund(tokenContract.address, 100, 10, {"from": accounts[0]}) Commented Nov 9, 2022 at 21:01

Your Answer

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

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