ERC-7943: Universal RWA Interface (uRWA)

The similarity with approve makes sense up to a certain limit.

It is true that increase/decrease allowance prevented the famous approve front-run issue. But it never showed to be a practical case and that’s why I believe OZ decided to remove those functions.

However the case with frozen tokens is different.

  • The incentive to front-run an approve was to double spend an approved amount.
  • The incentive to front-run a setFrozenTokens MIGHT be to make the setFrozenTokens to revert if the token doesn’t allow to freeze more assets than what is hold by the user.

The incentives are different even if the use-case and feature are similar.

In the case in which a 7943 does not allow to freeze more assets than what the user holds (which is allowed by the spec, even if not enforced) then the following can happen:

  • User A has 100 tokens.
  • 7943 admin decides to freeze all 100 tokens of user A and sends a setFrozenTokens transaction to freeze all of 100.
  • User A front-runs that by moving 1 token out to another wallet. Now user A has 99 tokens.
  • The setFrozenTokens transaction goes through and it reverts because of the assumption that the token doesn’t allow freezing more assets than what the user holds.

This effectivelys creates a DoS on the freezing feature, which doesn’t happen in the parallel case of an approve.

In summary, because of that I believe that setFrozenTokens might benefit from always acting as a delta increment/decrement as default behaviour.

Wdyt ?

1 Like

IMO delta is more appropriate.

The another related questions are:

  • name of the function should be something else than “set…”

  • parameters - int256 vs uint256 + direction signal

You mentioned frontrunning to avoid freezing. This is another reason why it would be wise to separate the freezing functions into separate ERC as the implementations incl. ABI will need to evolve.

If you are strongly considering the case other specialised smart contracts to call these functions the more important is to add that extending generic “calldata data” to pass some auxiliary data through. (not only relevant for that mentioned case of legalProof, but rather general for smart contracts integrated interactions)

Nevertheless there will be these future outcomes:

A) if state changing functions are bundled - some projects will deviate from 7943, which will naturally lead to lighter version
example - xERC20 that is too heavy with “wrapping” lockbox

B) if state changing functions are in the separate ERC, that is mutually linked with 7943, the automated systems will follow the extension

From our project’s perspective - we can be almost immediately compliant with read only parts - and even there it is already enhancing the concept of freezing partial amount (vs. currently used whole account in standing implementations like USDT/USDC). That is already a significant improvement.
I am not sure though about a strong compatibility with state changing functions.

Wrt mentioned 3643 TREX - I consider that one as specialised ERC for security tokens, while 7943 can be applied for much broader set of assets - RWAs including stablecoins.

setFrozenTokens delta updates

Given that token freezing will be a permissioned function, I don’t think there’s a big concern about race conditions:

  • User has 500 frozen tokens.
  • Admin A wants to freeze 100 extra tokens, so calls an update to 600.
  • Simultaneously, admin B wants to unfreeze 100 tokens, so calls an update to 400.

We can fix that very edge case by having delta updates, but then we lose idempotency.

I believe idempotency, simplicity, and ERC20-like behaviour (approve vs increaseAllowance/decreaseAllowance) are more important.

I also don’t think that the front-running issue can be solved with it.

  • User A has 100 tokens.
  • 7943 admin decides to freeze all 100 tokens of user A and sends a setFrozenTokens transaction to freeze all of 100.
  • User A front-runs that by moving 1 token out to another wallet. Now user A has 99 tokens.
  • The setFrozenTokens transaction goes through and it reverts because of the assumption that the token doesn’t allow freezing more assets than what the user holds.

If the contract reverts trying to freeze a higher token amount than the user holds, it doesn’t make a difference whether updates are absolute or delta based.

A similar problem cannot be solved either: the user front-runs the admin by sending their full balance to another available address or doing a swap. This second issue has higher specificity though, as it’s intrinsically more difficult to execute if whitelists are in place.


Freezing more tokens than available

I believe we want to update the spec for getFrozenTokens if we allow setFrozenTokens to freeze an amount higher than the user’s balance, as we currently do.

