Skip to content

feat(b20): reject B20-prefix addresses as credit recipients - #232

Open
rayyan224 wants to merge 8 commits into
mainfrom
rayyanalam/bop-743-disallow-b20-self-sends-in-base-std
Open

rayyan224 wants to merge 8 commits into
mainfrom
rayyanalam/bop-743-disallow-b20-self-sends-in-base-std

Conversation

@rayyan224

@rayyan224 rayyan224 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Reject crediting any B20 balance to any B20-prefix address (0xB2 + 9 zero bytes) with InvalidReceiver on transfer, mint, seize, and batchMint paths (BOP-743).
  • Covers this token and every other B20 via a pure prefix check (no isB20Initialized / factory call). Uncreated predicted 0xB2… addresses also revert.
  • Keep seizeWithMemo from a B20 address allowed so already-stuck balances remain recoverable; holder-to-holder self-transfers (from == to) are unchanged.
  • Add Denim changelog entry, docs, unit/revert-order tests (including cross-token), and smoke probes that skip cleanly on pre-Denim chains.

Test plan

  • forge test --match-test 'tokenRecipient|otherB20Recipient|fromTokenAddress'
  • forge test
  • python3 script/check-coverage.py
  • make fork-tests once the matching base/base change lands (cross-validate against Rust)
  • Smoke against a Denim-activated node: transfer/mint to tok.address and to another B20 → InvalidReceiver

@linear

linear Bot commented Sep 17, 2026

Copy link
Copy Markdown

BOP-743

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

Interface Coverage

✅ All interface functions have test coverage.

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

📊 Forge Coverage (src/lib/)

🟡 ≥95% across all metrics — some metrics below 99%.

File Lines Stmts Branches Funcs
🟡 B20FactoryLib.sol 97.70% 98.00% 100.00% 95.00%
🔴 test/lib/ForceFeeder.sol 0.00% 0.00% 100.00% 0.00%
🔴 test/lib/PrecompileProbe.sol 0.00% 0.00% 0.00% 0.00%
🟢 MockActivationRegistry.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockActivationRegistryStorage.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockB20.sol 100.00% 99.68% 100.00% 100.00%
🟢 MockB20Asset.sol 100.00% 100.00% 100.00% 100.00%
🟡 MockB20Factory.sol 98.96% 99.10% 100.00% 100.00%
🟢 MockB20Stablecoin.sol 100.00% 100.00% 100.00% 100.00%
🟢 MockB20Storage.sol 100.00% 100.00% 100.00% 100.00%
🟡 MockPolicyRegistry.sol 100.00% 99.58% 97.78% 100.00%
🟢 MockPolicyRegistryStorage.sol 100.00% 100.00% 100.00% 100.00%
Total 97.12% 97.50% 98.21% 97.03%

Full report: download artifact. To browse locally: make coverage (runs forge coverage + genhtml + opens the HTML report).

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown

⚠️ Fork tests: 80 failed, 742 passed

These failures indicate divergences where base/base needs to catch up to the base-std spec. This check is advisory and does not block merging.

Failing tests
  • test_batchMint_revert_tokenRecipient(address,uint256,uint256): Error != expected error: SupplyCapExceeded(340282366920938463463374607431768211455 [3.402e38], 382889016923482006373725080917517054244 [3.828e38]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x0e04a6a60000000000000000000000006290027a717e96a0377c19644b20314a7d916412000000000000000000000000017d519fbe53874fa7b1adf322e0f19cd2a8a40700000000bc26933963301ca600c3395e61ba36b82d0ebf18fc0ae7499dd00d1d args=[0x6290027A717E96A0377C19644B20314a7d916412, 8503694373684004700731711986292624396971975687 [8.503e45], 19814579707102688687231897064719062282465017581768088415516400553245 [1.981e67]]
  • test_invertedPolicyId_success_negatesAuthorization(address): custom error 0x6b468933; counterexample: calldata=0x4268079e0000000000000000000000008453000000000000000000000000000000000001 args=[0x8453000000000000000000000000000000000001]
  • test_invertedPolicyId_success_togglesAndRoundTrips(uint64): custom error 0x6b468933; counterexample: calldata=0x123f60c00000000000000000000000000000000000000000000000000000000000015180 args=[86400 [8.64e4]]
  • test_isAuthorized_success_intersectAllowAndNotX(address): PolicyNotFound(); counterexample: calldata=0x55848b8e0000000000000000000000000000000000000000000000000000000000000fda args=[0x0000000000000000000000000000000000000fda]
  • test_isAuthorized_success_invertAllowlistNonMemberAuthorized(address): assertion failed; counterexample: calldata=0xb07c21cd0000000000000000000000000000000000000000000000000000000000000325 args=[0x0000000000000000000000000000000000000325]
  • test_isAuthorized_success_invertAlwaysBlockAuthorizes(address): assertion failed; counterexample: calldata=0xca9d7161000000000000000000000000ac00513194153cb2c38a28d05f46350f23fc0c0b args=[0xAc00513194153Cb2c38a28d05f46350F23FC0C0B]
  • test_isAuthorized_success_invertBlocklistMemberAuthorized(address): assertion failed; counterexample: calldata=0x072617790000000000000000000000003431c31dd6a333e4d23d4699863283702d7dd43e args=[0x3431C31dD6a333E4d23D4699863283702d7dd43e]
  • test_mintWithMemo_revert_tokenRecipient(uint256,bytes32): Error != expected error: SupplyCapExceeded(340282366920938463463374607431768211455 [3.402e38], 537569168993557327081089387993669471309551844730755200 [5.375e53]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x4ca8134a000000000000000000059ccbc05f544b977fc9de76726f0445130ebd7ada5880d39d02bd9cef42872cb98bfe75d60f173471e397478858eda556a02532ddf48e args=[537569168993557327081089387993669471309551844730755200 [5.375e53], 0xd39d02bd9cef42872cb98bfe75d60f173471e397478858eda556a02532ddf48e]
  • test_mint_revertOrder_tokenRecipient_beats_cap(uint256): Error != expected error: SupplyCapExceeded(0, 13333 [1.333e4]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0xc475cac10000000000000000000000000000000000000000000000000000000000003415 args=[13333 [1.333e4]]
  • test_mint_revertOrder_tokenRecipient_beats_policy(uint256): Error != expected error: PolicyForbids(0xa0d5ae037e66a09119acf080a1d807abb9b6d03b6b9130eb19f7c1e6bdb8ffc8, 72057594037927937 [7.205e16]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x6ea980310000000000000000234e5c9dafedf22ad3e3c12e544d2a2017157032868111b9 args=[865703070393318204495387215882821525268811491942699438521 [8.657e56]]
  • test_mint_revert_tokenRecipient(uint256): Error != expected error: SupplyCapExceeded(340282366920938463463374607431768211455 [3.402e38], 17877714682488170593289722320399606972655793211844357769067131456761 [1.787e67]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x03224e1a00000000a9c2512905449ccf955a306a841e2ce288c9a114c28f1068da8e4cf9 args=[17877714682488170593289722320399606972655793211844357769067131456761 [1.787e67]]
  • test_policyAdmin_success_invertResolvesBaseAdmin(address): assertion failed: 0x0000000000000000000000000000000000000000 != 0x7140b8faC78B712B00F04c90812f2AFE5794b021; counterexample: calldata=0xee7ff5a60000000000000000000000007140b8fac78b712b00f04c90812f2afe5794b021 args=[0x7140b8faC78B712B00F04c90812f2AFE5794b021]
  • test_policyExists_success_invertMirrorsBase(uint56): assertion failed; counterexample: calldata=0xf4aec5dc00000000000000000000000000000000000000000000000000000000feb346eb args=[4273161963 [4.273e9]]
  • test_seizeWithMemo_revertOrder_tokenRecipient_beats_blocked(address): Error != expected error: AccountNotSeizable(0x4Bf863e83B0Aa81deC710fae7D58109f48Cf096E) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x4ce156d20000000000000000000000004bf863e83b0aa81dec710fae7d58109f48cf096e args=[0x4Bf863e83B0Aa81deC710fae7D58109f48Cf096E]
  • test_seizeWithMemo_revert_tokenRecipient(address,uint256): Error != expected error: InsufficientBalance(0xCB00000000000000000000000000000000000000, 0, 4393) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x220d05f3000000000000000000000000cb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001129 args=[0xCB00000000000000000000000000000000000000, 4393]
  • test_transferFromWithMemo_revert_tokenRecipient(address,address,uint256,bytes32): Error != expected error: InsufficientAllowance(0x00000000000000000000000000000000000010bA, 0, 15) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0xac063dd000000000000000000000000000000000000000000000000000000000000010ba0000000000000000000000000000000000000000000000000000000000000fa4000000000000000000000000000000000000000000000000000000000000000f0cc58340b26c619cd4edc70f833d3f4d9d26f3ae7d5ef2965f81fe5495049a4f args=[0x00000000000000000000000000000000000010bA, 0x0000000000000000000000000000000000000FA4, 15, 0x0cc58340b26c619cd4edc70f833d3f4d9d26f3ae7d5ef2965f81fe5495049a4f]
  • test_transferFrom_revertOrder_tokenRecipient_beats_allowance(address,address,uint256): Error != expected error: InsufficientAllowance(0x00000000000000000000000000000000000003e9, 0, 5728) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x98d9a95e00000000000000000000000000000000000000000000000000000000000003e90000000000000000000000000000000000000000000000000000000000002cac0000000000000000000000000000000000000000000000000000000000001660 args=[0x00000000000000000000000000000000000003e9, 0x0000000000000000000000000000000000002cac, 5728]
  • test_transferFrom_revertOrder_tokenRecipient_beats_executorPolicy(address,address,uint256): Error != expected error: InsufficientAllowance(0xe76D17d22AD7493453e637038F56c65827f862b0, 0, 86320550757263956168498131171920745200 [8.632e37]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x0fbad031000000000000000000000000e76d17d22ad7493453e637038f56c65827f862b00000000000000000000000000d9bdf06106b102a05c0ce3b63e16be09ed22bc8000000000000000000000003eba43c8840f0bbbcf3ec50f1b0d6a9d69c5e5668 args=[0xe76D17d22AD7493453e637038F56c65827f862b0, 0x0D9Bdf06106B102a05c0Ce3B63e16be09Ed22bc8, 5729780338450748422620748951849503712289847400040 [5.729e48]]
  • test_transferFrom_revert_selfCaller_executorPolicyForbids(address,address,uint256): next call did not revert as expected; counterexample: calldata=0x641b204b0000000000000000000000009611de05f5cbdc4440b2408d72fa6b872a298244000000000000000000000000f7f42c30952b43457d810a630df0e4f43b2ff7f100000000e81977e0f0a1949f02c52b34027a12c852526b399f970b905130de34 args=[0x9611DE05f5cBdc4440b2408d72fA6B872a298244, 0xf7F42c30952B43457d810a630DF0e4F43b2ff7F1, 24442928708186839617213318316176984136871047776404279853770091847220 [2.444e67]]
  • test_transferFrom_revert_tokenRecipient(address,address,uint256): Error != expected error: InsufficientAllowance(0x0000000000000000000000000000000000001743, 0, 7877) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0xb9f547920000000000000000000000000000000000000000000000000000000000001743000000000000000000000000000000000000000000000000000000000000071a0000000000000000000000000000000000000000000000000000000000001ec5 args=[0x0000000000000000000000000000000000001743, 0x000000000000000000000000000000000000071A, 7877]
  • test_transferWithMemo_revertOrder(address,address,uint256,bytes32): Error != expected error: PolicyForbids(0xb81736c875ab819dd97f59f2a6542cfb731ad52b4ae15a6f24df2fb02b0327f5, 72057594037927937 [7.205e16]) != PolicyForbids(0x10be5173aff2a44e748bd9acd8b19fe34689581398a9db7ba2fb671e786ff7d8, 72057594037927937 [7.205e16]); counterexample: calldata=0xf5e3112500000000000000000000000030dd8107e43118e0fb5ad91deab0ab46f69ac73f00000000000000000000000085677a6c8c6d0c1bec775468994ac7f6e9b006bc00000001014b8844fcb849dfd39c722c468be9ab90cc330813f633033c0e53427d4e8e3b5faa01344d23254ae4fbae8ce3491e7be304b3d2eb657f65da2b6dea args=[0x30dD8107e43118E0FB5aD91deaB0AB46F69aC73f, 0x85677a6c8c6D0c1bEc775468994AC7f6e9b006BC, 27096331145882580182783875512793146668701101445551786095113145307970 [2.709e67], 0x7d4e8e3b5faa01344d23254ae4fbae8ce3491e7be304b3d2eb657f65da2b6dea]
  • test_transferWithMemo_revert_executorPolicyForbids(address,address,uint256,bytes32): Error != expected error: InsufficientBalance(0x00000000000000000000000000000000E9A9C850, 0, 141754402752330845590481055736416358019311210650423788660828243181423970816 [1.417e74]) != PolicyForbids(0x10be5173aff2a44e748bd9acd8b19fe34689581398a9db7ba2fb671e786ff7d8, 72057594037927937 [7.205e16]); counterexample: calldata=0xa3802c1700000000000000000000000000000000000000000000000000000000e9a9c85000000000000000000000000000000000000000000000000000000000000003aa00503aeb06982fa1fe3151dc68f90b3946c55c449dfd447e49dcaece71ba4a00a37a6aa18ea0fbed1c98362f86b3c15260c32a104568f7727886ce7dd72b9c60 args=[0x00000000000000000000000000000000E9A9C850, 0x00000000000000000000000000000000000003Aa, 141754402752330845590481055736416358019311210650423788660828243181423970816 [1.417e74], 0xa37a6aa18ea0fbed1c98362f86b3c15260c32a104568f7727886ce7dd72b9c60]
  • test_transferWithMemo_revert_tokenRecipient(address,uint256,bytes32): Error != expected error: InsufficientBalance(0x694b5A6203EdC0bfAD98E0653CE14a5Cf50ED1e3, 0, 115792089237316195423570985008687907853269984665640564039457584007913129639934 [1.157e77]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0xe48d5155000000000000000000000000694b5a6203edc0bfad98e0653ce14a5cf50ed1e3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe160434cc2b68c527b28d645f30fe58d1db03f0ca0d0c26a3bfbb0eae979a052b args=[0x694b5A6203EdC0bfAD98E0653CE14a5Cf50ED1e3, 115792089237316195423570985008687907853269984665640564039457584007913129639934 [1.157e77], 0x160434cc2b68c527b28d645f30fe58d1db03f0ca0d0c26a3bfbb0eae979a052b]
  • test_transfer_revertOrder_executorPolicy_beats_balance(address,address,uint256): Error != expected error: InsufficientBalance(0xD222BFF2d87603448A82C2bBA508fE503482e331, 0, 340282366920938463463374607431768211452 [3.402e38]) != PolicyForbids(0x10be5173aff2a44e748bd9acd8b19fe34689581398a9db7ba2fb671e786ff7d8, 72057594037927937 [7.205e16]); counterexample: calldata=0x76b31f6e000000000000000000000000d222bff2d87603448a82c2bba508fe503482e3310000000000000000000000005c2b5b92f48f8f5e2679f2bc417debf221ebe18dfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc args=[0xD222BFF2d87603448A82C2bBA508fE503482e331, 0x5c2B5B92F48F8F5E2679F2Bc417debf221eBE18D, 115792089237316195423570985008687907853269984665640564039457584007913129639932 [1.157e77]]
  • test_transfer_revertOrder_executorPolicy_beats_receiverPolicy(address,address,uint256): Error != expected error: PolicyForbids(0x8a4b3fa2d8b921852bc0089c6ef0958aa6961897be36fd731330fe2cd23f8363, 72057594037927937 [7.205e16]) != PolicyForbids(0x10be5173aff2a44e748bd9acd8b19fe34689581398a9db7ba2fb671e786ff7d8, 72057594037927937 [7.205e16]); counterexample: calldata=0x67f822c50000000000000000000000007ab56548948d947aad45b260bc1555124f2766a300000000000000000000000040fd89c40a2050fd1d8d5dec4b25f05d27b24ff80000000000082ea54f25608a5a4883d3a0c91d93a92d81a7bd790f702cb1524e args=[0x7aB56548948D947AaD45B260Bc1555124F2766a3, 0x40FD89c40A2050Fd1D8d5dec4B25f05d27b24Ff8, 3365965927119044098938089162439003280300233592176963417021698638 [3.365e63]]
  • test_transfer_revertOrder_executorPolicy_beats_senderPolicy(address,address,uint256): Error != expected error: PolicyForbids(0xb81736c875ab819dd97f59f2a6542cfb731ad52b4ae15a6f24df2fb02b0327f5, 72057594037927937 [7.205e16]) != PolicyForbids(0x10be5173aff2a44e748bd9acd8b19fe34689581398a9db7ba2fb671e786ff7d8, 72057594037927937 [7.205e16]); counterexample: calldata=0xe9945cbb000000000000000000000000441abb369ae8d1d0eb3611a4f7715e1b0633cff7000000000000000000000000b0efac292819982fd79b92277d5e49a13b3f24570000000000000000000000000000000000000000000000003463c4aa342704df args=[0x441ABB369ae8D1d0eB3611A4f7715E1b0633CFf7, 0xb0eFac292819982fd79b92277D5e49a13b3F2457, 3775077147965064415 [3.775e18]]
  • test_transfer_revertOrder_tokenRecipient_beats_balance(address,uint256): Error != expected error: InsufficientBalance(0x000000000000000000000000000000003f7286f3, 0, 252928767978502097101385532755038344471 [2.529e38]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x71022c5e000000000000000000000000000000000000000000000000000000003f7286f3c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6 args=[0x000000000000000000000000000000003f7286f3, 90743482286830539503240959006302832933333810038750515972785732718729991261126 [9.074e76]]
  • test_transfer_revertOrder_tokenRecipient_beats_executorPolicy(address,uint256): Error != expected error: InsufficientBalance(0x1709cc751c2b7DdE8639A5947e45316580e2830B, 0, 23822197580031679233103063500316611142247462204488265931460335141455047640902 [2.382e76]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x5de2a22f0000000000000000000000001709cc751c2b7dde8639a5947e45316580e2830b34aae2d1de10e1ca54be14c586273e3cae512a0e39d1c6bc5a498307022f4746 args=[0x1709cc751c2b7DdE8639A5947e45316580e2830B, 23822197580031679233103063500316611142247462204488265931460335141455047640902 [2.382e76]]
  • test_transfer_revertOrder_tokenRecipient_beats_receiverPolicy(address,uint256): Error != expected error: PolicyForbids(0x8a4b3fa2d8b921852bc0089c6ef0958aa6961897be36fd731330fe2cd23f8363, 72057594037927937 [7.205e16]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x85d001e4000000000000000000000000db9dbc4bd1772097b163027cfe14d096370167c4fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe args=[0xdB9DbC4bD1772097b163027CFE14D096370167C4, 115792089237316195423570985008687907853269984665640564039457584007913129639934 [1.157e77]]
  • test_transfer_revertOrder_tokenRecipient_beats_senderPolicy(address,uint256): Error != expected error: PolicyForbids(0xb81736c875ab819dd97f59f2a6542cfb731ad52b4ae15a6f24df2fb02b0327f5, 72057594037927937 [7.205e16]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x82b0eb1a000000000000000000000000000000000000000000000000000000000000115e000000000000000000000000000000000000000000000000000000000000000f args=[0x000000000000000000000000000000000000115e, 15]
  • test_transfer_revertOrder_tokenRecipient_beats_zeroSender(uint256): Error != expected error: InvalidSender(0x0000000000000000000000000000000000000000) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x842ce6140000000000000000000000000d3116dfda339c7d0474dcf7f315ba482f243e27 args=[75311609178735920625364338039622165158963723815 [7.531e46]]
  • test_transfer_revert_executorPolicyForbids(address,address,uint256): Error != expected error: InsufficientBalance(0x26e18eadbFab2618577f0e58f66f6d37744566a8, 0, 27358255128981117493846138638763354960657614249844472551254777378926486070948 [2.735e76]) != PolicyForbids(0x10be5173aff2a44e748bd9acd8b19fe34689581398a9db7ba2fb671e786ff7d8, 72057594037927937 [7.205e16]); counterexample: calldata=0xe8de369d00000000000000000000000026e18eadbfab2618577f0e58f66f6d37744566a80000000000000000000000000bb8e87ca1e42b1f396e326a0c8286f8e4a090f23c7c3941f7054b69db123d7ef07d93f77e785d6fcf6e45879e263af808aba6a4 args=[0x26e18eadbFab2618577f0e58f66f6d37744566a8, 0x0bb8e87cA1e42B1F396E326A0c8286f8E4a090F2, 27358255128981117493846138638763354960657614249844472551254777378926486070948 [2.735e76]]
  • test_transfer_revert_externalExecutorPolicyDenies(address,address,uint256): Error != expected error: InsufficientBalance(0x7d6d7374515394b30284b464Ce6Ca7b2DF9d38Ca, 0, 3135583289253396222631694322089566868378365106410049 [3.135e51]) != PolicyForbids(0x10be5173aff2a44e748bd9acd8b19fe34689581398a9db7ba2fb671e786ff7d8, 72057594037927938 [7.205e16]); counterexample: calldata=0x66ef86710000000000000000000000007d6d7374515394b30284b464ce6ca7b2df9d38ca0000000000000000000000008ca23f68ffa2714a5249eb3232e78f4a8994b7160000000000000000000008617401892955b9ded8f2ca8295db2f88d55d855241 args=[0x7d6d7374515394b30284b464Ce6Ca7b2DF9d38Ca, 0x8CA23F68ffa2714a5249Eb3232E78F4A8994b716, 3135583289253396222631694322089566868378365106410049 [3.135e51]]
  • test_transfer_revert_tokenRecipient(address,uint256): Error != expected error: InsufficientBalance(0x8634902f143E655ebe74c2D60367A8a02eE963ED, 0, 2698731583617692908579130023716892581654082 [2.698e42]) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf); counterexample: calldata=0x0221ddf60000000000000000000000008634902f143e655ebe74c2d60367a8a02ee963ed00000000000000000000000000001efadbfce01301ceefc9bfc720a94e296e42 args=[0x8634902f143E655ebe74c2D60367A8a02eE963ED, 2698731583617692908579130023716892581654082 [2.698e42]]
    [FAIL: Error != expected error: InvalidSender(0x0000000000000000000000000000000000000000) != InvalidReceiver(0xB2000000000000000000009E871a2184B5aFaCCf)] test_seizeWithMemo_revertOrder_tokenRecipient_beats_zeroSender() (gas: 47044)
    [FAIL: Error != expected error: PolicyNotFound() != InvalidChildPolicy(9367487224930631684 [9.367e18])] test_createCompositePolicy_revert_invertedCompositeChild() (gas: 234691)
    [FAIL: PolicyNotFound()] test_compositePolicyChildIds_success_invertedCompositeIdReturnsSameSet() (gas: 130069)
    [FAIL: PolicyNotFound()] test_compositePolicyChildIds_success_returnsChildrenVerbatim() (gas: 130113)
    [FAIL: PolicyNotFound()] test_compositePolicyChildIds_success_returnsInvertedChildVerbatimAfterUpdate() (gas: 234463)
    [FAIL: PolicyNotFound()] test_createCompositePolicy_success_invertedSimpleChild() (gas: 130123)

@rayyan224 rayyan224 changed the title feat(b20): reject the token as a credit recipient feat(b20): reject B20-prefix addresses as credit recipients Sep 17, 2026
Comment thread test/lib/mocks/MockB20.sol Outdated

## Motivation

A holder who sends tokens to a B20 token address cannot move them back. The precompile has no code that can call `transfer` on its own behalf, so the credited balance is stuck unless an admin seizes it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Simplify Language here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • users can accidentally send tokens to a b20 token address especially when on the clipboard
  • Once tokens are sent here the tokens are lost unless the token issuer seizes those funds back to the user
  • we want to stop this accidental transfer of funds, to do so we are gating all to address form being the TO address

Comment thread test/lib/mocks/MockB20.sol Outdated
/// seize, and batchMint. Seize *from* a B20 address remains allowed so
/// already-stuck balances can be recovered.
function _requireValidReceiver(address to) internal pure {
if (to == address(0) || _hasB20Prefix(to)) revert InvalidReceiver(to);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only Gate on address(this), not if _hasB20Prefix

Comment thread test/lib/mocks/MockB20.sol Outdated
/// call `transfer` on its own behalf. Shared by transfer, mint, seize, and
/// batchMint. Seize *from* `address(this)` remains allowed so a balance
/// already stuck at the token can be recovered.
function _requireValidReceiver(address to) internal view {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_isContractAddressOrZero

Comment thread test/lib/mocks/MockB20.sol Outdated
/// Reverts `InvalidReceiver(to)` before
/// `InvalidSender(from)` so the precedence between the two
/// matches the canonical order.
function _requireNonZeroSenderAndValidReceiver(address from, address to) internal view {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove and go for verbose

@stephancill
stephancill marked this pull request as ready for review September 21, 2026 14:02
rayyan224 and others added 7 commits September 21, 2026 16:06
Prevent unrecoverable self-sends by reverting InvalidReceiver when any
credit path targets address(this). Seize from the token stays allowed so
already-stuck balances can still be recovered.

Co-authored-by: Cursor <cursoragent@cursor.com>
Widen InvalidReceiver to every factory-layout destination (0xB2 + 9
zero bytes), covering this token and every other B20 without a factory
call. Seize from a B20 address remains the recovery path.

Co-authored-by: Cursor <cursoragent@cursor.com>
The helper now rejects B20-prefix recipients in addition to zero addresses, so its old nonzero-only name was misleading.

Co-authored-by: Codex <codex-noreply@coinbase.com>
Name the transfer validation helper for its asymmetric sender and receiver checks, and use has-prefix predicates for address classification.

Co-authored-by: Codex <codex-noreply@coinbase.com>
Move byte-layout detail out of Summary into Background where it belongs,
fix it's → its typo, replace dangling "incident" with "problem this
change addresses", and comment out stale initializedSlot write in mock.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Address PR review: only reject `to == address(this)` on transfer, mint,
seize, and batchMint — not every B20-prefix address. Paste-error protection
for the token being called; sends between different B20 tokens are legal
again. Deletes `_hasB20Prefix`, drops `otherB20Recipient` unit tests and
smoke probes, rewrites the Denim changelog, and syncs interface natspec.

Also fixes a bare `_requireRole` inversion introduced in 44d38e5 that
would have blocked every role-gated post-bootstrap operation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…icate

Address PR review: drop the `_require*` wrapper indirection. Replace
`_requireValidReceiver` / `_requireNonZeroSenderAndValidReceiver` with a
verbose inlined check at every callsite (transfer, transferFrom, memo
variants, mint, mintWithMemo, seizeWithMemo, batchMint), backed by a
`_isContractAddressOrZero(address)` bool predicate. Each callsite now
reads the revert directly instead of behind a helper name. Mutation
harness updated to target the predicate body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@stephancill
stephancill force-pushed the rayyanalam/bop-743-disallow-b20-self-sends-in-base-std branch from a72d573 to f239da5 Compare September 21, 2026 14:07
Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
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.

2 participants