lip | title | author | discussions-to | status | type | created | requires |
---|---|---|---|---|---|---|---|
1 |
Universal Receiver |
JG Carvalho (@jgcarv), Fabian Vogelsteller <@frozeman> |
Draft |
LSP |
2019-09-01 |
ERC165 |
An entry function enabling a contract to receive arbitrary information, handle incoming or outgoing transactions/transfers and contract interactions.
LSP1, introduces the Universal Receiver, a unique feature that revolutionizes how transactions, transfers, and contract interactions are handled on the blockchain. Imagine a world where every incoming action to your blockchain identity or asset is not just received but also intelligently responded to. That's what the Universal Receiver does.
By implementing LSP1, any smart contract or wallet can detect incoming transactions and automatically trigger customized responses. This could range from accepting certain types of assets, rejecting unwanted tokens, or even executing specific actions when receiving a message (e.g: store automatically certain received assets in a digital vault). The beauty of the Universal Receiver lies in its flexibility and the ability to delegate these responses to external contracts called Universal Receiver Delegates, which can have their own custom mechanisms for different purposes.
This ensures that your digital presence on the blockchain is not just passive but active, by managing and responding to interactions according to your own predefined rules. Whether it is for personal wallets, digital identities, or complex decentralized applications, the Universal Receiver provides a foundational layer for more secure, efficient, and user-friendly blockchain interactions.
Similar to a smart contract's fallback function, which allows a contract to be notified of an incoming transaction with a value, the universalReceiver(bytes32,bytes)
function allows for any contract to receive information about any interaction.
This allows receiving contracts to react on incoming transfers or other interactions.
There is often the need to inform other smart contracts about actions another smart contract did perform.
A good example are token transfers, where the token smart contract should inform receiving contracts about the transfer.
By creating a universal function that many smart contracts implement, receiving of asset and information can be unified.
In cases where smart contracts function as a profile or wallet over a long time, an upgradable universal receiver can allow for future assets to be received, without the need for the interface to be changed.
LSP1-UniversalReceiver interface id according to ERC165: 0x6bb56a14
.
Smart contracts implementing the LSP1-UniversalReceiver standard MUST implement the ERC165 supportsInterface(..)
function and MUST support ERC165 and LSP1 interface ids.
Every contract that complies with the LSP1-UniversalReceiver standard MUST implement:
function universalReceiver(bytes32 typeId, bytes memory data) external payable returns (bytes memory)
Allows to be called by any external contract to inform the contract about any incoming transfers, interactions or simple information.
The universalReceiver(...)
function can be customized to react on a different aspect of the call such as the typeId
, the data sent, the caller or the value sent to the function (e.g, reacting on a token or a vault transfer).
Parameters:
-
typeId
is the hash of a standard, or the type relative to thedata
received. -
data
is a byteArray of arbitrary data. Receiving contracts SHOULD take thetypeId
in consideration to properly decode thedata
.
Returns: bytes
which can be used to encode response values.
Note: The
universalReceiver(...)
function COULD be allowed to return no data (no return as the equivalent of the opcode instructionreturn(memory_pointer, 0)
). If anybytes
data is returned, bytes not conforming to the default ABI encoding will result in a revert. See the specification for the abi-encoding ofbytes
for more details.
event UniversalReceiver(address indexed from, uint256 indexed value, bytes32 indexed typeId, bytes receivedData, bytes returnedValue);
This event MUST be emitted when the universalReceiver
function is successfully executed.
Values:
-
from
is the address calling theuniversalReceiver(..)
function. -
value
is the amount of value sent to theuniversalReceiver(..)
function. -
typeId
is the hash of a standard, or the type relative to thedata
received. -
receivedData
is a byteArray of arbitrary data received. -
returnedValue
is the data returned by theuniversalReceiver(..)
function.
UniversalReceiver delegation allows to forward the universalReceiver(..)
call on one contract to another external contract, allowing for upgradeability and changing behaviour of the initial universalReceiver(..)
call.
The ability to react to upcoming actions with a logic hardcoded within the universalReceiver(..)
function comes with limitations, as only a fixed functionality can be coded or the UniversalReceiver
event be fired.
This section explains a way to forward the call to the universalReceiver(..)
function to an external smart contract to extend and change functionality over time.
The delegation works by simply forwarding a call to the universalReceiver(..)
function to a delegated smart contract calling the universalReceiverDelegate(..)
function on the external smart contract.
As the external smart contract doesn't know about the initial msg.sender
and the msg.value
, this specification proposes to add these values as arguments. This allows the external contract to know the full context of the initial universalReceiver
call and react accordingly.
LSP1-UniversalReceiverDelegate interface id according to ERC165: 0xa245bbda
.
The UniversalReceiverDelegate is an optional extension. It allows the universalReceiver(..)
function to delegate its functionality to an external contract that can be customized to react differently based on the typeId
and the data
received.
function universalReceiverDelegate(address caller, uint256 value, bytes32 typeId, bytes memory data) external returns (bytes memory);
Allows to be called by any external contract when an address wants to delegate its universalReceiver functionality to another smart contract.
Parameters:
-
caller
is the address calling theuniversalReceiver
function. -
value
is the amount of value sent to theuniversalReceiver
function. -
typeId
is the hash of a standard, or the type relative to thedata
received. -
data
is a byteArray of arbitrary data. Receiving contracts should take thetypeId
in consideration to properly decode thedata
.
Returns: bytes
, which can be used to encode response values.
If the contract implementing the LSP1 standard is an ERC725Y, the address of the UniversalReceiverDelegate contract COULD be stored under the following ERC725Y data key:
{
"name": "LSP1UniversalReceiverDelegate",
"key": "0x0cfc51aec37c55a4d0b1a65c6255c4bf2fbdf6277f3cc0730c45b828b6db8b47",
"keyType": "Singleton",
"valueType": "address",
"valueContent": "Address"
}
Additionally, some specific UniversalReceiverDelegate contracts COULD be mapped to react on specific typeId
. The address of each of these contracts for each specific typeId
COULD be stored under the following ERC725Y data key:
{
"name": "LSP1UniversalReceiverDelegate:<bytes32>",
"key": "0x0cfc51aec37c55a4d0b10000<bytes32>",
"keyType": "Mapping",
"valueType": "address",
"valueContent": "Address"
}
In scenarios where both a default UniversalReceiverDelegate and a custom UniversalReceiverDelegate are present, it is recommended to prioritize the default one first. This ensures that more generic conditions are assessed first, providing a consistent and broad level of handling. Subsequently, the custom should be utilized to address specific conditions associated with the typeId.
The <bytes32\>
in the data key name corresponds to the typeId
passed to the universalReceiver(..)
function. For example, for the typeId 0xcafecafecafecafecafecafecafecafecafecafebeefbeefbeefbeefbeefbeef
, the data key above will be constructed as follow:
0x0cfc51aec37c55a4d0b10000cafecafecafecafecafecafecafecafecafecafe
This is an abstraction of the ideas behind ERC223 and ERC777, that contracts are called when they are receiving tokens or other assets.
With this proposal, we can allow contracts to receive any information in a generic manner over a standardised interface.
As this function is generic and only the sent typeId
changes, smart contract accounts that can upgrade its behaviour using the UniversalReceiverDelegate technique can be created.
The UniversalReceiverDelegate functionality COULD be implemented using call
, or delegatecall
, both of which have different security properties.
An implementation can be found in the lukso-network/lsp-smart-contracts repository.
After transferring token from TokenABC
to MyWallet
, the owner of MyWallet
contract can know, by looking at the emitted UniversalReceiver event, that the typeId
is _TOKEN_RECEIVING_HASH
.
Enabling the owner to know the token sender address and the amount sent by looking into the data
.
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
contract TokenABC {
// Hash of the word `_TOKEN_RECEIVING_HASH`
bytes32 constant public _TOKEN_RECEIVING_HASH = 0x7901c95ea4b5fe1fba45bb8c10d7ddabf715dea547785f933d8be283925c4883;
bytes4 _INTERFACE_ID_LSP1 = 0x6bb56a14;
function sendToken(address to, uint256 amount) public {
balance[msg.sender] -= amount;
balance[to] += amount;
_informTheReceiver(to, amount);
}
function _informTheReceiver(address receiver, uint256 amount) internal {
// If the contract receiving the tokens supports LSP1 InterfaceID then call the unviersalReceiver function
if(ERC165Checker.supportsInterface(receiver,_INTERFACE_ID_LSP1)){
ILSP1(receiver).universalReceiver(_TOKEN_RECEIVING_HASH, abi.encodePacked(msg.sender, amount));
}
}
}
contract MyWallet is ERC165, ILSP1 {
bytes4 _INTERFACE_ID_LSP1 = 0x6bb56a14;
constructor() public {
_registerInterface(_INTERFACE_ID_LSP1);
}
function universalReceiver(bytes32 typeId, bytes memory data) public payable returns (bytes memory) {
emit UniversalReceiver(msg.sender, msg.value, typeId, data, 0x);
return 0x0;
}
}
This example is the same example written above except that MyWallet
contract now delegates the universalReceiver functionality to a UniversalReceiverDelegate contract.
The TokenABC
contract will inform the MyWallet
contract about the transfer by calling the universalReceiver(..)
function. This function will then call the universalReceiver(..)
function on the UniversalReceiverDelegate address set by the owner, to react on the transfer accordingly.
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
contract TokenABC {
// Hash of the word `_TOKEN_RECEIVING_HASH`
bytes32 constant public _TOKEN_RECEIVING_HASH = 0x7901c95ea4b5fe1fba45bb8c10d7ddabf715dea547785f933d8be283925c4883;
bytes4 _INTERFACE_ID_LSP1 = 0x6bb56a14;
function sendToken(address to, uint256 amount) public onlyOwner {
balance[to] += amount;
_informTheReceiver(to, amount);
}
function _informTheReceiver(address receiver, uint256 amount) internal {
// If the contract receiving the tokens supports LSP1 InterfaceID then call the unviersalReceiver function
if(ERC165Checker.supportsInterface(receiver,_INTERFACE_ID_LSP1)){
ILSP1(receiver).universalReceiver(_TOKEN_RECEIVING_HASH, abi.encodePacked(address(this),amount));
}
}
}
contract MyWallet is ERC165, ILSP1 {
bytes4 _INTERFACE_ID_LSP1 = 0x6bb56a14;
bytes4 _INTERFACE_ID_LSP1_DELEGATE = 0xa245bbda;
address public universalReceiverDelegate;
constructor() public {
_registerInterface(_INTERFACE_ID_LSP1);
}
function setUniversalReceiverDelegate(address _newUniversalReceiverDelegate) public onlyOwner {
// The address set SHOULD support LSP1Delegate InterfaceID
universalReceiverDelegate = _newUniversalReceiverDelegate;
}
function universalReceiver(bytes32 typeId, bytes memory data) public payable returns (bytes memory) {
// if the address set as universalReceiverDelegate supports LSP1Delegate then call the universalReceiverDelegate function
if(ERC165Checker.supportsInterface(universalReceiverDelegate,_INTERFACE_ID_LSP1_DELEGATE)){
// Call the universalReceiverDelegate function on universalReceiverDelegate address
returnedData = ILSP1(universalReceiverDelegate).universalReceiverDelegate(msg.sender, msg.value, typeId, data);
emit UniversalReceiver(msg.sender, msg.value, typeId, data, returnedData);
return returnedData;
}
}
contract UniversalReceiverDelegate is ERC165, ILSP1 {
bytes4 _INTERFACE_ID_LSP1_DELEGATE = 0xa245bbda;
constructor() public {
_registerInterface(_INTERFACE_ID_LSP1_DELEGATE);
}
function universalReceiverDelegate(address caller, uint256 value, bytes32 typeId, bytes memory data) public returns (bytes memory) {
// Any logic could be written here:
// - Interact with DeFi protocol contract to sell the new tokens received automatically.
// - Register the token received on other registry contract.
// - Allow only tokens with `_TOKEN_RECEIVING_HASH` hash and reject the others.
// - revert; so in this way the wallet will have the option to reject any token.
}
}
interface ILSP1 /* is ERC165 */ {
event UniversalReceiver(address indexed from, uint256 value, bytes32 indexed typeId, bytes receivedData, bytes returnedValue);
function universalReceiver(bytes32 typeId, bytes memory data) external payable returns (bytes memory);
}
interface ILSP1Delegate /* is ERC165 */ {
function universalReceiverDelegate(address caller, uint256 value, bytes32 typeId, bytes memory data) external returns (bytes memory);
}
Copyright and related rights waived via CC0.