Otherwise, one could assume that if a user has an X frozen tokens balance, they must also hold an underlying X tokens balance.

    /// @notice Checks the frozen status/amount.
    /// @param user The address of the user.
    /// @dev It could return an amount higher than the user's balance.
    /// @return amount The amount of tokens currently frozen for `user`.
    function getFrozenTokens(address user) external view returns (uint256 amount);

    /// @notice Checks the frozen status of a specific `tokenId`.
    /// @dev It could return true even if user does not hold the token.
    /// @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 the frozen status/amount of a specific `tokenId`.
    /// @dev It could return an amount higher than the user's balance.
    /// @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);
  • getFrozenTokens CAN return an amount higher than the user’s balance due to setFrozenTokens allowing to freeze tokens exceeding user holdings. In ERC721 tokens, it CAN return true even if the user does not hold the token.

setFrozenTokens return type

I don’t think there’s any strong functional reason to return true on success since we’re reverting on failure.

Nonetheless, I’m all for consistency.


isUserAllowed rename to isVerified

I’m ok with it. I don’t really have a strong opinion here.

Indeed. Or freezing action can freeze up to the given amount. E.g. for this case:

User A has 100 tokens and set approval for 50 tokens to contract B for some automation.
Issuer is requested to freeze 75 tokens. Issuer disclaims to the requester that it can be set up to 75 tokens as some can be moved before the request transaction is finalised.

Indeed, contract B executed the transfer of 30 tokens in the previous block before the issuer’s freezing instruction.

  • if it is up to 75 tokens, then 70 tokens are frozen
  • if it is exactly 75 tokens, tx reverts.

One can assume the requester intents to freeze whatever there is up to the given level.

For setFrozenTokens:

Using signed integers can confuse integrators because sometimes the value can mean an “absolute” value and sometimes a “delta” value. Most tooling usually expects uint256 when dealing with amounts.

I don’t think the DoS on the freezing feature is a plausible scenario. Even if something like that happens, there are a few ways to solve this using one or more of the other permissioned functions.

Thanks @mihaic195 @radek @tinom9 for your comments !

Let me summarize the feedback here :slight_smile:


isUserAllowed renaming

It seems that isVerified seems “too narrow” which I agree with (I believe “verified” restricts the scope of the function) and that “isUserAllowed” is also a bit too specific (is it user? or a contract? or both?).

After thinking a lot about it, maybe a good compromise here is “canInteract” that accept both users and contracts and doesn’t restrict to “verified” only purpose, it would just signal whether an address can interact with the contract. This would be also consistent with “canTransfer”.

Considering backward compatibility with 7734 and 3643:

  • 3643 call’s isVerified internally and doesn’t expose this at token interface level so 3643 wouldn’t be incompatible and there wouldn’t be any overlap. Any 3643 token can implement a canInteract getter that internally calls its identity registry at the isVerified function.
  • 7734 has isVerified as member of a struct which again is not part of the public facing interface so again any 7734 token can implement an canInteract method and be compatible with no extra efforts.

Alternative that I see is also “isAllowed”


On extending state changing functions with calldata parameter

I still believe this would definetely break backward compatibility, expecially on the forcedTransfer method. 3643 has no extra parameter and if we add that, we will then lose compatibility. I wouldn’t go for it. We discussed extensively already on how one can implement custom functions outside the ERC to accomodate for that.


On separating state changing functions on separate interfaces

I see that this can boost flexibility but it can also make the ERC overly complex. We went already from one interface to 3 interfaces. Splitting now to state changing only interfaces would require going from 3 to 6 interfaces (3 read-only, 3 state changing ones):

interface IERC7943FungibleRead {}
interface IERC7943NonFungibleRead {}
interface IERC7943MultiTokenRead {}
interface IERC7943FungibleWrite {}
interface IERC7943NonFungibleWrite {}
interface IERC7943MultiTokenWrite {}

How do you feel about this @MASDXI @ernestognw @tinom9 @mihaic195 ? I know @radek is already up for that.


On setFrozenTokens behaviour

On this by @tinom9

I believe we want to update the spec for getFrozenTokens if we allow setFrozenTokens to freeze an amount higher than the user’s balance, as we currently do.

Yes, that’s a good catch, will include this in the next batch of changes once we come up with an agreement.

About this by @mihaic195

