question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

transferFrom calls public virtual allowance function breaking overridden behaviour

See original GitHub issue

PR #3170 introduced a breaking change which is also a bug in that transferFrom() now calls (via _spendAllowance()) the overridable virtual function allowance() instead of accessing _allowances directly.

This is problematic where concrete implementations of ERC20 or ERC777 override virtual functions to adjust amounts so that the amounts exposed in the derived contract are a function of the amounts stored in the base contracts, and vice versa.

💻 Environment

@openzeppelin/contracts-upgradeable 4.5.1 Truffle

📝 Details

The general design of the ERC20 and ERC777 base contracts follows the pattern that public functions are virtual, with the default implementation that they call the corresponding internal virtual function. For example, approve() calls _approve(), and burn() calls _burn(). Other functions in the base contract use the internal implementations of these functions so that the behaviour is deterministic. Concrete implementations may choose to override the internal function to change the internal behaviour. But they may also choose to override the public function so that internal behaviour is unaffected.

This design is now broken by calling the public function allowance() in the transformFrom() function.

Given the thin nature of the allowance() function, the proper approach would be to follow a similar pattern to the public virtual balance() function and its underlying _balances variable. All reads of _balances are done directly. No function calls balance(). Similarly, no function should call allowance(). All reads of _allowances should be done directly.

🔢 Code to reproduce bug

    function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
        return super.transfer(recipient, f_(amount));
    }

    function allowance(address holder, address spender) public view virtual override returns (uint256) {
        return f(super.allowance(holder, spender));
    }

    function approve(address spender, uint256 value) public virtual override returns (bool) {
        return super.approve(spender, f_(value));
    }

    function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
        return super.transferFrom(sender, recipient, f_(amount));
    }

f() is a function that modifies the amount passed to it and returns the result. f_() is the complement of that function.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:12 (8 by maintainers)

github_iconTop GitHub Comments

1reaction
frangiocommented, Feb 23, 2022

I liked the approach suggested by @Amxx but the issue you pointed out in transferFrom is correct. But it is possible to patch that by also overriding _spendAllowance. The simplest patch is to do

    function allowance(address holder, address spender) public view override returns (uint256) {
        return f(super.allowance(holder, spender));
    }

    function _approve(address holder, address spender, uint256 value) internal override {
        return super._approve(holder, spender, f_(value));
    }

    function _spendAllowance(address owner, address spender, uint256 amount) internal override {
        super._spendAllowance(owner, spender, f(amount));
    }
    
    function _send(
        address from,
        address to,
        uint256 amount,
        bytes memory userData,
        bytes memory operatorData,
        bool requireReceptionAck
    ) internal override {
        return super._send(from, to, f_(amount), userData, operatorData, requireReceptionAck);
    }

Alternatively, you can copy the implementation of _spendAllowance and insert super. This is probably better since you wouldn’t be applying f_ and f successively.

    function _spendAllowance(
        address owner,
        address spender,
        uint256 amount
    ) internal virtual {
        uint256 currentAllowance = super.allowance(owner, spender);
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "ERC777: insufficient allowance");
            unchecked {
                super._approve(owner, spender, currentAllowance - amount);
            }
        }
    }
0reactions
NeoXtreemcommented, Feb 25, 2022

@frangio Your earlier reply indicated your suggested code was aggregate to @Amxx’s suggested code which included a _transfer() override. But I understand now. And I was already overriding _send() as @Amxx already noticed I was doing in his earlier reply, but which he thought was confusing because I also override transfer() and transferFrom(), before he realised that this is not an issue in the currently released version. You can see that override here. So, once the next release comes out with send() being called from transfer() and transferFrom(), it shouldn’t be too much of an issue - I’ll just remove the transfer() and transferFrom() overrides.

breaking changes should not be made on a non-major upgrade unless absolutely necessary

We agree on this in principle, but as I tried to explain earlier, following this discipline strictly would leave us with an ossified codebase, never improved, and this is not something we think would benefit users. There are some virtual functions that are meant to be overriden (e.g. _beforeTokenTransfer), and for these we can guarantee backwards compatibility, but for all the other virtual functions (which are literally all functions) we simply can’t guarantee that overrides will not break in minor updates due to otherwise simple refactors.

I understand what you’re saying here, but that’s why I suggested adding _allowance() like you have with other functions that have public virtual and internal virtual implementations. This would solve the above problem (by using a pattern you’re already using!), and your code would be fully compliant with both ISP and LSP. You effectively give the user a choice whether to change the behaviour or not by offering them public virtual and internal virtual functions in all cases. This way, you can introduce breaking changes without affecting the behaviour of concrete derivations. That’s pretty much how your code has been up to now. Like I keep saying, _allowance() would’ve solved all of this, and still allowed you to make the functional change - adding _spendAllowance().

All of this said, we will consider the topics mentioned in this discussion going forward. Perhaps we should establish a guideline that internal functions should not call public functions.

I think that guideline would be perfect, and that would then force you to add the _allowance() function I suggested, or similar.

Read more comments on GitHub >

github_iconTop Results From Across the Web

ɴeᴏ ( , ) on Twitter: "3 days ago, I discovered a recent breaking ...
PR #3170 introduced a breaking change which is also a bug in that transferFrom() now calls (via _spendAllowance()) the overridable virtual function allowance() ......
Read more >
ERC-20 Contract Walk-Through - ethereum.org
The transferFrom function. This is the function that a spender calls to spend an allowance. This requires two operations: transfer the amount ...
Read more >
In OpenZeppelin's implementation of ERC20's transferFrom ...
Is there a reason why _transfer is called prior to checking the withdrawer ( msg.sender ) allowance? Would transferFrom still work correctly if ......
Read more >
"StandardERC20" hit a require or revert statement somewhere ...
This value changes when {approve} or {transferFrom} are called. ... function name() public view virtual override returns (string memory) ...
Read more >
BTTC Contract Diff Checker - BitTorrent Chain Explorer
This value changes when {approve} or {transferFrom} are called. ... function approve(address spender, uint256 amount) public virtual override returns (bool) ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found