Disbursing rewards

I wish to know if the function given below correctly transfers rewards from the contract caller to the required account or not. Is there anything to consider before calling this function from some source?

I'm assuming that I need to approve an amount that the contract can spend from. Can this be done once at the time of contract deployment? If so, how can I ensure that every address with the DISBURSER_ROLE gets approved, i.e. during role assignment?

:1234: Code to reproduce

function disburseReward(address to, uint256 rewardWei)
    external
    onlyRole(DISBURSER_ROLE)
    returns (bool)
{
    if (msg.sender.balance < gasleft()) revert("native balance < gas");
    if (token.balanceOf(msg.sender) < rewardWei)
        revert("token balance < reward");

    if (!token.transferFrom(msg.sender, to, rewardWei))
        revert("Token transfer failed.");
    emit RewardClaimed(msg.sender, to, rewardWei);

    return true;
}

:computer: Environment

Hardhat 2.8.3

I gave up trying to set up approvals for each DISBURSER_ROLE address and instead decided to refresh approvals at the time of disbursement. Catch is obviously the approval amount must be lower than the token's total supply, which is easy to set up.

I'm using this definition now:

/**
    @dev Disburses `reward` to `claimant`.

    Emits {Approval} events in case {disburserApproveAmount} changes.
    Emits {RewardClaimed}.
    */
function disburseReward(address claimant, uint256 reward)
    external
    onlyRole(DISBURSER_ROLE)
    whenNotPaused
    returns (bool)
{
    address disburser = msg.sender;
    // checks
    require(
        disburser.balance >= gasleft(),
        "RewardClaim: native bal < gas"
    );
    require(
        token.balanceOf(disburser) >= reward,
        "RewardClaim: token bal < reward"
    );

    // interactions
    address tokenAddress = address(token);
    uint256 currentAllowance = token.allowance(disburser, tokenAddress);
    // Will always be true for a first-time disburser
    if (currentAllowance < reward) {
        if (
            !token.increaseAllowance(
                disburser,
                disburserApproveAmount - currentAllowance
            )
        ) {
            revert("RewardClaim: approval failed.");
        }
    }

    if (!token.transferFrom(disburser, claimant, reward))
        revert("RewardClaim: claim failed.");

    emit RewardClaimed(disburser, claimant, reward);

    return true;
}

Public repo: https://gitlab.com/Vivraan/UltraContracts

I think no, cause I think you use transferFrom() rather than transfer(), so you have got to call approve() separately.

And for your second fragment, I am not sure whether it can work as expect or not.

uint256 currentAllowance = token.allowance(disburser, tokenAddress);

1)、I think it should be:

uint256 currentAllowance = token.allowance(disburser, address(this));

2)、And I think maybe increaseAllowance() is not correct either
3)、Is your reward token a standard ERC20 token? cause for some tokens, they do not return value for the function transferFrom(), maybe you can use safeTransferFrom

That second fragment had me in a pickle really because there's the "owner" and a "spender" and I think the owner is the disburser in this case? So I thought that maybe it's the token contract that's doing the spending. Should it be this contract instead?

I'm using ERC20Upgradeable (not the interface) so I have access to increaseAllowance. OZ's transferFrom does return a boolean, though it should revert on failure anyway (in which case that check is probably redundant). Will check that in some time.

Emmm, if possible, you can share the whole contract code, so I can have a look.

1 Like

Public repo is on the second post!

For convenience: https://gitlab.com/Vivraan/UltraContracts

I'm further planning to call this method from a backend hosted by a service called Moralis. Our backend code is closed source, but essentially it has a database of addresses which can receive rewards, so that's why it will call disburseReward.

I'll be handling that line of enquiries on their forum.

I found a test file at here: test/index.ts Β· main Β· Shivam Mukherjee / UltraContracts Β· GitLab

But it only has some code to deploy contracts rather than call functions, and for your contracts, I think it really needs some changes to work, for example: contracts/RewardClaim.sol
If you add the modifier initializer for the constructor(), I do not think you can call the following function initialize(), cause you have initialized your contract when deployed, so I think you should remove the modifier initializer after the constructor(), and then you can call initialize().