Using signed integers can confuse integrators because sometimes the value can mean an “absolute” value and sometimes a “delta” value. Most tooling usually expects uint256 when dealing with amounts.

I don’t think the DoS on the freezing feature is a plausible scenario. Even if something like that happens, there are a few ways to solve this using one or more of the other permissioned functions.

I agree indeed that it makes things more complex, however I still don’t see how you can avoid the DoS mentioned in my example ? In which ways are you thinkiing to solve this ? One I can think of is:

  • The admin first blacklists
  • Then freeze
  • Eventually remove from blacklist ?

Because that would assume that the same admin has both powers.

I agree also with @tinom9 that we lose idempotency. My conclusion is probably that the only way is to discourage freezing up to user balance since if this limitation is in place, a lot of race conditions can emerge.

Are you all ok in defaulting back to use absolute values but to also discourage (not really prohibit) the limitation of freezing up to user’s balance and instead encourage leaving it open to freeze more than that ?


I will ignore that opened pull request and start fresh once we finalize this open threads. Also, I blieve these are the only points left for discussion and we can move soon to finalize this.

Thanks again for all your efforts so far !

1 Like

A nice summary @xaler thx!

Wrt interfaces - from the perspective of the token creator - I would just select 1 or 2 out of those 6 that are relevant. Combining several interfaces for the token contract is common.

Not sure about the other perspectives you mentioned (indexers, …). But IMO the more granular / atomic the easier / modular implementations.

Ad signed integers - I agree with @mihaic195 it is better to have a consistent uint256. As I mentioned, the delta direction can be signalled by the extra parameter.

Wrt idempotency - I do not understand you are considering this should be idempotent at all. My understanding is that freezing of certain amount is similar to ERC20.transfer() . Contract just leaves that amount in place, but blocked.

We could name that freezeTokens(address user, uint256 amount) instead of setFrozenTokens()- would that help?

By that you can also clearly distinguish that direction, having:

  • freezeTokens(address user, uint256 amount)
  • unFreezeTokens(address user, uint256 amount)

where amount is the delta similarly to transfer.

Hey @xaler,

isUserAllowed renaming

I support canInteract for consistency with canTransfer. It reads like a capability check and isn’t tied to verification semantics. I’m also ok with isAllowed.

On extending state changing functions with calldata parameter

The whole idea of ERC-7943 is that is modular and easily extendable. I agree not to change the existing function signatures. This way, we preserve backward compatibility. The standard already allows for an opt-in path through project-specific methods outside the ERC, or through a separate extension interface.

On separating state changing functions on separate interfaces

I’d avoid this, unless there is a compelling implementation requirement. Borrowing the command/query separation principle from traditional software engineering practices doesn’t make sense to me. We should keep things lean and simple.

On setFrozenTokens behaviour

I’m in favour of absolute values using uint256, and explicitly allowing to freeze above balance. It’s idempotent and enforced by the transfer rules. Computing deltas seems more of an off-chain concern to me. IMHO smart contracts should prioritize enforcing rules instead of computing stuff needed by off-chain systems.

I agree indeed that it makes things more complex, however I still don’t see how you can avoid the DoS mentioned in my example ? In which ways are you thinkiing to solve this ? One I can think of is:

  • The admin first blacklists
  • Then freeze
  • Eventually remove from blacklist ?

Exactly, including forced transfers. RWA/compliance tokens are centralized by design and rely on governance/ops when edge cases like this arise.

What you mention is an issue to be considered in the development of the uRWA, you get too many permutations of assets, legal framework per jurisdiction, regional/national/supranational bodies…

The debate is just endless, and I applaud your first step in creating an abstraction, but the bulk of the thing is still ahead, one has to consider as you said, just different metadata for different assets and issuance jurisdiction and complex dynamics of the assets make a monolithic approach unpractical.
This merits that the construction of a token is “modular” token issuance, my priors in this aspect is what we always do when a problem is complex, break it into smaller ones, use a “Diamond token creation schema”, but instead of using it for storage we can use it to invoke the relevant smartcontracts involved in the minting of a given token.

