transferFrom calls public virtual allowance function breaking overridden behaviour
See original GitHub issuePR #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:
- Created 2 years ago
- Comments:12 (8 by maintainers)
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 doAlternatively, you can copy the implementation of
_spendAllowance
and insertsuper
. This is probably better since you wouldn’t be applyingf_
andf
successively.@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 overridetransfer()
andtransferFrom()
, 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 withsend()
being called fromtransfer()
andtransferFrom()
, it shouldn’t be too much of an issue - I’ll just remove thetransfer()
andtransferFrom()
overrides.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()
.I think that guideline would be perfect, and that would then force you to add the
_allowance()
function I suggested, or similar.