And there are also some errors need to fix, write more test cases to test.

That piece of code gets added by default when I use the OZ Wizard to make an ERC20Upgradeable token. I also think I'll want to initialise on construction for safety reasons mainly. I assume when upgrades are proxied their initialisers are treated as being called the first time?

I'll test on the internal network?

Ohhhh, I see, cause when you use the proxy pattern, you also need to initialize your implementation contract. yeah, it is right, just leave it alone.

Just your local environment.

So when I deploy on the local blockchain I see that the deployments are always deterministically choosing the same set of addresses. Is there a way to attach an already deployed contract in this manner, or do I have to deploy afresh each time?

Code is available on the public repo.

Yeah, cause your local environment will be initialized when you run the scripts to do actions, the same actions will lead to the same result.

Yeah, you can. It seems like you use ethers.js, so I think you can use the method attach, such as:

contractFactory.attach(contractAddress);

What I meant was, once I have deployed a contract on Hardhat Network once using hardhat test or hardhat run deploy.ts, can I attach it? It seems like that won't be possible if the contracts are deployed on deterministic addresses.

It may be possible if I run the console I guess.

yeah, I think you can.

Okay so I'm seeing that the disburseReward call isn't working.

I made a test like so:

it(`Should disburse rewards`, async () => {
  const banzanToken = await deployBanzanToken(true);

  const RewardClaim = await ethers.getContractFactory(NAME_REWARD_CLAIM);
  const rewardClaim = await upgrades.deployProxy(
    RewardClaim,
    [banzanToken.address, initDisburseApproveAmount],
    { kind: "uups" }) as RewardClaim;

  const [owner, addr1] = await ethers.getSigners();

  const reward = BigNumber.from(decimals).div(100_000);
  expect(rewardClaim.disburseReward(addr1.address, reward))
    .to.not.be.reverted;
  expect(reward).to.equal(await banzanToken.balanceOf(addr1.address));
  expect(initMintAmount.sub(reward)).to.equal(await banzanToken.balanceOf(owner.address));
})

But I get this failure:

1) RewardClaim
       Should disburse rewards:

      AssertionError: Expected "10000000000000" to be equal 0
      + expected - actual

       {
      -  "_hex": "0x00"
      +  "_hex": "0x09184e72a000"
         "_isBigNumber": true
       }

This indicates that no amount was disbursed at all!

I think it should be

uint256 currentAllowance = token.allowance(disburser, address(this));

Cause the disburser should approve this contract, and then this contract can transfer token from disburser to user.
And it seems like you use the hardhat, so I think you can use the console.sol in the contract to check.

I had tested with this change already.

The latest change uses this:

it(`Should disburse rewards`, async () => {
  const banzanToken = await deployBanzanToken(true);

  const RewardClaim = await ethers.getContractFactory(NAME_REWARD_CLAIM);
  const rewardClaim = await upgrades.deployProxy(
    RewardClaim,
    [banzanToken.address, initDisburseApproveAmount],
    { kind: "uups" }) as RewardClaim;

  const [owner, addr1] = await ethers.getSigners();

  const reward = BigNumber.from(decimals).div(100_000);
  await expect(rewardClaim.disburseReward(addr1.address, reward))
    .to.not.be.reverted.then(async () => {
      expect(reward).to.equal(await banzanToken.balanceOf(addr1.address));
      expect(initMintAmount.sub(reward)).to.equal(await banzanToken.balanceOf(owner.address));
    });
})

In this case, I find that the transaction reverts:

BanzanToken
    √ Should have symbol ZAN (for now) (1090ms)
    √ Should not be able to attach to existing contract deployments for BanzanToken

  RewardClaim
    √ Should not deploy RewardClaim if BanzanToken has no supply (170ms)
    √ Should deploy RewardClaim if BanzanToken has supply (1202ms)
    1) Should disburse rewards


  4 passing (4s)
  1 failing

  1) RewardClaim
       Should disburse rewards:
     AssertionError: Expected transaction NOT to be reverted