My initial idea is that the base contract should be able to interact with specific smart contracts tailored to each jurisdiction. For example, if one wishes to tokenize an asset—say, stock ABC—under Singaporean law, the uRWA-base_contract would call the uRWA-stock_Singapore smart contract during the minting process. This would ensure compliance with local issuance regulations and incorporate the required jurisdiction-specific data and metadata, which may vary across regions.

Conversely, if the same asset were to be tokenized under U.S. law, the uRWA-base_contract would instead invoke the uRWA-stock_USA smart contract.

Ideally, policymakers would aim to harmonize these legal frameworks, at least partially, to minimize the number of jurisdiction-specific smart contracts required. Such alignment would enhance the cross-jurisdictional usability and interoperability of uRWA tokens.

Another aspect that requires further consideration is the ENFORCER_ROLE you proposed. This role should be transferable, as the authority responsible for seizing or freezing assets may change under certain circumstances. For instance, this could occur if the asset holder relocates to another country—with potential impacts in the relevant supervisory body—or due to life events such as a divorce, where the enforcement authority might shift from a regulatory body (e.g., the SEC) to a different institution such as a tribunal.

Agree, we should point on something like the base behavior of tokenization asset, policies enforement rule can be optional in my opinion.

Question for implementation is its should have contain attribute or a way to describe the asset e.g. tokenization stock or carbon credit its should describe that its intangible in real world.

isUserAllowed renaming

I like the capability-based wording to match canTransfer.

canInteract leads me to believe the user cannot interact with the token at all, including allowance functions, which is not the case.

canSendOrReceive is too verbose in my opinion, and canTransfer(address) may not display properly in some UIs due to the name collision.

I believe canTransact is clearer than canInteract. I would support either.

Lastly, I’d add clarification in the spec that the function only restrict transfer capability.


- /// @notice Checks if a specific user is allowed to interact according to token rules.

+ /// @notice Checks if a specific user is allowed to send or receive transfers according to token rules.


On separating state changing functions on separate interfaces

I believe most of the ERC’s value comes from read functions being integrated in frontends and protocols, and not necessarily the permissioned write functions, which will presumably be executed from admin dashboards.

We don’t want to end up in an ERC-173 vs ERC-5313 situation, having to specify the read interface in a follow-up standard, especially in an already fragmented space.

I also don’t see a world in which only the write interface is implemented. We’re probably looking at either read-only or read-and-write.

I’d discourage, but allow, the usage of a read-only interface.

Thanks all folks @tinom9 @mihaic195

I just pushed what is to me probably the last big set of changes. In the latest version this changed:

  • Added a return value for forcedTransfer and setFrozenTokens to match exactly ERC-3643 and improve consistency
  • Renamed isUserAllowed to canTransact for better specificity and consistency with canTransfer. I believe canTransact is better than canInteract as pointed by @tinom9
  • Renamed ERC7943NotAllowedUser to ERC7943CannotTransact for consistency
  • Added ERC7943CannotTransfer
  • Updated interfaceIds
  • Moved reference implementation into the assets folder, I’ve added also tests and coverage. It can be used as example implementation now. Answering your point @LanderEA I believe this is all already possible with the proposed EIP. The example implementation uses roles which are transferable and functions are agnostic on the type of implementation so you can code them to call one regulatory contract or another, or even multiple ones. What you propose is already fully compatible with the EIP :slight_smile:
  • Reverted back the deltas on setFrozenTokens and improved docstrings on getFrozenTokens as we suggested.

I also finally decided to not split read/write and keep it read-and-write. I believe there’s enough arguments to keep it there instead of keep splitting, defining other ERCs or bloating the base itself.


@MASDXI on this

Question for implementation is its should have contain attribute or a way to describe the asset e.g. tokenization stock or carbon credit its should describe that its intangible in real world.

I believe this is a possible extension to the EIP. Similar to the “legalProof” example in the EIP description, I believe one can extend the token implementation to signal the type of token.


@radek on freezeTokens vs setFrozenTokens we opted for the latter because it’s backward compatible with other standards like 3643. On having an additional unfreeze which is the converse of freeze one, we can discuss it but to me you can freeze 50 tokens, and if you want to unfreeze 20, you can just call freeze again with 30, acting indeed as an unfreeze. A point here is that since those are permissioned functions, there’s an assumption in place that it will be used correctly.

Two suggested changes

Unifying error names

