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
msg.sender
fortoken
contractapprove
function will be the contract from which you are sending the interface call. SoVestingVault
contract should be allowed to spend the tokens. The approve function should go liketokenContract.approve(VestingVaultaddress , 10000, {"from": accounts[2]})
tokenContract.approve(VestingVaultaddress , 10000, {"from": accounts[0]})
Because you are sending tokens from the account[0] not account[2]A
from another contractB
through interface, at that particular call the msg.sender for the contractA
is the address of contractB
. As you called the transferFrom method, the msg.sender is contractB
means contract B's address is signing the transaction, so it needs to be approved. The contractA
doesn't know anything about which wallet you used to trigger the function in contractB
. The msg.sender will always be contract B's address.