Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
270 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
--- | ||
lip: 17 | ||
title: Royalties | ||
author: Volodymyr Lykhonis <vlad@universal.page>, Jake Prins <jake@universal.page> | ||
discussions-to: https://discord.gg/E2rJPP4 | ||
status: Draft | ||
type: LSP | ||
created: 2022-11-23 | ||
requires: LSP2, LSP4 | ||
--- | ||
|
||
## Simple Summary | ||
This standard describes a set of [ERC725Y](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-725.md) data key values to store royalties recipient addresses and corresponding percentages in a [ERC725Y](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-725.md) smart contract. | ||
|
||
## Abstract | ||
This data key value standard describes a set of data keys that can be added to an [ERC725Y](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-725.md) smart contract to describe royalties: | ||
|
||
- `LSP17Royalties[]` is an [LSP2 array](./LSP-2-ERC725YJSONSchema.md) of royalties recipient addresses. | ||
- `LSP17RoyaltiesMap` is a dynamic address mapping, which contains: | ||
- royaties percentage allocation per recipient address. | ||
- and the index in the `LSP17Royalties[]` array. | ||
|
||
The data key `LSP17RoyaltiesMap` exists so that smart contracts can detect if an address is present in the array (e.g. as done in the [LSP1-UniversalReceiverDelegate](./LSP-1-UniversalReceiver.md)). | ||
|
||
## Motivation | ||
This standard allows to create a decentralised asset royalties allocations by a smart contract. | ||
|
||
## Specification | ||
Every contract that supports the LSP17DigitalAssetMetadata SHOULD have the following data keys: | ||
|
||
### ERC725Y Data Keys | ||
|
||
|
||
#### LSP17Royalties[] | ||
|
||
An array of royalties recipients addresses (see [LSP-0-ERC725Account](./LSP-0-ERC725Account.md)). | ||
|
||
|
||
```json | ||
{ | ||
"name": "LSP17Royalties[]", | ||
"key": "0xdec0515ce1218e77ec83befd2effbad7360a0078ebf1c63210d7b3fdce74a8df", | ||
"keyType": "Array", | ||
"valueType": "address", | ||
"valueContent": "Address" | ||
} | ||
``` | ||
|
||
For more info about how to access each index of the `LSP17Royalties[]` array, see [ERC725Y JSON Schema > `keyType`: `Array`](https://github.com/lukso-network/LIPs/blob/master/LSPs/LSP-2-ERC725YJSONSchema.md#array) | ||
|
||
#### LSP17RoyaltiesMap | ||
|
||
References royalties amounts of each recipient addresses. | ||
|
||
The data value MUST be constructed as follows: `bytes8(indexNumber) + bytes2(type) + bytesN`. Where: | ||
- `indexNumber` = the index in the [`LSP17Royalties[]` Array](#lsp17royalties). | ||
- `type` = the type of a payload: | ||
- `0` = percentage | ||
- `bytes4(amountNumber)` = the percentage of royalties allocation in points with `100_000` basis. e.g. `15%` is `15_000` points, and `1.5%` is `1_500` points. | ||
- `1` = fixed amount in `LYX` | ||
- `bytes32(amount)` = wei amount | ||
|
||
|
||
```json | ||
{ | ||
"name": "LSP17RoyaltiesMap:<address>", | ||
"key": "0x98f539625bcd2ffd06d50000<address>", | ||
"keyType": "Mapping", | ||
"valueType": "(bytes8,bytes2,bytes4)", | ||
"valueContent": "(Number,Number,Number)" | ||
} | ||
``` | ||
|
||
## Rationale | ||
|
||
## Implementation | ||
|
||
ERC725Y JSON Schema `LSP15Royalties`: | ||
```json | ||
[ | ||
{ | ||
"name": "LSP17Royalties[]", | ||
"key": "0xdec0515ce1218e77ec83befd2effbad7360a0078ebf1c63210d7b3fdce74a8df", | ||
"keyType": "Array", | ||
"valueType": "address", | ||
"valueContent": "Address" | ||
}, | ||
{ | ||
"name": "LSP17RoyaltiesMap:<address>", | ||
"key": "0x98f539625bcd2ffd06d50000<address>", | ||
"keyType": "Mapping", | ||
"valueType": "(bytes8,bytes2,bytes4)", | ||
"valueContent": "(Number,Number,Number)" | ||
} | ||
] | ||
``` | ||
|
||
```solidity | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.9; | ||
import {BytesLib} from "solidity-bytes-utils/contracts/BytesLib.sol"; | ||
import {IERC725Y} from "@erc725/smart-contracts/contracts/interfaces/IERC725Y.sol"; | ||
import {LSP2Utils} from "@lukso/lsp-smart-contracts/contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol"; | ||
error InvalidLSP17RoyaltiesArrayLength(bytes invalidValue, uint256 invalidValueLength); | ||
error InvalidLSP17RoyaltiesAmount(uint256 amount); | ||
bytes32 constant _LSP17_ROYALTIES_KEY = 0xdec0515ce1218e77ec83befd2effbad7360a0078ebf1c63210d7b3fdce74a8df; | ||
string constant _LSP17_ROYALTIES_MAP_KEY_PREFIX = "LSP17RoyaltiesMap"; | ||
uint32 constant _LSP17_ROYALTIES_BASIS = 100_000; | ||
library Royalties { | ||
function setRoyalties( | ||
address asset, | ||
address recipient, | ||
uint256 amount | ||
) internal { | ||
if (amount > _LSP17_ROYALTIES_BASIS) { | ||
revert InvalidLSP17RoyaltiesAmount(amount); | ||
} | ||
bytes32 recipientMapKey = LSP2Utils.generateMappingKey(_LSP17_ROYALTIES_MAP_KEY_PREFIX, recipient); | ||
bytes memory recipientMapValue = IERC725Y(asset).getData(recipientMapKey); | ||
// removing royalties by setting it to 0 | ||
if (amount == 0) { | ||
// already removed | ||
if (bytes12(recipientMapValue) == bytes12(0)) { | ||
return; | ||
} | ||
(bytes32[] memory keys, bytes[] memory values) = _removeRoyaltiesEntry( | ||
IERC725Y(asset), | ||
recipientMapKey, | ||
recipientMapValue | ||
); | ||
IERC725Y(asset).setData(keys, values); | ||
} else if (bytes12(recipientMapValue) == bytes12(0)) { | ||
// setting up royalties for the first time | ||
(bytes32[] memory keys, bytes[] memory values) = _addRoyaltiesEntry(IERC725Y(asset), recipient, amount); | ||
IERC725Y(asset).setData(keys, values); | ||
} else { | ||
// updating existing royalties | ||
(bytes32[] memory keys, bytes[] memory values) = _setRoyaltiesEntry( | ||
amount, | ||
recipientMapKey, | ||
recipientMapValue | ||
); | ||
IERC725Y(asset).setData(keys, values); | ||
} | ||
} | ||
function royaltiesOf(address asset, address recipient) internal view returns (uint256) { | ||
bytes32 recipientMapKey = LSP2Utils.generateMappingKey(_LSP17_ROYALTIES_MAP_KEY_PREFIX, recipient); | ||
bytes memory recipientMapValue = IERC725Y(asset).getData(recipientMapKey); | ||
return BytesLib.toUint32(recipientMapValue, 10); | ||
} | ||
function royaltiesRecipients(address asset) internal view returns (address[] memory) { | ||
bytes memory encodedLength = IERC725Y(asset).getData(_LSP17_ROYALTIES_KEY); | ||
// if key is not set or invalid (uint256) | ||
if (encodedLength.length != 32) { | ||
return new address[](0); | ||
} | ||
uint256 arrayLength = abi.decode(encodedLength, (uint256)); | ||
bytes32[] memory keys = new bytes32[](arrayLength); | ||
for (uint256 i = 0; i < arrayLength; i++) { | ||
keys[i] = LSP2Utils.generateArrayElementKeyAtIndex(_LSP17_ROYALTIES_KEY, i); | ||
} | ||
bytes[] memory values = IERC725Y(asset).getData(keys); | ||
address[] memory result = new address[](arrayLength); | ||
for (uint256 i = 0; i < arrayLength; i++) { | ||
result[i] = address(bytes20(values[i])); | ||
} | ||
return result; | ||
} | ||
function _setRoyaltiesEntry( | ||
uint256 amount, | ||
bytes32 recipientMapKey, | ||
bytes memory recipientMapValue | ||
) private pure returns (bytes32[] memory keys, bytes[] memory values) { | ||
keys = new bytes32[](1); | ||
values = new bytes[](1); | ||
uint64 index = _extractIndexFromMap(recipientMapValue); | ||
keys[0] = recipientMapKey; | ||
values[0] = bytes.concat(bytes8(index), bytes2(0), bytes4(uint32(amount))); | ||
} | ||
function _addRoyaltiesEntry( | ||
IERC725Y asset, | ||
address recipient, | ||
uint256 amount | ||
) private view returns (bytes32[] memory keys, bytes[] memory values) { | ||
bytes memory encodedArrayLength = asset.getData(_LSP17_ROYALTIES_KEY); | ||
uint256 newArrayLength; | ||
if (encodedArrayLength.length == 0) { | ||
newArrayLength = 1; | ||
} else if (encodedArrayLength.length == 32) { | ||
uint256 arrayLength = abi.decode(encodedArrayLength, (uint256)); | ||
newArrayLength = arrayLength + 1; | ||
} else { | ||
revert InvalidLSP17RoyaltiesArrayLength(encodedArrayLength, encodedArrayLength.length); | ||
} | ||
uint256 index = newArrayLength - 1; | ||
keys = new bytes32[](3); | ||
values = new bytes[](3); | ||
keys[0] = _LSP17_ROYALTIES_KEY; | ||
values[0] = bytes.concat(bytes32(newArrayLength)); | ||
keys[1] = LSP2Utils.generateArrayElementKeyAtIndex(_LSP17_ROYALTIES_KEY, index); | ||
values[1] = bytes.concat(bytes20(recipient)); | ||
keys[2] = LSP2Utils.generateMappingKey(_LSP17_ROYALTIES_MAP_KEY_PREFIX, recipient); | ||
values[2] = bytes.concat(bytes8(uint64(index)), bytes2(0), bytes4(uint32(amount))); | ||
} | ||
function _extractIndexFromMap(bytes memory mapValue) private pure returns (uint64) { | ||
bytes memory val = BytesLib.slice(mapValue, 0, 8); | ||
return BytesLib.toUint64(val, 0); | ||
} | ||
function _removeRoyaltiesEntry( | ||
IERC725Y asset, | ||
bytes32 recipientMapKey, | ||
bytes memory recipientMapValue | ||
) private view returns (bytes32[] memory keys, bytes[] memory values) { | ||
bytes memory encodedArrayLength = asset.getData(_LSP17_ROYALTIES_KEY); | ||
uint256 arrayLength = abi.decode(encodedArrayLength, (uint256)); | ||
uint256 newArrayLength = arrayLength - 1; | ||
uint64 index = _extractIndexFromMap(recipientMapValue); | ||
bytes32 recipientKey = LSP2Utils.generateArrayElementKeyAtIndex(_LSP17_ROYALTIES_KEY, index); | ||
if (index == arrayLength - 1) { | ||
keys = new bytes32[](3); | ||
values = new bytes[](3); | ||
keys[0] = _LSP17_ROYALTIES_KEY; | ||
values[0] = bytes.concat(bytes32(newArrayLength)); | ||
keys[1] = recipientMapKey; | ||
values[1] = ""; | ||
keys[2] = recipientKey; | ||
values[2] = ""; | ||
} else { | ||
keys = new bytes32[](5); | ||
values = new bytes[](5); | ||
keys[0] = _LSP17_ROYALTIES_KEY; | ||
values[0] = bytes.concat(bytes32(newArrayLength)); | ||
keys[1] = recipientMapKey; | ||
values[1] = ""; | ||
bytes32 lastRecipientKey = LSP2Utils.generateArrayElementKeyAtIndex(_LSP17_ROYALTIES_KEY, newArrayLength); | ||
bytes memory lastRecipientValue = asset.getData(lastRecipientKey); | ||
bytes32 lastRecipientMapKey = LSP2Utils.generateMappingKey( | ||
_LSP17_ROYALTIES_MAP_KEY_PREFIX, | ||
address(bytes20(lastRecipientValue)) | ||
); | ||
bytes memory lastRecipientMapValue = asset.getData(lastRecipientMapKey); | ||
bytes memory data = BytesLib.slice(lastRecipientMapValue, 8, 6); | ||
keys[2] = recipientKey; | ||
values[2] = lastRecipientValue; | ||
keys[3] = lastRecipientKey; | ||
values[3] = ""; | ||
keys[4] = lastRecipientMapKey; | ||
values[4] = bytes.concat(bytes8(index), data); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
## Copyright | ||
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). |