This EIP proposes the Universal RWA (uRWA) standard, a set of interfaces for tokenized Real World Assets (RWAs) such as securities, real estate, commodities, or other physical/financial assets on the blockchain.
Real World Assets often require regulatory compliance features not found in standard tokens, including the ability to freeze assets, perform enforcement transfers for legal compliance, and restrict transfers to authorized users. The uRWA standard extends common token standards like ERC-20, ERC-721 or ERC-1155 by introducing essential compliance functions while remaining minimal and not opinionated about specific implementation details.
This enables DeFi protocols and applications to interact with tokenized real-world assets in a standardized way, knowing they can check transfer permissions, whether users are allowed to interact, handle frozen assets appropriately, and integrate with compliant RWA tokens regardless of the underlying asset type or internal compliance logic. It also adopts ERC-165 for introspection.
Real World Assets (RWAs) represent a significant opportunity to bridge traditional finance and decentralized finance (DeFi). By tokenizing assets like real estate, corporate bonds, commodities, art, or securities, we can unlock benefits such as fractional ownership, programmable compliance, enhanced liquidity through secondary markets for traditionally illiquid assets and integration with decentralized protocols.
However, tokenizing real world assets introduces regulatory requirements often absent in purely digital assets, such as allowlists for users, transfer restrictions, asset freezing or law enforcement rules. Existing token standards like ERC-20, ERC-721 and ERC-1155 lack the inherent structure to address these compliance needs directly within the standard itself.
Attempts at defining universal RWA standards historically imposed unnecessary complexity and gas overhead for simpler use cases that do not require the full spectrum of features like granular role-based access control, mandatory on-chain whitelisting, specific on-chain identity solutions, or metadata handling solutions mandated by the standard.
Additionally, the broad spectrum of RWA classes inherently suggests the need to move away from a one-size-fits-all solution. With the purpose in mind of defining an EIP for it, a minimalistic approach, unopinionated features list and maximally compatible design should be kept in mind.
The uRWA standard seeks a more refined balance by defining an essential interface, establishing a common ground for interaction regarding compliance and control, without dictating the underlying implementation mechanisms. This allows core token implementations to remain lean while providing standard functions for RWA-specific interactions.
The final goal is to build composable DeFi around RWAs, providing the same interface when dealing with compliance and regulation.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
The following defines the standard interfaces for an ERC-7943 token contract, which MUST extend from one base token interface such as ERC-20, ERC-721 or ERC-1155:
/// @notice Interface for ERC-20 based implementations.
interface IERC7943Fungible is IERC165 {
/// @notice Emitted when tokens are taken from one address and transferred to another.
/// @param from The address from which tokens were taken.
/// @param to The address to which seized tokens were transferred.
/// @param amount The amount seized.
event ForcedTransfer(address indexed from, address indexed to, uint256 amount);
/// @notice Emitted when `setFrozenTokens` is called, changing the frozen `amount` of tokens for `user`.
/// @param user The address of the user whose tokens are being frozen.
/// @param amount The amount of tokens frozen after the change.
event Frozen(address indexed user, uint256 amount);
/// @notice Error reverted when a user is not allowed to interact.
/// @param account The address of the user which is not allowed for interactions.
error ERC7943NotAllowedUser(address account);
/// @notice Error reverted when a transfer is attempted from `user` with an `amount` less or equal than its balance, but greater than its unfrozen balance.
/// @param user The address holding the tokens.
/// @param amount The amount being transferred.
/// @param unfrozen The amount of tokens that are unfrozen and available to transfer.
error ERC7943InsufficientUnfrozenBalance(address user, uint256 amount, uint256 unfrozen);
/// @notice Takes tokens from one address and transfers them to another.
/// @dev Requires specific authorization. Used for regulatory compliance or recovery scenarios.
/// @param from The address from which `amount` is taken.
/// @param to The address that receives `amount`.
/// @param amount The amount to force transfer.
function forcedTransfer(address from, address to, uint256 amount) external;
/// @notice Changes the frozen status of `amount` tokens belonging to a `user`.
/// This overwrites the current value, similar to an `approve` function.
/// @dev Requires specific authorization. Frozen tokens cannot be transferred by the user.
/// @param user The address of the user whose tokens are to be frozen/unfrozen.
/// @param amount The amount of tokens to freeze/unfreeze.
function setFrozenTokens(address user, uint256 amount) external;
/// @notice Checks if a specific user is allowed to interact according to token rules.
/// @dev This is often used for allowlist/KYC/KYB/AML checks.
/// @param user The address to check.
/// @return allowed True if the user is allowed, false otherwise.
function isUserAllowed(address user) external view returns (bool allowed);
/// @notice Checks the frozen status/amount.
/// @param user The address of the user.
/// @return amount The amount of tokens currently frozen for `user`.
function getFrozenTokens(address user) external view returns (uint256 amount);
/// @notice Checks if a transfer is currently possible according to token rules. It enforces validations on the frozen tokens.
/// @dev This may involve checks like allowlists, blocklists, transfer limits and other policy-defined restrictions.
/// @param from The address sending tokens.
/// @param to The address receiving tokens.
/// @param amount The amount being transferred.
/// @return allowed True if the transfer is allowed, false otherwise.
function canTransfer(address from, address to, uint256 amount) external view returns (bool allowed);
}
/// @notice Interface for ERC-721 based implementations.
interface IERC7943NonFungible is IERC165 {
/// @notice Emitted when `tokenId` is taken from one address and transferred to another.
/// @param from The address from which `tokenId` is taken.
/// @param to The address to which seized `tokenId` is transferred.
/// @param tokenId The ID of the token being transferred.
event ForcedTransfer(address indexed from, address indexed to, uint256 indexed tokenId);
/// @notice Emitted when `setFrozenTokens` is called, changing the frozen status of `tokenId` for `user`.
/// @param user The address of the user whose `tokenId` is subjected to freeze/unfreeze.
/// @param tokenId The ID of the token subjected to freeze/unfreeze.
/// @param frozenStatus Whether `tokenId` has been frozen or unfrozen.
event Frozen(address indexed user, uint256 indexed tokenId, bool indexed frozenStatus);
/// @notice Error reverted when a user is not allowed to interact.
/// @param account The address of the user which is not allowed for interactions.
error ERC7943NotAllowedUser(address account);
/// @notice Error reverted when a transfer is attempted from `user` with a `tokenId` which has been previously frozen.
/// @param user The address holding the tokens.
/// @param tokenId The ID of the token being frozen.
error ERC7943FrozenTokenId(address user, uint256 tokenId);
/// @notice Takes `tokenId` from one address and transfers it to another.
/// @dev Requires specific authorization. Used for regulatory compliance or recovery scenarios.
/// @param from The address from which `tokenId` is taken.
/// @param to The address that receives `tokenId`.
/// @param tokenId The ID of the token being transferred.
function forcedTransfer(address from, address to, uint256 tokenId) external;
/// @notice Changes the frozen status of `tokenId` belonging to a `user`.
/// This overwrites the current value, similar to an `approve` function.
/// @dev Requires specific authorization. Frozen tokens cannot be transferred by the user.
/// @param user The address of the user whose tokens are to be frozen/unfrozen.
/// @param tokenId The ID of the token to freeze/unfreeze.
/// @param frozenStatus whether `tokenId` is being frozen or not.
function setFrozenTokens(address user, uint256 tokenId, bool frozenStatus) external;
/// @notice Checks if a specific user is allowed to interact according to token rules.
/// @dev This is often used for allowlist/KYC/KYB/AML checks.
/// @param user The address to check.
/// @return allowed True if the user is allowed, false otherwise.
function isUserAllowed(address user) external view returns (bool allowed);
/// @notice Checks the frozen status of a specific `tokenId`.
/// @param user The address of the user.
/// @param tokenId The ID of the token.
/// @return frozenStatus Whether `tokenId` is currently frozen for `user`.
function getFrozenTokens(address user, uint256 tokenId) external view returns (bool frozenStatus);
/// @notice Checks if a transfer is currently possible according to token rules. It enforces validations on the frozen tokens.
/// @dev This may involve checks like allowlists, blocklists, transfer limits and other policy-defined restrictions.
/// @param from The address sending tokens.
/// @param to The address receiving tokens.
/// @param tokenId The ID of the token being transferred.
/// @return allowed True if the transfer is allowed, false otherwise.
function canTransfer(address from, address to, uint256 tokenId) external view returns (bool allowed);
}
/// @notice Interface for ERC-1155 based implementations.
interface IERC7943MultiToken is IERC165 {
/// @notice Emitted when tokens are taken from one address and transferred to another.
/// @param from The address from which tokens were taken.
/// @param to The address to which seized tokens were transferred.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount seized.
event ForcedTransfer(address indexed from, address indexed to, uint256 indexed tokenId, uint256 amount);
/// @notice Emitted when `setFrozenTokens` is called, changing the frozen `amount` of `tokenId` tokens for `user`.
/// @param user The address of the user whose tokens are being frozen.
/// @param tokenId The ID of the token being frozen.
/// @param amount The amount of tokens frozen after the change.
event Frozen(address indexed user, uint256 indexed tokenId, uint256 amount);
/// @notice Error reverted when a user is not allowed to interact.
/// @param account The address of the user which is not allowed for interactions.
error ERC7943NotAllowedUser(address account);
/// @notice Error reverted when a transfer is attempted from `user` with an `amount` of `tokenId` less or equal than its balance, but greater than its unfrozen balance.
/// @param user The address holding the tokens.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount being transferred.
/// @param unfrozen The amount of tokens that are unfrozen and available to transfer.
error ERC7943InsufficientUnfrozenBalance(address user, uint256 tokenId, uint256 amount, uint256 unfrozen);
/// @notice Takes tokens from one address and transfers them to another.
/// @dev Requires specific authorization. Used for regulatory compliance or recovery scenarios.
/// @param from The address from which `amount` is taken.
/// @param to The address that receives `amount`.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount to force transfer.
function forcedTransfer(address from, address to, uint256 tokenId, uint256 amount) external;
/// @notice Changes the frozen status of `amount` of `tokenId` tokens belonging to a `user`.
/// This overwrites the current value, similar to an `approve` function.
/// @dev Requires specific authorization. Frozen tokens cannot be transferred by the user.
/// @param user The address of the user whose tokens are to be frozen/unfrozen.
/// @param tokenId The ID of the token to freeze/unfreeze.
/// @param amount The amount of tokens to freeze/unfreeze.
function setFrozenTokens(address user, uint256 tokenId, uint256 amount) external;
/// @notice Checks if a specific user is allowed to interact according to token rules.
/// @dev This is often used for allowlist/KYC/KYB/AML checks.
/// @param user The address to check.
/// @return allowed True if the user is allowed, false otherwise.
function isUserAllowed(address user) external view returns (bool allowed);
/// @notice Checks the frozen status/amount of a specific `tokenId`.
/// @param user The address of the user.
/// @param tokenId The ID of the token.
/// @return amount The amount of `tokenId` tokens currently frozen for `user`.
function getFrozenTokens(address user, uint256 tokenId) external view returns (uint256 amount);
/// @notice Checks if a transfer is currently possible according to token rules. It enforces validations on the frozen tokens.
/// @dev This may involve checks like allowlists, blocklists, transfer limits and other policy-defined restrictions.
/// @param from The address sending tokens.
/// @param to The address receiving tokens.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount being transferred.
/// @return allowed True if the transfer is allowed, false otherwise.
function canTransfer(address from, address to, uint256 tokenId, uint256 amount) external view returns (bool allowed);
}
isUserAllowed
, canTransfer
and getFrozenTokens
These provide views into the implementing contract's compliance, transfer policy logic and freezing status. These functions:
msg.sender
).canTransfer
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 SHOULD perform an isUserAllowed
check on the from
and to
parameters. An important documentation note is that ERC-3643 doesn't perform an isUserAllowed
check within canTransfer
as optionally suggested.canTransfer
should return false if the contract is paused.forcedTransfer
This function provides a standard mechanism for forcing a transfer from a from
to a to
address. The function:
from
to to
either by transferring or burning from from
and minting to to
.ForcedTransfer
event.Frozen
event before the underlying base token transfer event reflecting the change. Having the unfrozen amount changed before the actual transfer is critical for tokens that might be susceptible to reentrancy attacks doing external checks on recipients as it is the case for ERC-721 and ERC-1155 tokens. canTransfer
.isUserAllowed
check on the to
parameter.setFrozenTokens
It provides a way to freeze or unfreeze assets held by a specific user. This is useful for temporary lock mechanisms. This function:
Frozen
event.The contract MUST implement the ERC-165 supportsInterface
function and MUST return true for the bytes4
value (representing the interfaceId
):
- 0x20e30a04
for the fungible interface.
- 0xa1264b3e
for the non-fungible interface.
- 0x5ffc456d
for the multi token interface.
Implementations of these interfaces MUST implement the necessary functions of their chosen base standard (e.g., ERC-20 for the fungible interface, ERC-721 for the non fungible interface and ERC-1155 for the multi token interface) and MUST also restrict access to sensitive functions like forcedTransfer
and setFrozenTokens
using an appropriate access control mechanism (e.g., onlyOwner
, Role-Based Access Control). The specific mechanism is NOT mandated by this interface standard.
Implementations MUST ensure their transfer methods exhibit the following behavior:
transfer
, transferFrom
, safeTransferFrom
, etc.) MUST NOT succeed in cases in which canTransfer
or isUserAllowed
would return false
for either one or both from
and to
addresses.isUserAllowed
would return false
.canTransfer
or isUserAllowed
checks on the token holder. It MAY be restricted to prevent burning more assets than the unfrozen amount (e.g., in public burning functions). It MAY burn more assets than the unfrozen amount (e.g., in permissioned burning functions), in which case the contract MUST update the frozen status accordingly and emit a Frozen
event before the underlying base token transfer event.The ERC7943NotAllowedUser
error CAN be used as a general revert mechanism whenever internal calls to isUserAllowed
return false. It MAY NOT be used or MAY be replaced by more specific errors depending on the custom checks performed inside those calls.
In general, the standard prioritizes error specificity, meaning that specific errors such as ERC7943InsufficientUnfrozenBalance
/ERC7943FrozenTokenId
SHOULD be thrown when applicable. The ERC7943InsufficientUnfrozenBalance
/ERC7943FrozenTokenId
error SHOULD be triggered when a transfer is attempted from user
with an amount
less than or equal to its balance, but greater than its unfrozen balance or with a tokenId
which is currently frozen. If the amount
is greater than the whole balance or the tokenId
is not owned by the user
, unrelated from the frozen amount, more specific errors from the base standard SHOULD be used instead.
forcedTransfer
, setFrozenTokens
, isUserAllowed
, canTransfer
, getFrozenTokens
) and associated events/errors needed for common RWA compliance and control patterns, avoiding mandated complexity or opinionated features. The reason to introduce specific errors (ERC7943NotAllowedUser
and ERC7943InsufficientUnfrozenBalance
) is to provide completeness with the introduced functionalities (isUserAllowed
and getFrozenTokens
). As dictated in the specifications, error specificity is prioritized, leaving space for implementations to accommodate more explicit errors. Regarding the events Frozen
and ForcedTransfer
, the reason for their existence is to signal uncommon transfers (like in forcedTransfer
) but also to help off-chain indexers to correctly keep track and account for asset seizures and freezing. As mentioned in the specifications, special attention should be paid to the order in which these events are emitted in relation to the base token contract events.isUserAllowed
, canTransfer
, getFrozenTokens
) for compliance checks without dictating how those checks are implemented internally by the token contract. This allows diverse compliance strategies.forcedTransfer
and setFrozenTokens
as standard functions, acknowledging its importance for regulatory enforcement in the RWA space, distinct from standard transfers. Mandates access control for this sensitive function.As an example, an AMM pool or a lending protocol can integrate with ERC-7943 based ERC-20 tokens by calling isUserAllowed
or canTransfer
to handle these assets in a compliant manner. Enforcement actions like forcedTransfer
and setFrozenTokens
can either be called by third party entities or be integrated by external protocols to allow for automated and programmable compliance. Users can then expand these tokens with additional features to fit the specific needs of individual asset types, either with on-chain identity systems, historical balances tracking for dividend distributions, semi-fungibility with token metadata, and other custom functionalities.
While this ERC provides the necessary primitives for regulated assets, any additional feature can be added through extensions. As an example, if for any administrative function like setFrozenTokens
a valid legal proof must be provided and attached to the call, the contract can have a function that batches operations like:
contract ComplianceOperator is IERC7943MultiToken {
...
function setFrozenTokensWithProof(address user, uint256 tokenId, uint256 amount, bytes calldata legalProof) public {
/// do anything with `legalProof`
token.setFrozenToken(user, tokenId, amount);
}
}
Alternatively, developers can also perform several operations through the use of multicall patterns similar to the one defined in ERC-6357 so that a mix of the given primitives with additional features can be batched in one transaction. Finally, functionalities like pausability can be added on top, either through the use of modifiers or directly within functions implementations.
The naming conventions in this ERC were carefully chosen to establish clarity and semantic consistency within the broader RWA ecosystem while maintaining neutrality and broad applicability.
forcedTransfer
: This term was selected for its neutrality. While names like "confiscation," "revocation," or "recovery" describe specific motivations, forcedTransfer
purely denotes the direct action of transferring assets, irrespective of the underlying reason. This name was adopted from ERC-3643 for consistency across RWA specifications.canTransfer
: This name was chosen for consistency with established RWA standards including ERC-3643 and ERC-7518. This alignment promotes interoperability and reduces cognitive overhead when working across different RWA tokens.setFrozenTokens
/ getFrozenTokens
: These names were chosen for managing transfer restrictions and align with ERC-3643 naming patterns.setFrozenTokens
function (which overwrites the frozen asset quantity) and one Frozen
event were favored over distinct freeze
/unfreeze
functions and events.ERC7943InsufficientUnfrozenBalance
: Discussions around "insufficient" being similar to "unavailable" arose, where "unavailable" might have better suggested a temporal condition like a freezing status. However the term "available"/ "unavailable" was also overlapping with "frozen" / "unfrozen" creating more confusion and duality. Finally, coupling "insufficient" with the specified "unfrozen balance" better represents the domain, prefix and subject of the error, according to ERC-6093 guidelines.This EIP defines a new interface standard and does not alter existing ones like ERC-20, ERC-721 and ERC-1155. Standard wallets and explorers can interact with the base token functionality of implementing contracts, subject to the rules enforced by that contract's implementation of isUserAllowed
, canTransfer
and getFrozenTokens
functions. Full support for the ERC-7943 functions requires explicit integration.
Examples of basic implementation for ERC-20, ERC-721 and ERC-1155 which include a basic whitelist for users and an enumerable role based access control:
pragma solidity ^0.8.29;
/* required imports ... */
contract uRWA20 is Context, ERC20, AccessControlEnumerable, IERC7943Fungible {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
bytes32 public constant ENFORCER_ROLE = keccak256("ENFORCER_ROLE");
bytes32 public constant WHITELIST_ROLE = keccak256("WHITELIST_ROLE");
mapping(address user => bool whitelisted) public isWhitelisted;
mapping(address user => uint256 amount) internal _frozenTokens;
event Whitelisted(address indexed account, bool status);
error NotZeroAddress();
constructor(string memory name, string memory symbol, address initialAdmin) ERC20(name, symbol) {
/* give initialAdmin necessary roles ...*/
}
function canTransfer(address from, address to, uint256 amount) public virtual view returns (bool allowed) {
if (balanceOf(from) < _frozenTokens[from]) return;
if (amount > balanceOf(from) - _frozenTokens[from]) return;
if (!isUserAllowed(from) || !isUserAllowed(to)) return;
allowed = true;
}
function isUserAllowed(address user) public virtual view returns (bool allowed) {
if (isWhitelisted[user]) allowed = true;
}
function getFrozenTokens(address user) external view returns (uint256 amount) {
amount = _frozenTokens[user];
}
function changeWhitelist(address account, bool status) external onlyRole(WHITELIST_ROLE) {
require(account != address(0), NotZeroAddress());
isWhitelisted[account] = status;
emit Whitelisted(account, status);
}
/* standard mint and burn functions with access control ...*/
function setFrozenTokens(address user, uint256 amount) public onlyRole(ENFORCER_ROLE) {
_frozenTokens[user] = amount;
emit Frozen(user, amount);
}
function forcedTransfer(address from, address to, uint256 amount) public onlyRole(ENFORCER_ROLE) {
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
_excessFrozenUpdate(from, amount);
super._update(from, to, amount);
emit ForcedTransfer(from, to, amount);
}
function _excessFrozenUpdate(address user, uint256 amount) internal {
uint256 unfrozenBalance = _unfrozenBalance(user);
if(amount > unfrozenBalance && amount <= balanceOf(user)) {
// Protect from underflow: if amount > balanceOf(user) the call will revert in super._update with insufficient balance error
_frozenTokens[user] -= amount - unfrozenBalance; // Reduce by excess amount
emit Frozen(user, _frozenTokens[user]);
}
}
function _unfrozenBalance(address user) internal view returns(uint256 unfrozenBalance) {
unfrozenBalance = balanceOf(user) < _frozenTokens[user] ? 0 : balanceOf(user) - _frozenTokens[user];
}
function _update(address from, address to, uint256 amount) internal virtual override {
if (from != address(0) && to != address(0)) { // Transfer
require(amount <= balanceOf(from), IERC20Errors.ERC20InsufficientBalance(from, balanceOf(from), amount));
uint256 unfrozenBalance = _unfrozenBalance(from);
require(amount <= unfrozenBalance, ERC7943InsufficientUnfrozenBalance(from, amount, unfrozenBalance));
require(canTransfer(from, to, amount), "ERC7943: transfer not allowed");
} else if (from == address(0)) { // Mint
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
} else { // Burn
_excessFrozenUpdate(from, amount);
}
super._update(from, to, amount);
}
function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlEnumerable, IERC165) returns (bool) {
return interfaceId == type(IERC7943Fungible).interfaceId ||
interfaceId == type(IERC20).interfaceId ||
super.supportsInterface(interfaceId);
}
}
pragma solidity ^0.8.29;
/* required imports ... */
contract uRWA721 is Context, ERC721, AccessControlEnumerable, IERC7943NonFungible {
/* same definitions, constructor and changeWhitelist function as before ...*/
mapping(address user => mapping(uint256 tokenId => bool frozen)) internal _frozenTokens;
function isUserAllowed(address user) public view virtual override returns (bool allowed) {
if (isWhitelisted[user]) allowed = true;
}
function canTransfer(address from, address to, uint256 tokenId) public view virtual override returns (bool allowed) {
address owner = _ownerOf(tokenId);
if (owner != from || owner == address(0)) return;
if (!isUserAllowed(from) || !isUserAllowed(to)) return;
if (_frozenTokens[from][tokenId]) return;
allowed = true;
}
function getFrozenTokens(address user, uint256 tokenId) external view returns (bool frozenStatus) {
frozenStatus = _frozenTokens[user][tokenId];
}
function setFrozenTokens(address user, uint256 tokenId, bool frozenStatus) public onlyRole(ENFORCER_ROLE) {
require(user == ownerOf(tokenId), IERC721Errors.ERC721InvalidOwner(user));
_frozenTokens[user][tokenId] = frozenStatus;
emit Frozen(user, tokenId, frozenStatus);
}
function forcedTransfer(address from, address to, uint256 tokenId) public virtual override onlyRole(ENFORCER_ROLE) {
require(to != address(0), ERC721InvalidReceiver(address(0)));
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
_excessFrozenUpdate(from , tokenId);
super._update(to, tokenId, address(0)); // Skip _update override
ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, "");
emit ForcedTransfer(from, to, tokenId);
}
function _excessFrozenUpdate(address from, uint256 tokenId) internal {
_validateCorrectOwner(from, tokenId);
if(_frozenTokens[from][tokenId]) {
_frozenTokens[from][tokenId] = false; // Unfreeze the token if it was frozen
emit Frozen(from, tokenId, false);
}
}
function _validateCorrectOwner(address claimant, uint256 tokenId) internal view {
address currentOwner = ownerOf(tokenId);
require(currentOwner == claimant, ERC721IncorrectOwner(claimant, tokenId, currentOwner));
}
/* standard mint function with access control ...*/
function burn(uint256 tokenId) external virtual onlyRole(BURNER_ROLE) {
address previousOwner = _update(address(0), tokenId, _msgSender());
if (previousOwner == address(0)) revert ERC721NonexistentToken(tokenId);
}
function _update(address to, uint256 tokenId, address auth) internal virtual override returns(address) {
address from = _ownerOf(tokenId);
if (auth != address(0)) {
_checkAuthorized(from, auth, tokenId);
}
if (from != address(0) && to != address(0)) { // Transfer
_validateCorrectOwner(from, tokenId);
require(!_frozenTokens[from][tokenId], ERC7943FrozenTokenId(from, tokenId));
require(canTransfer(from, to, tokenId), "ERC7943: transfer not allowed");
} else if (from == address(0)) { // Mint
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
} else { // Burn
_excessFrozenUpdate(from, tokenId);
}
return super._update(to, tokenId, auth);
}
function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlEnumerable, ERC721, IERC165) returns (bool) {
return interfaceId == type(IERC7943NonFungible).interfaceId ||
super.supportsInterface(interfaceId);
}
}
pragma solidity ^0.8.29;
/* required imports ... */
contract uRWA1155 is Context, ERC1155, AccessControlEnumerable, IERC7943MultiToken {
/* same definitions, constructor and changeWhitelist function as before ...*/
mapping(address user => mapping(uint256 tokenId => uint256 amount)) internal _frozenTokens;
function canTransfer(address from, address to, uint256 tokenId, uint256 amount) public view virtual override returns (bool allowed) {
if (balanceOf(from, tokenId) < _frozenTokens[from][tokenId]) return;
if (balanceOf(from, tokenId) < amount) return;
if (!isUserAllowed(from) || !isUserAllowed(to)) return;
if (amount > balanceOf(from, tokenId) - _frozenTokens[from][tokenId]) return;
allowed = true;
}
function isUserAllowed(address user) public view virtual override returns (bool allowed) {
if (isWhitelisted[user]) allowed = true;
}
function getFrozenTokens(address user, uint256 tokenId) external view returns (uint256 amount) {
amount = _frozenTokens[user][tokenId];
}
function setFrozenTokens(address user, uint256 tokenId, uint256 amount) public onlyRole(ENFORCER_ROLE) {
_frozenTokens[user][tokenId] = amount;
emit Frozen(user, tokenId, amount);
}
function forcedTransfer(address from, address to, uint256 tokenId, uint256 amount) public onlyRole(ENFORCER_ROLE) {
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
// Reimplementing _safeTransferFrom to avoid the check on _update
if (to == address(0)) {
revert ERC1155InvalidReceiver(address(0));
}
if (from == address(0)) {
revert ERC1155InvalidSender(address(0));
}
_excessFrozenUpdate(from, tokenId, amount);
uint256[] memory ids = new uint256[](1);
uint256[] memory values = new uint256[](1);
ids[0] = tokenId;
values[0] = amount;
super._update(from, to, ids, values);
if (to != address(0)) {
address operator = _msgSender();
if (ids.length == 1) {
uint256 id = ids[0];
uint256 value = values[0];
ERC1155Utils.checkOnERC1155Received(operator, from, to, id, value, "");
} else {
ERC1155Utils.checkOnERC1155BatchReceived(operator, from, to, ids, values, "");
}
}
emit ForcedTransfer(from, to, tokenId, amount);
}
function _excessFrozenUpdate(address user, uint256 tokenId, uint256 amount) internal {
uint256 unfrozenBalance = _unfrozenBalance(user, tokenId);
if(amount > unfrozenBalance && amount <= balanceOf(user, tokenId)) {
// Protect from underflow: if amount > balanceOf(user) the call will revert in super._update with insufficient balance error
_frozenTokens[user][tokenId] -= amount - unfrozenBalance; // Reduce by excess amount
emit Frozen(user, tokenId, _frozenTokens[user][tokenId]);
}
}
/* standard mint and burn functions with access control ...*/
function _unfrozenBalance(address user, uint256 tokenId) internal view returns(uint256 unfrozenBalance) {
unfrozenBalance = balanceOf(user, tokenId) < _frozenTokens[user][tokenId] ? 0 : balanceOf(user, tokenId) - _frozenTokens[user][tokenId];
}
function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal virtual override {
if (ids.length != values.length) {
revert ERC1155InvalidArrayLength(ids.length, values.length);
}
if (from != address(0) && to != address(0)) { // Transfer
for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids[i];
uint256 value = values[i];
require(value <= balanceOf(from, id), ERC1155InsufficientBalance(from, balanceOf(from, id), value, id));
uint256 unfrozenBalance = _unfrozenBalance(from, id);
require(value <= unfrozenBalance, ERC7943InsufficientUnfrozenBalance(from, id, value, unfrozenBalance));
require(canTransfer(from, to, id, value), "ERC7943: transfer not allowed");
}
}
if (from == address(0)) { // Mint
require(isUserAllowed(to), ERC7943NotAllowedUser(to));
} else if (to == address(0)) { // Burn
for (uint256 j = 0; j < ids.length; ++j) {
_excessFrozenUpdate(from, ids[j], values[j]);
}
}
super._update(from, to, ids, values);
}
function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControlEnumerable, ERC1155, IERC165) returns (bool) {
return interfaceId == type(IERC7943MultiToken).interfaceId ||
super.supportsInterface(interfaceId);
}
}
forcedTransfer
and setFrozenTokens
: The security of the mechanism chosen by the implementer to restrict access to these functions is paramount. Unauthorized access could lead to asset theft. Secure patterns (multisig, timelocks) are highly recommended. setFrozenTokens
function: The setFrozenTokens
function might be susceptible to front-running, similar to the approve
function of the ERC-20. If the suggestion of allowing freezing more than what an account owns is not followed, a front-run might be an incentive to an account to avoid any attempt of freezing its balance. Additional features to gradually increment or decrement the frozen status CAN be considered for implementation.Copyright and related rights waived via CC0.