Thinking about the frozen errors:

// Fungible case (ERC-20)
error ERC7943InsufficientUnfrozenBalance(address account, uint256 amount, uint256 unfrozen);

// Non Fungible case (ERC-721)
error ERC7943FrozenTokenId(address account, uint256 tokenId);

// Multi token case (ERC-1155)
error ERC7943InsufficientUnfrozenBalance(address account, uint256 tokenId, uint256 amount, uint256 unfrozen);

Can be unified to something like ERC7943ExceedsFrozenStatus


Improving docs about setFrozenTokens absolute value vs deltas

I was thinking that patterns like blocklistsetFrozenTokenswhitelist indeed present the same issues of potentially front-running the blocklist call and move all balance out before the first blocklist action lands. This is fundamentally unsolvable to me because of fundamental nature of an open transparent ledger and mempool.

Coming down instead into race-conditions with other concurrent admins calling the setFrozenTokens I believe one can build an extension (not making this part of th EIP) which goes with an expected previous value.

function setFrozenTokensIf(address account, uint256 expectedPrev, uint256 newAmount) external onlyRole(FREEZING_ROLE) returns(bool result) {
     require(frozenTokens[account] == expectedPrev, ERC7943ExpectedValueMismatch(expectedPrev, forzenTokens[account]);
     return this.setFrozenTokens(account,newAmount);
}

and if you really want deltas

function setFrozenTokensDelta(address account, int256 deltaAmount) external onlyRole(FREEZING_ROLE) returns(bool result) {
     uint256 actualValue = frozenTokens[account];
     if(deltaAmount >= 0) actualValue += uint256(deltaAmount);
     else actualValue -= uint256(-deltaAmount); // Will underflow and revert if abs[deltaAmount] > currentValue, which is correct IMO
     return this.setFrozenTokens(account, actualValue);
}

And you even combine both. All of this to say that it makes sense to me to leave setFrozenTokens as it is, but potentially documenting above examples to enhance clarity ?


What do you think about above two proposals ?

Hi there!

Thanks for the update @xaler.

I’m addressing your proposals and adding some final considerations on the spec.


Unifying error names

We’re already using getFrozenTokens and setFrozenTokens for the 3 interfaces, being specific to ERC-20 and ERC-1155 but not to ERC-721.

I agree we should unify naming for ERC7943InsufficientUnfrozenBalance as well, to remain consistent.

That said, I believe balance is a better term than status: users care about their total balances, their frozen balances, and their unfrozen balances (the maximum of 0 and total minus frozen).


Improving docs about setFrozenTokens absolute value vs deltas

I agree front-running is an unsolvable problem for the ERC.

I believe it’s a good idea to add the ...If and ...Delta function variations as examples in the Extensibility section.

To be nitpicky, is there any reason we want to be using this.func() external calls instead of func() or super.func() in the examples? I’d prefer public functions and internal calls.


Relying on canTransfer to check if a transfer will revert

One can argue that the most important part of this standard is to be able to tell whether a given transfer will revert due to the token’s permissioned nature. To me, that means that every check beyond the permissionless token ones (required allowances and balances) should be able to be ruled out if canTransfer returns true.

The current spec does not enforce that behaviour, as per the following statements:

Additionally it (canTransfer) MAY perform a canTransact check on the from and to parameters

canTransfer should return false if the contract is paused.

Those should be MUST statements, and a third clarifying statement should be added to the spec, along the lines of:

canTransfer MUST return false if any permissioned check will cause a given transfer to fail. A transfer is understood as any interaction that emits a canonical token transfer event. For example, if the transfers are paused or canTransact returns false for the sender or recipient, canTransfer MUST return false. Required allowances or balances, or underlying permissionless logic of the tokens, MUST NOT be considered permissioned checks.

I understand this would break compatibility with current ERC-3643 implementations, and we would need to document it.


canTransact and canTransfer on forcedTransfer, mint, and burn

I believe the following spec statements can be made more consistent:

it (forcedTransfer) SHOULD perform a canTransact check on the to parameter.

Minting MUST NOT succeed to accounts where canTransact on the recipient would return false.

Burning SHOULD NOT be restricted by canTransfer or canTransact 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.

Minting or burning can be permissionless actions, in which case MUST keywords would apply, with canTransact and canTransfer checks. This is related to the issue from above, as it’d give users a standardized way of checking whether the actions would succeed.

But they can also be permissioned, in which we want to use SHOULD keywords, as integrators may not want to be that restrictive when operating the token.

I’d add that distinction to the spec.


Freezing more than balances

I believe getFrozenTokens is much easier to integrate and understand if setFrozenTokens only has one behaviour: allowing to freeze a greater amount than a user holds in their balance.

Integrators may wrongly assume that if a user has X frozen tokens, they must also have at least X token balance. This may or may not be accurate given the current spec.

The SHOULD keyword as per RFC-2119 states that:

This word, or the adjective “RECOMMENDED”, mean that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course.

Are there valid reasons to revert on setFrozenTokens if the resulting frozen amount exceeds the balance?

If we foresee any, we should use the SHOULD keyword (and therefore have two different behaviours for getFrozenTokens). Otherwise, we should go with a MUST (and only one behaviour for getFrozenTokens).


ERC7943InsufficientUnfrozenBalance enforcement

The ERC7943InsufficientUnfrozenBalance/ERC7943FrozenTokenId error SHOULD be triggered when a transfer is attempted from account with an amount less than or equal to its balance, but greater than its unfrozen balance or with a tokenId which is currently frozen.

I’d consider using a MUST keyword there instead of SHOULD.

Hi! I am Olesia Bilenka, Smart Contract Auditor at Hacken. I am sharing below some design review points:

  • Forced transfer vs freezing checks

// Spec states BOTH simultaneously:
“CAN bypass the freezing validations” // Optional
“MUST bypass checks enforced by canTransfer” // Mandatory

canTransfer MUST validate unfrozen >= amount per spec
If forcedTransfer CAN bypass → implementation-dependent behavior
Two compliant implementations will be incompatible

  • frozen > balance

What the Specification Says:

/// notice Changes the frozen status of amount tokens belonging to account.
/// dev Requires specific authorization. Frozen tokens cannot be transferred.
/// param amount The amount of tokens to freeze.
/// It can be greater than account balance.
function setFrozenTokens(address account, uint256 amount)
external returns(bool result);

Key phrase: “It can be greater than account balance”

The math problem

// Standard calculation for transferable amount:
uint256 unfrozen = balance[account] - frozen[account];

// Scenario:
balance[alice] = 100
frozen[alice] = 200 // Allowed by spec

unfrozen = 100 - 200 = -100 // error

// In Solidity 0.8.0+:
// This REVERTS with underflow panic
// prev solidity - underflow issue

The Core Semantic Problem

What does frozen[account] = 200 mean when balance[account] = 100?

Interpretation 1: Future freeze
Interpretation 2: Maximum freeze limit
Interpretation 3: Proportional freeze

Recommendation:
Option A: Prohibit frozen > balance (simplest)
Option B: Define explicit “future freeze” semantics

  • ERC-721 (account, tokenId) Tuple

What the Specification Says:

interface IERC7943NonFungible {
/// @notice Changes the frozen status of tokenId belonging to account.
function setFrozenTokens(address account, uint256 tokenId, bool frozen)
external returns(bool);

/// @notice Checks the frozen status of a specific `tokenId`.
/// @dev It could return true even if account does not hold the token.
function getFrozenTokens(address account, uint256 tokenId) 
    external view returns (bool);

}

Key phrase: “It could return true even if account does not hold the token”

The Fundamental Question
What happens to freeze status when ownership changes? The spec does not answer this question.

Why This is Architecturally Wrong
NFTs represent unique assets. Freeze semantics should be:
Asset-centric: “Property #123 is frozen” (not “Alice’s property #123”)
Ownership-independent: Freeze status should persist across ownership changes
Global: Anyone checking freeze should get same answer
Current spec creates account-centric freeze:
“Alice’s token #123 is frozen”
But Bob can own the same token #123
No relationship between Alice’s freeze and Bob’s ownership

Recommendations:
Token-centric freeze for NFTs
Alternative: Account-specific freeze

  • ERC-1155 Missing Batch Operations

Current Spec (Single Transfer Only):

interface IERC7943MultiToken {
function forcedTransfer(
address from,
address to,
uint256 tokenId,
uint256 amount
) external returns(bool result);

event ForcedTransfer(
    address indexed from, 
    address indexed to, 
    uint256 indexed tokenId, 
    uint256 amount
);

}

ERC-1155 standard includes batch operations:

interface IERC1155 {
function safeBatchTransferFrom(
address from,
address to,
uint256 calldata ids,
uint256 calldata amounts,
bytes calldata data
) external;

event TransferBatch(
    address indexed operator,
    address indexed from,
    address indexed to,
    uint256[] ids,
    uint256[] amounts
);

}

Problem 1: Non-atomic seizure
Problem 2: Extreme Gas costs

Recommendation:
ERC-7943 may have equivalent batch operations for forcedTransfer.

  • Burning and Freeze Interaction

What the Specification Says:

“Burning MAY be restricted to prevent burning more assets than the unfrozen amount. It MAY burn more assets than the unfrozen amount in permissioned functions, in which case the contract MUST update the frozen status accordingly.”

// Scenario:
balance[alice] = 100
frozen[alice] = 80
unfrozen = 20

// Permissioned burn of 50 tokens:
burn(alice, 50)

// After burn:
balance[alice] = 50
frozen[alice] = ?

Recommendation:
Public burn functions SHOULD only allow burning unfrozen tokens.
Permissioned burn functions MAY burn frozen tokens.
If burning frozen tokens, the frozen amount MUST be reduced.

I agree about the asset-centric; it should be at the token behavior, not the account behavior.

Then the forced transfer for me is considered optional; it’s useful for the controller/asset issuer.

Thanks @tinom9 @MASDXI and @OlesiaBilenka , below answers to your comments and at the end a summary of changes I’d like to introduce:

On @tinom9 ones:

Unifying error names

Agreed. Look at the changes below.

Improving docs about setFrozenTokens absolute value vs deltas

Agreed. Look at the changes below.

Relying on canTransfer to check if a transfer will revert

Agreed. Look at the changes below.

canTransact and canTransfer on forcedTransfer, mint, and burn

Agreed. Look at the changes below.

Freezing more than balances

Agreed. Look at the changes below.

ERC7943InsufficientUnfrozenBalance enforcement

We decided to keep it as SHOULD to reflect the fact that the EIP prioritize error specificity and if more specific errors are to be emitted, this errors might become obsolete. Let’s keep them as SHOULD.


on @OlesiaBilenka

Forced transfer vs freezing checks

Agreed. Look at the changes below.

The math problem

Option B: Define explicit “future freeze” semantics

This is already suggested in here

  • MUST allow freezing more assets than those held. This allows for future balances withholding.

The Fundamental Question

What happens to freeze status when ownership changes? The spec does not answer this question.

That’s a legit doubt. However the current behaviour is consistent with ERC-20 and ERC-1155 cases in athe sense that:

  • One can result to have some tokens frozen even if they have no tokens at all.
  • The implementation can decide to reset ownership at will, this will not break the getFrozenTokens requirement
  • One can easily have a block on a specific token even if not owning it just because it’s prohibited for that account to own it. I believe it makes sense to keep it as it is cc @MASDXI
  • getFrozenTokens will return the absolute frozen amount, which MAY exceed the account’s current balance. In ERC-721 tokens, it MAY return true even if the account does not hold the token.

ERC-1155 Missing Batch Operations

Agreed. Look at the changes below. It will be covered by the Multicall that anyone can implement. I wouldn’t bloat directly the interface with it. With Multicall we would get the same result.

Burning and Freeze Interaction

I agree here and will describe changes below.


Changes of last round:

1. Enhanced Specification Clarity

  • canTransfer: Now explicitly requires canTransact checks on both from and to parameters, making compliance enforcement more comprehensive
  • The 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).
    - MUST perform a canTransact check on the from and to parameters. An important documentation note is that ERC-3643 doesn’t perform a canTransact check within canTransfer as required.
    - MUST return false in general, if any permissioned rule will would prevent a given transfer from succeeding. A transfer refers to any operation that emits the token’s canonical transfer event. A permissioned check can be a pausing mechanism, a call to canTransact or anything else that requires privileged actors.
  • Minting/Burning behavior: Clearer distinction between permissionless and permissioned contexts
  • Minting in permissionless contexts (e.g., public mint functions) MUST NOT succeed for accounts where canTransact on the recipient would return false. In permissioned contexts (e.g., authorized minting by privileged roles), minting SHOULD respect canTransact checks on the recipient, though implementations MAY bypass these checks when necessary for operational or compliance reasons.
  • Burning in permissionless contexts (e.g., public burn functions) MUST respect canTransfer check, MUST respect the canTransact check on the token holder and MUST NOT allow burning more assets than the unfrozen amount. In permissioned contexts (e.g., authorized burning by privileged roles), burning MAY NOT be restricted by canTransfer or canTransact checks on the token holder and MAY burn more assets than the unfrozen amount, in which case the contract MUST update the frozen status accordingly and emit a Frozen event before the underlying base token transfer event.

