Skip to content

Reject ERC20uRWA forced transfer to self#246

Open
PramodTKodag wants to merge 1 commit into
OpenZeppelin:masterfrom
PramodTKodag:fix/erc20urwa-forced-self-transfer
Open

Reject ERC20uRWA forced transfer to self#246
PramodTKodag wants to merge 1 commit into
OpenZeppelin:masterfrom
PramodTKodag:fix/erc20urwa-forced-self-transfer

Conversation

@PramodTKodag

@PramodTKodag PramodTKodag commented Jun 25, 2026

Copy link
Copy Markdown

Fixes #244

Problem

ERC20uRWA.forcedTransfer(from, to, amount) does not reject from == to. In that case _update(from, to, amount) moves no tokens, but the frozen-balance adjustment runs first:

uint256 currentFrozen = frozen(from);
uint256 newBalance;
unchecked { newBalance = balanceOf(from) - amount; }
if (currentFrozen > newBalance) {
    _setFrozen(from, newBalance);
}

So a self-directed forced transfer leaves the balance unchanged while lowering the frozen amount — an implicit, unauthorized unfreeze. This is most relevant when the enforcer and freezer permissions are held by different entities: an enforcer could reduce frozen balances without holding the freezer role, breaking the intended separation of duties (also noted for the ERC-7943 reference implementation in ethereum/ERCs#1778).

Example: balanceOf(alice) = 100, frozen(alice) = 100. Calling forcedTransfer(alice, alice, 100) results in frozen(alice) = 0 with the balance untouched.

Fix

Reject the degenerate self-transfer by reverting with the existing ERC7943CannotTransfer(from, to, amount) error before any state changes:

require(from != to, ERC7943CannotTransfer(from, to, amount));

Scope

This PR addresses point 1 of #244 (the self-forced-transfer edge case), which is a contained correctness/security fix. Points 2–4 in that issue (the setFrozenTokens balance cap, the canSend/canReceive vs canTransact interface split, and canTransfer semantics) are interface/semantic decisions and are intentionally left out of this PR for the maintainers to decide separately.

PR Checklist

  • Tests — added a regression test asserting forcedTransfer(holder, holder, ...) reverts and leaves the frozen balance untouched; full ERC20uRWA suite passes (40 passing).
  • Documentation — added a CHANGELOG.md entry and an explanatory code comment.

Summary by CodeRabbit

  • Bug Fixes

    • Forced transfers now correctly reject attempts to send tokens to the same address, preventing frozen balances from being reduced without any token movement.
    • Self-directed forced transfers now fail with a clear error and leave balances unchanged.
  • Tests

    • Added coverage to verify same-address forced transfers revert as expected and preserve frozen token balances.
  • Documentation

    • Updated the changelog with the latest behavior change.

forcedTransfer(from, from, amount) moves no tokens, but the frozen-balance
adjustment still lowered the frozen amount, effectively unfreezing tokens
without the freezer role. This bypassed the intended separation between the
enforcer and freezer roles (closes OpenZeppelin#244).

Reject the case by reverting with ERC7943CannotTransfer when from == to,
and add a regression test.
@PramodTKodag PramodTKodag requested a review from a team as a code owner June 25, 2026 06:06
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ae3c6842-4247-45df-9a9c-43d8136e19da

📥 Commits

Reviewing files that changed from the base of the PR and between f7e5f08 and 12aa589.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • contracts/token/ERC20/extensions/ERC20uRWA.sol
  • test/token/ERC20/extensions/ERC20uRWA.test.js

Walkthrough

ERC20uRWA now rejects forcedTransfer when the sender and recipient are the same address. A regression test covers the revert and frozen-balance state, and the changelog records the change.

Changes

ERC20uRWA self-forced-transfer guard

Layer / File(s) Summary
Self-transfer check in forcedTransfer
contracts/token/ERC20/extensions/ERC20uRWA.sol
forcedTransfer now rejects calls where from equals to before adjusting frozen balances or moving tokens.
Regression test and release note
test/token/ERC20/extensions/ERC20uRWA.test.js, CHANGELOG.md
A new test asserts the self-transfer revert and unchanged frozen balance, and the changelog records the new guard.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A rabbit hopped and twitched its nose,
“No self-transfer!” the carrot prose.
🐰 Frozen bits stay snug and neat,
When sender meets receiver’s seat.
Hop-hop, the guard is яс?—no, just complete!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting self forced transfers in ERC20uRWA.
Linked Issues check ✅ Passed The PR implements the reported self-forced-transfer fix from #244 and uses the existing ERC7943CannotTransfer error as requested.
Out of Scope Changes check ✅ Passed The changelog, code comment, and regression test are all directly related to the self-transfer fix and stay within scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ERC20uRWA: potential ERC-7943 compliance issues and self-forced-transfer edge case

1 participant