-
Notifications
You must be signed in to change notification settings - Fork 21
Add ERC20Freezable, ERC20Restricted and ERC20uRWA #186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
mapping(address account => uint256) private _frozenBalances; | ||
|
||
/// @dev The operation failed because the user has insufficient unfrozen balance. | ||
error ERC20InsufficientUnfrozenBalance(address user, uint256 needed, uint256 available); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about ERC7943InsufficientUnfrozenBalance
? Is not mandatory but just curious of your thoughts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I felt it was not worth introducing concepts outside of ERC20Freezable's domain. So I preferred to keep ERC20InsufficientUnfrozenBalance
for consistency with the contract naming. None is objectively better imo, though.
btw I just recalled that we already have an ERC20Custodian that might replace this ERC20Freezable, I just need to take a deeper look
* } | ||
* ``` | ||
*/ | ||
function isUserAllowed(address user) public view virtual returns (bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Choosing a default looks opinionated, how about leaving that abstract?
Maybe I misunderstood you initial comment here:
otherwise the ERC20uRWA implementation would keep isUserAllowed virtual to return allowed or blocked accordingly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that comment is related to how the ERC20uRWA was set up in the first place. See this comment.
I agree that a default is opinionated, but I think we don want to be opinionated here to avoid users to make a decision at first. I prefer a non-restrictive default rather than a virtual function
* ``` | ||
*/ | ||
function isUserAllowed(address user) public view virtual returns (bool) { | ||
return getRestriction(user) != Restriction.RESTRICTED; // i.e. DEFAULT && UNRESTRICTED |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about adding the two isAllowed(address user)
& isNotBlocked(address user)
so user can straightfowardly override:
function isUserAllowed(address user) public view override returns (bool) {
return isNotBlocked(user); // or !isBlocked(..)
}
function isUserAllowed(address user) public view override returns (bool) {
return isAllowed(user);
}
(?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good idea. Though, these functions come in ERC20Allowlist and ERC20Blocklist (i.e. allowed
and blocked
respectively). Initially ERC20Restricted was not a thing and this was the default recommendation (see here):
// Using ERC20Allowlist
contract MyuRWA20 is uRWA20, ERC20Allowlist {
function isUserAllowed(address user) public view virtual override returns (bool) {
return allowed(user);
}
}
// Using ERC20Blocklist
contract MyuRWA20 is uRWA20, ERC20Blocklist {
function isUserAllowed(address user) public view virtual override returns (bool) {
return !blocked(user);
}
}
I currently prefer the ERC20Freezable with a default "blocklist" behavior, since I believe is the least restrictive option (i.e. an allowlist is just a stricter blocklist)
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; | ||
|
||
// solhint-disable-next-line contract-name-capwords | ||
abstract contract uRWA20Mock is uRWA20, AccessControl { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
abstract contract uRWA20Mock is uRWA20, AccessControl { | |
contract uRWA20Mock is uRWA20, AccessControl { |
?
Do we need the mock to be abstract?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not needed but it helps because hardhat-exposed exposes the constructors automatically 😄
EDIT: See cf08cbf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the file name match the contract?
// We don't check frozen balance for approvals since the actual transfer | ||
// will be checked in _update. This allows for more flexible approval patterns. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm almost sure there was a reason why we were restricting approvals in ERC20Allowlist and ERC20Blocklist but can't remember it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For clarity, it is not possible to bypass the restriction since every transferFrom
would go through _update
} else if (to != address(0)) { | ||
// Transfer | ||
require(isTransferAllowed(from, to, 0, amount), ERC7943NotAllowedTransfer(from, to, 0, amount)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'll remove this check. Essentially, it duplicates the check that super._update
already does given it inherits from ERC20Freezable.
In terms of compliance, the spec states:
Public transfers (transfer, transferFrom, safeTransferFrom, etc.) MUST NOT succeed in cases in which isTransferAllowed or isUserAllowed would return false for either one or both from and to addresses.
So this is still true even if we don't call isTransferAllowed
here.
The isTransferAllowed MUST validate that the amount being transferred doesn't exceed the unfrozen amount (which is the difference between the current balance and the frozen balance). Additionally it MUST perform an isUserAllowed check on the from and to parameters.
-
ERC20Freezable._update
checks:if (from != address(0))
→ validatesamount <= available(from)
✅
-
ERC20Restricted._update
checks:if (from != address(0)) _checkRestricted(from)
→ callsisUserAllowed(from)
✅if (to != address(0)) _checkRestricted(to)
→ callsisUserAllowed(to)
✅
This change would stop emitting the ERC7943NotAllowedTransfer error, but I think that's fine since it's not very expressive and the super._update
call will revert with more appropriate errors. The ERC allows to skip reverting with ERC7943NotAllowedTransfer.
As a side effect, I think any override to isTransferAllowed
would not be enforced in _update
automatically, so I guess we can note it.
ERC-7943 made it as a draft, so I think it's fine to start by implementing the ERC20 version. A couple of notes:
isUserAllowed
virtual to returnallowed
orblocked
accordinglyapprove
function but we're doing it in ERC20Allowlist and ERC20Blocklist so we need to make sure there's not an alternative path to bypass the restrictions