2. Improved forcedTransfer Specification

forcedTransfer and setFrozenTokens require a restriction in access. This just means that those are not permisionless. In case in which ownership is not only one account at once, we can differentiate between single-party and multi-party permissioned contexts. In this I believe the forcedTranser should behave differently.

  • In single-party permissioned contexts:
    - It CAN bypass the canTransfer checks. If this happens, it MUST unfreeze the assets first and emit a 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.
    - It SHOULD at least perform a canTransact check on the to parameter to ensure compliance.
  • In multi-party permissioned contexts:
    - It MUST perform the canTransfer checks. // which includes the canTransact and the freezing validations.

3. Enhanced Extensibility Examples

The current version includes more practical extension patterns:

  • Delta-based freezing functions
  • Expected value checks for race condition prevention
  • Multicall integration examples

Freezing absolute vs delta values again…

I realise this has been discussed at some length already. I’m putting this here to see if it strikes a chord, but it seems reasonable if people read it and decide that no change is needed.

The one option I would rule out is the one where it depends on the value - I’d rather have 3 methods: freeze(uint delta), unfreeze(uint delta), setFrozenTokens(uint amount)

If in a future world there will be various requests to a single-party that sets up the freezes each time, and they have e.g. a “requestFreeze(amount)” and “requestUnfreeze(amount)” API where the calculation happens offchain, then keeping the spec a bit smaller seems reasonable.