When run without the expect:

1) RewardClaim
       Should disburse rewards:
     Error: VM Exception while processing transaction: reverted with reason string 'ERC20: transfer amount exceeds allowance'   
      at BanzanToken.transferFrom (@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:164)
      at <UnrecognizedContract>.<unknown> (0xa513e6e4b8f2a923d98304ec87f64353c4d5c853)
      at RewardClaim.disburseReward (contracts/RewardClaim.sol:148)
      at <UnrecognizedContract>.<unknown> (0x8a791620dd6260079bf849dc5567adc3f2fdc318)
      at HardhatNode._mineBlockWithPendingTxs (node_modules\hardhat\src\internal\hardhat-network\provider\node.ts:1650:23)   

Basically the allowance was not set up correctly.

I changed the contract to log out like so:

// ...
// interactions
        address selfAddress = address(this);
        uint256 currentAllowance = token.allowance(disburser, selfAddress);
        console.log(
            "DEBUG: currentAllowance = %d, reward = %d",
            currentAllowance,
            reward
        );
        // Will always be true for a first-time disburser
        if (currentAllowance < reward) {
            if (
                !token.increaseAllowance(
                    disburser,
                    disburserApproveAmount - currentAllowance
                )
            ) revert("RewardClaim: approval failed.");
            else
                console.log(
                    "DEBUG: approval succeeded. Allowance = %d, disburserApproveAmount = %d",
                    token.allowance(disburser, selfAddress),
                    disburserApproveAmount
                );
        }

        if (!token.transferFrom(disburser, claimant, reward))
            revert("RewardClaim: claim failed.");
        else console.log("DEBUG: claim succeeded!");
// ...

Upon which I got

  RewardClaim
    √ Should not deploy RewardClaim if BanzanToken has no supply (149ms)
    √ Should deploy RewardClaim if BanzanToken has supply (1211ms)
DEBUG: currentAllowance = 0, reward = 10000000000000
DEBUG: approval succeeded. Allowance = 0, disburserApproveAmount = 1e+22
    1) Should disburse rewards


  4 passing (4s)
  1 failing

  1) RewardClaim
       Should disburse rewards:
     Error: VM Exception while processing transaction: reverted with reason string 'ERC20: transfer amount exceeds allowance'
...

So I'm absolutely sure the allowance isn't going up.

Same error when I try token.increaseAllowance(selfAddress, disburserApproveAmount - currentAllowance), going by the allowance checking logic.

Inside ERC20Upgradeable.sol:

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * Requirements:
     *
     * - `sender` and `recipient` cannot be the zero address.
     * - `sender` must have a balance of at least `amount`.
     * - the caller must have allowance for ``sender``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) public virtual override returns (bool) {
        _transfer(sender, recipient, amount);

        uint256 currentAllowance = _allowances[sender][_msgSender()];
        require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
        unchecked {
            _approve(sender, _msgSender(), currentAllowance - amount);
        }

        return true;
    }

    /**
     * @dev Atomically increases the allowance granted to `spender` by the caller.
     *
     * This is an alternative to {approve} that can be used as a mitigation for
     * problems described in {IERC20-approve}.
     *
     * Emits an {Approval} event indicating the updated allowance.
     *
     * Requirements:
     *
     * - `spender` cannot be the zero address.
     */
    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue);
        return true;
    }

Since both of these methods are called by RewardClaim, the msg.sender for both (since ContextUpgradeable defaults this value and _msgSender() isn't overriden in ERC20*) will remain the same. As such I think it's impossible to both approve an allowance and perform a transfer from the same contract.

Unless of course I don't know how msg.sender works, in which case, please educate me! :sweat_smile:

It seems like you did not call the approve() before call disburseReward(), I think the correct steps should be:

  • Alice gets some banzanToken
  • Alice calls banzanToken.approve(rewardClaimAddress, uint256(-1))
  • Alice calls rewardClaim.disburseReward(BobAddress, AMOUNT)