-
Notifications
You must be signed in to change notification settings - Fork 23
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?
Changes from all commits
2072d11
f8dac42
25c4e9e
2b9b080
a9f4f7b
f874ce4
bffb8db
85f14e1
7629289
9f4c5d7
469daac
cf08cbf
e29a289
8f81436
382a4c6
ae6b960
ee40dcf
237d84f
509bcf7
2885586
a75dd13
2b08724
d3750e5
ec5a726
c1e16f0
33a017b
73c78aa
c1cd6b5
678682f
ed18ff1
ff6b169
902f32a
f5b2008
bc86ed6
e65db0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.26; | ||
|
||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
import {ERC20uRWA} from "../../token/ERC20/extensions/ERC20uRWA.sol"; | ||
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; | ||
|
||
abstract contract ERC20uRWAMock is ERC20uRWA, AccessControl { | ||
bytes32 public constant FREEZER_ROLE = keccak256("FREEZER_ROLE"); | ||
bytes32 public constant ENFORCER_ROLE = keccak256("ENFORCER_ROLE"); | ||
|
||
constructor(address freezer, address enforcer) { | ||
_grantRole(FREEZER_ROLE, freezer); | ||
_grantRole(ENFORCER_ROLE, enforcer); | ||
} | ||
|
||
function supportsInterface( | ||
bytes4 interfaceId | ||
) public view virtual override(ERC20uRWA, AccessControl) returns (bool) { | ||
return super.supportsInterface(interfaceId); | ||
} | ||
|
||
function _checkEnforcer(address, address, uint256) internal view override onlyRole(ENFORCER_ROLE) {} | ||
|
||
function _checkFreezer(address, uint256) internal view override onlyRole(FREEZER_ROLE) {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.26; | ||
|
||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; | ||
import {IERC7943} from "../../../interfaces/IERC7943.sol"; | ||
|
||
/** | ||
* @dev Extension of {ERC20} that allows to implement a freezing | ||
* mechanism that can be managed by an authorized account with the | ||
* {_freezeTokens} and {_unfreezeTokens} functions. | ||
* | ||
* The freezing mechanism provides the guarantee to the contract owner | ||
* (e.g. a DAO or a well-configured multisig) that a specific amount | ||
* of tokens held by an account won't be transferable until those | ||
* tokens are unfrozen using {_unfreezeTokens}. | ||
*/ | ||
abstract contract ERC20Freezable is ERC20 { | ||
/// @dev Frozen amount of tokens per address. | ||
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); | ||
|
||
/// @dev Returns the frozen balance of an account. | ||
function frozen(address account) public view virtual returns (uint256) { | ||
return _frozenBalances[account]; | ||
} | ||
|
||
/// @dev Returns the available (unfrozen) balance of an account. Up to {balanceOf}. | ||
function available(address account) public view virtual returns (uint256) { | ||
(bool success, uint256 unfrozen) = Math.trySub(balanceOf(account), _frozenBalances[account]); | ||
return success ? unfrozen : 0; | ||
} | ||
|
||
/// @dev Internal function to set the frozen token amount for a user. | ||
function _setFrozen(address user, uint256 amount) internal virtual { | ||
_frozenBalances[user] = amount; | ||
emit IERC7943.Frozen(user, 0, amount); | ||
} | ||
|
||
/** | ||
* @dev See {ERC20-_update}. | ||
* | ||
* Requirements: | ||
* | ||
* * `from` must have sufficient unfrozen balance. | ||
*/ | ||
function _update(address from, address to, uint256 value) internal virtual override { | ||
if (from != address(0)) { | ||
uint256 unfrozen = available(from); | ||
require(unfrozen >= value, ERC20InsufficientUnfrozenBalance(from, value, unfrozen)); | ||
} | ||
super._update(from, to, value); | ||
} | ||
|
||
// We don't check frozen balance for approvals since the actual transfer | ||
// will be checked in _update. This allows for more flexible approval patterns. | ||
Comment on lines
+58
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. For clarity, it is not possible to bypass the restriction since every There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agree with not checking |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.26; | ||
|
||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
|
||
/** | ||
* @dev Extension of {ERC20} that allows to implement user account transfer restrictions | ||
* through the {isUserAllowed} function. Inspired by https://eips.ethereum.org/EIPS/eip-7943[EIP-7943]. | ||
* | ||
* By default, each account has no explicit restriction. The {isUserAllowed} function acts as | ||
* a blocklist. Developers can override {isUserAllowed} to check that `restriction == ALLOWED` | ||
* to implement an allowlist. | ||
*/ | ||
abstract contract ERC20Restricted is ERC20 { | ||
enum Restriction { | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
DEFAULT, // User has no explicit restriction | ||
BLOCKED, // User is explicitly blocked | ||
ALLOWED // User is explicitly allowed | ||
} | ||
|
||
mapping(address account => Restriction) private _restrictions; | ||
|
||
/// @dev Emitted when a user account's restriction is updated. | ||
event UserRestrictionsUpdated(address indexed account, Restriction restriction); | ||
|
||
/// @dev The operation failed because the user account is restricted. | ||
error ERC20UserRestricted(address account); | ||
|
||
/// @dev Returns the restriction of a user account. | ||
function getRestriction(address account) public view virtual returns (Restriction) { | ||
return _restrictions[account]; | ||
} | ||
|
||
/** | ||
* @dev Returns whether a user account is allowed to interact with the token. | ||
* | ||
* Default implementation only disallows explicitly BLOCKED accounts (i.e. a blocklist). | ||
* | ||
* To convert into an allowlist, override as: | ||
* | ||
* ```solidity | ||
* function isUserAllowed(address account) public view virtual override returns (bool) { | ||
* return getRestriction(account) == Restriction.ALLOWED; | ||
* } | ||
* ``` | ||
*/ | ||
function isUserAllowed(address account) public view virtual returns (bool) { | ||
return getRestriction(account) != Restriction.BLOCKED; // i.e. DEFAULT && ALLOWED | ||
} | ||
|
||
/** | ||
* @dev See {ERC20-_update}. Enforces restriction transfers (excluding minting and burning). | ||
* | ||
* Requirements: | ||
* | ||
* * `from` must be allowed to transfer tokens (see {isUserAllowed}). | ||
* * `to` must be allowed to receive tokens (see {isUserAllowed}). | ||
*/ | ||
function _update(address from, address to, uint256 value) internal virtual override { | ||
if (from != address(0)) _checkRestriction(from); // Not minting | ||
if (to != address(0)) _checkRestriction(to); // Not burning | ||
super._update(from, to, value); | ||
} | ||
|
||
// We don't check restrictions for approvals since the actual transfer | ||
// will be checked in _update. This allows for more flexible approval patterns. | ||
|
||
/// @dev Updates the restriction of a user account. | ||
function _setRestriction(address account, Restriction restriction) internal virtual { | ||
if (getRestriction(account) != restriction) { | ||
_restrictions[account] = restriction; | ||
emit UserRestrictionsUpdated(account, restriction); | ||
} // no-op if restriction is unchanged | ||
} | ||
|
||
/// @dev Convenience function to block a user account (set to BLOCKED). | ||
function _blockUser(address account) internal virtual { | ||
_setRestriction(account, Restriction.BLOCKED); | ||
} | ||
|
||
/// @dev Convenience function to allow a user account (set to ALLOWED). | ||
function _allowUser(address account) internal virtual { | ||
_setRestriction(account, Restriction.ALLOWED); | ||
} | ||
|
||
/// @dev Convenience function to reset a user account to default restriction. | ||
function _resetUser(address account) internal virtual { | ||
_setRestriction(account, Restriction.DEFAULT); | ||
} | ||
|
||
/// @dev Checks if a user account is restricted. Reverts with {ERC20Restricted} if so. | ||
function _checkRestriction(address account) internal view virtual { | ||
require(isUserAllowed(account), ERC20UserRestricted(account)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.26; | ||
|
||
import {IERC7943} from "../../../interfaces/IERC7943.sol"; | ||
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; | ||
import {ERC165, IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; | ||
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; | ||
import {ERC20Freezable} from "./ERC20Freezable.sol"; | ||
import {ERC20Restricted} from "./ERC20Restricted.sol"; | ||
|
||
/** | ||
* @dev Extension of {ERC20} according to https://eips.ethereum.org/EIPS/eip-7943[EIP-7943]. | ||
* | ||
* Combines standard ERC-20 functionality with RWA-specific features like user restrictions, | ||
* asset freezing, and forced asset transfers. | ||
*/ | ||
abstract contract ERC20uRWA is ERC20, ERC165, ERC20Freezable, ERC20Restricted, IERC7943 { | ||
/// @inheritdoc ERC20Restricted | ||
function isUserAllowed(address user) public view virtual override(IERC7943, ERC20Restricted) returns (bool) { | ||
return super.isUserAllowed(user); | ||
} | ||
|
||
/// @inheritdoc ERC165 | ||
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { | ||
return interfaceId == type(IERC7943).interfaceId || super.supportsInterface(interfaceId); | ||
} | ||
|
||
/** | ||
* @dev See {IERC7943-isTransferAllowed}. | ||
* | ||
* CAUTION: This function is only meant for external use. Overriding it will not apply the new checks to | ||
* the internal {_update} function. Consider overriding {_update} accordingly to keep both functions in sync. | ||
*/ | ||
function isTransferAllowed(address from, address to, uint256, uint256 amount) external view virtual returns (bool) { | ||
return (amount <= available(from) && isUserAllowed(from) && isUserAllowed(to)); | ||
} | ||
|
||
/// @inheritdoc IERC7943 | ||
function getFrozen(address user, uint256) public view virtual returns (uint256 amount) { | ||
return frozen(user); | ||
} | ||
|
||
/** | ||
* @dev See {IERC7943-setFrozen}. | ||
* | ||
* NOTE: The `amount` is capped to the balance of the `user` to ensure the {IERC7943-Frozen} event | ||
* emits values that consistently reflect the actual amount of tokens that are frozen. | ||
*/ | ||
function setFrozen(address user, uint256, uint256 amount) public virtual { | ||
uint256 actualAmount = Math.min(amount, balanceOf(user)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ERC does specify this, but I'm not sure I agree. |
||
_checkFreezer(user, actualAmount); | ||
_setFrozen(user, actualAmount); | ||
} | ||
|
||
/** | ||
* @dev See {IERC7943-forceTransfer}. | ||
* | ||
* NOTE: Allows to bypass the freezing mechanism. However, in cases where the balance after | ||
* the transfer is less than the frozen balance, the frozen balance is adjusted to the new balance. | ||
*/ | ||
function forceTransfer(address from, address to, uint256, uint256 amount) public virtual { | ||
_checkEnforcer(from, to, amount); | ||
require(isUserAllowed(to), ERC7943NotAllowedUser(to)); | ||
|
||
// Update frozen balance if needed. ERC-7943 requires that balance is unfrozen first and then send the tokens. | ||
uint256 currentFrozen = frozen(from); | ||
uint256 newBalance; | ||
unchecked { | ||
// Safe because ERC20._update will check that balanceOf(from) >= amount | ||
newBalance = balanceOf(from) - amount; | ||
} | ||
if (currentFrozen > newBalance) { | ||
_setFrozen(from, newBalance); | ||
} | ||
|
||
ERC20._update(from, to, amount); // Explicit raw update to bypass all restrictions | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this result in other effects being skipped? Voting power transfer for example. |
||
emit ForcedTransfer(from, to, 0, amount); | ||
} | ||
|
||
/// @inheritdoc ERC20 | ||
function _update( | ||
address from, | ||
address to, | ||
uint256 amount | ||
) internal virtual override(ERC20, ERC20Freezable, ERC20Restricted) { | ||
// Note: We rely on the inherited _update chain (ERC20Freezable + ERC20Restricted) to enforce | ||
// the same restrictions that isTransferAllowed would check. This avoids duplicate validation | ||
// while maintaining consistency between external queries and internal transfer logic. | ||
super._update(from, to, amount); | ||
} | ||
|
||
/** | ||
* @dev Internal function to check if the `enforcer` is allowed to forcibly transfer the `amount` of `tokens`. | ||
* | ||
* Example usage with {AccessControl-onlyRole}: | ||
* | ||
* ```solidity | ||
* function _checkEnforcer(address from, address to, uint256 amount) internal view override onlyRole(ENFORCER_ROLE) {} | ||
* ``` | ||
*/ | ||
function _checkEnforcer(address from, address to, uint256 amount) internal view virtual; | ||
|
||
/** | ||
* @dev Internal function to check if the `freezer` is allowed to freeze the `amount` of `tokens`. | ||
* | ||
* Example usage with {AccessControl-onlyRole}: | ||
* | ||
* ```solidity | ||
* function _checkFreezer(address user, uint256 amount) internal view override onlyRole(FREEZER_ROLE) {} | ||
* ``` | ||
*/ | ||
function _checkFreezer(address user, uint256 amount) internal view virtual; | ||
} |
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 thoughtsThere 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