If in a future world there are multiple parties who might have freeze authority then it’s more painful for each of them to implement the calculation, and more likelihood of realising race conditions. At that point I think it becomes important for the Token to have the methods, and they might as well be a standard.

It’s not a huge piece either way. I do appreciate the effort to keep the spec as light as possible, and obviously it’s always possible to write another spec that sits on top and defines standard delta-based methods. (e.g. copy-paste the examples in the spec :slight_smile: ).

On the other hand, I cannot think of a use case that starts with setting an absolute freeze amount instead of a delta.

(Thanks for all the work by the way. Reading through this entire thread I appreciate the effort and thinking that has gone into this already.)

Thanks for the feedback @chaals ! You raise great points about multi-party contexts. Let me explain why we went with absolute values:

The TL;DR: Absolute values are the primitive; delta operations are the extension. Just like ERC-20 has approve() (absolute) with increaseAllowance()/decreaseAllowance() added later as extensions.

Why absolute values?

  1. Race conditions exist in both approaches, just differently:
  • Absolute: Last write wins (deterministic, easy to audit)
  • Delta: Accumulating errors compound (Party A freezes 100, Party B freezes 100 → 200 total when maybe only 100 was intended)
  1. Extensibility by design: The spec explicitly shows how to add delta methods (example #2 in Extensibility section). Implementations needing them can add: this way we don’t force complexity on simple single-party use cases.

  2. Gas efficiency: Single SSTORE vs SLOAD + check + SSTORE for deltas.

The three methods (freeze, unfreeze, setFrozenTokens) would triple the interface size and still not solve the race condition, you’d still need coordination between parties.

The current design keeps the core minimal while letting implementations add what they need. Different RWA contexts need different freeze coordination strategies, and ERC-7943 provides the primitive all strategies can build upon.

Does this make sense ? Always open to concrete scenarios we might have missed! :slightly_smiling_face: