Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 132 additions & 26 deletions contracts/interfaces/IERC7943.sol
Original file line number Diff line number Diff line change
@@ -1,21 +1,134 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
pragma solidity ^0.8.26;

import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";

/// @notice Defines the ERC-7943 interface, the uRWA.
/// When interacting with specific token standards:
/// - For ERC-721 like (non-fungible) tokens 'amount' parameters typically represent a single token (i.e., 1).
/// - For ERC-20 like (fungible) tokens, 'tokenId' parameters are generally not applicable and should be set to 0.
interface IERC7943 is IERC165 {
/// @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 tokenId, uint256 amount);
event ForcedTransfer(address indexed from, address indexed to, uint256 indexed tokenId, uint256 amount);

/// @notice Emitted when `setFrozen` is called, changing the frozen `amount` of `tokenId` tokens for `user`.
/// @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.
Expand All @@ -25,13 +138,6 @@ interface IERC7943 is IERC165 {
/// @param account The address of the user which is not allowed for interactions.
error ERC7943NotAllowedUser(address account);

/// @notice Error reverted when a transfer is not allowed due to restrictions in place.
/// @param from The address from which tokens are being transferred.
/// @param to The address to which tokens are being transferred.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount being transferred.
error ERC7943NotAllowedTransfer(address from, address to, uint256 tokenId, uint256 amount);

/// @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.
Expand All @@ -45,21 +151,27 @@ interface IERC7943 is IERC165 {
/// @param to The address that receives `amount`.
/// @param tokenId The ID of the token being transferred.
/// @param amount The amount to force transfer.
function forceTransfer(address from, address to, uint256 tokenId, uint256 amount) external;
function forcedTransfer(address from, address to, uint256 tokenId, uint256 amount) external;

/// @notice Changes the frozen status of `amount` of `tokenId` tokens belonging to an `user`.
/// @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 setFrozen(address user, uint256 tokenId, uint256 amount) external;
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 getFrozen(address user, uint256 tokenId) external view returns (uint256 amount);
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.
Expand All @@ -68,16 +180,10 @@ interface IERC7943 is IERC165 {
/// @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 isTransferAllowed(
function canTransfer(
address from,
address to,
uint256 tokenId,
uint256 amount
) external view returns (bool allowed);

/// @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);
}
4 changes: 2 additions & 2 deletions contracts/token/ERC20/extensions/ERC20Freezable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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";
import {IERC7943Fungible} from "../../../interfaces/IERC7943.sol";

/**
* @dev Extension of {ERC20} that allows to implement a freezing
Expand Down Expand Up @@ -37,7 +37,7 @@ abstract contract ERC20Freezable is ERC20 {
/// @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);
emit IERC7943Fungible.Frozen(user, amount);
}

/**
Expand Down
32 changes: 17 additions & 15 deletions contracts/token/ERC20/extensions/ERC20uRWA.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import {IERC7943} from "../../../interfaces/IERC7943.sol";
import {IERC7943Fungible} 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";
Expand All @@ -14,46 +14,48 @@ import {ERC20Restricted} from "./ERC20Restricted.sol";
* 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 {
abstract contract ERC20uRWA is ERC20, ERC165, ERC20Freezable, ERC20Restricted, IERC7943Fungible {
/// @inheritdoc ERC20Restricted
function isUserAllowed(address user) public view virtual override(IERC7943, ERC20Restricted) returns (bool) {
function isUserAllowed(
address user
) public view virtual override(IERC7943Fungible, 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);
return interfaceId == type(IERC7943Fungible).interfaceId || super.supportsInterface(interfaceId);
}

/**
* @dev See {IERC7943-isTransferAllowed}.
* @dev See {IERC7943Fungible-canTransfer}.
*
* 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) {
function canTransfer(address from, address to, 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) {
/// @inheritdoc IERC7943Fungible
function getFrozenTokens(address user) public view virtual returns (uint256 amount) {
return frozen(user);
}

/**
* @dev See {IERC7943-setFrozen}.
* @dev See {IERC7943Fungible-setFrozenTokens}.
*
* NOTE: The `amount` is capped to the balance of the `user` to ensure the {IERC7943-Frozen} event
* NOTE: The `amount` is capped to the balance of the `user` to ensure the {IERC7943Fungible-Frozen} event
* emits values that consistently reflect the actual amount of tokens that are frozen.
*/
function setFrozen(address user, uint256, uint256 amount) public virtual {
function setFrozenTokens(address user, uint256 amount) public virtual {
uint256 actualAmount = Math.min(amount, balanceOf(user));
_checkFreezer(user, actualAmount);
_setFrozen(user, actualAmount);
}

/**
* @dev See {IERC7943-forceTransfer}.
* @dev See {IERC7943Fungible-forcedTransfer}.
*
* Bypasses the {ERC20Restricted} restrictions for the `from` address and adjusts the frozen balance
* to the new balance after the transfer.
Expand All @@ -63,7 +65,7 @@ abstract contract ERC20uRWA is ERC20, ERC165, ERC20Freezable, ERC20Restricted, I
* to add additional restrictions or logic, those changes will also apply here.
* Consider overriding this function to bypass newer restrictions if needed.
*/
function forceTransfer(address from, address to, uint256, uint256 amount) public virtual {
function forcedTransfer(address from, address to, uint256 amount) public virtual {
_checkEnforcer(from, to, amount);
require(isUserAllowed(to), ERC7943NotAllowedUser(to));

Expand All @@ -80,14 +82,14 @@ abstract contract ERC20uRWA is ERC20, ERC165, ERC20Freezable, ERC20Restricted, I

// Temporarily bypass restrictions rather than calling ERC20._update directly.
// This preserves any side effects from future overrides to _update.
// Assuming `forceTransfer` will be used occasionally, the added costs of temporary
// Assuming `forcedTransfer` will be used occasionally, the added costs of temporary
// restrictions would be justifiable under this path.
Restriction restriction = getRestriction(from);
bool wasUserAllowed = isUserAllowed(from);
if (!wasUserAllowed) _setRestriction(from, Restriction.ALLOWED);
_update(from, to, amount); // Explicit raw update to bypass all restrictions
if (!wasUserAllowed) _setRestriction(from, restriction);
emit ForcedTransfer(from, to, 0, amount);
emit ForcedTransfer(from, to, amount);
}

/// @inheritdoc ERC20
Expand Down
2 changes: 1 addition & 1 deletion test/token/ERC20/extensions/ERC20Freezable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('ERC20Freezable', function () {
const frozenAmount = 40n;
await expect(this.token.$_setFrozen(this.holder, frozenAmount))
.to.emit(this.token, 'Frozen')
.withArgs(this.holder, 0, frozenAmount);
.withArgs(this.holder, frozenAmount);
});
});

Expand Down
Loading