problem · erc721 safe transfer problems

ERC-721 safe transfer problems

safeTransferFrom is only half a hook.

ERC-721 LSP1 LSP7 LSP8

It checks IERC721Receiver — only, only, only. Every other asset type strands silently.

ERC-721 onERC721Received(...) returns (bytes4)
one asset type narrow hook
LSP1 universalReceiver(typeId, data)
one hook for everything typeId-routed
quotable answer

ERC-721's `safeTransferFrom` checks `onERC721Received` on the recipient — but the check is opt-in (non-safe `transferFrom` still ships), and every asset standard defines its own receiver: `IERC721Receiver`, `IERC1155Receiver`, `IERC1155BatchReceiver`, ERC-777 `tokensReceived`. A multi-asset contract implements four interfaces; a legacy contract silently strands ERC-20s and un-safe ERC-721 transfers. LSP1 on LUKSO defines one interface — `universalReceiver(bytes32 typeId, bytes data)` — that LSP7, LSP8, and value transfers all call, with typeId as the discriminator.

LUKSO route

If you're dealing with ERC-721 safe transfer problems, the LUKSO route is LSP1 + LSP7 + LSP8. LSP1 is a single universalReceiver hook with a typeId discriminator. LSP7, LSP8, and value transfers all call it. Multi-asset contracts stop implementing four receiver interfaces; account contracts react through a Universal Receiver Delegate.

Why this breaks

safeTransferFrom is real safety, but it is narrow. It only checks IERC721Receiver, only for that one call, only when the caller chose the “safe” variant. The non-safe transferFrom still ships, and most ERC-20 transfers go straight to a balance update with no recipient interaction at all.

In practice, every asset standard has its own receiver shape — IERC721Receiver, IERC1155Receiver, ERC-777 tokensReceived. A multi-asset contract implements each one. And there is still no general “I received value of some kind” hook.

What people try

Implement every receiver interface

Boilerplate that grows per asset standard. Required for compliance, doesn’t compose into shared policy.

OpenZeppelin Holder mixins

ERC721Holder, ERC1155Holder. Accepts everything, defers the policy question.

Defensive transfer-then-call patterns

Ad hoc per integration. Not interoperable.

Sweeper / rescue contracts

Recovers stranded assets after the fact. Treats the symptom.

How LSP solves it

LSP1 defines one hook:

function universalReceiver(bytes32 typeId, bytes calldata data)
  external payable returns (bytes memory);

LSP7 and LSP8 transfers call it on both sender and recipient, with a typeId that tells the receiver what kind of interaction it is (token sent, token received, asset registered, etc.).

A Universal Profile uses a Universal Receiver Delegate to dispatch: register received assets in LSP5, reject spam by typeId, or run app-specific logic. One hook, every asset type, declared policy.

continue at the source