diff --git a/LSPs/LSP-18-Royalties.md b/LSPs/LSP-18-Royalties.md new file mode 100644 index 00000000..83d3c9ed --- /dev/null +++ b/LSPs/LSP-18-Royalties.md @@ -0,0 +1,270 @@ +--- +lip: 18 +title: Royalties +author: Volodymyr Lykhonis , Jake Prins +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: + +- `LSP18Royalties[]` is an [LSP2 array](./LSP-2-ERC725YJSONSchema.md) of royalties recipient addresses. +- `LSP18RoyaltiesMap` is a dynamic address mapping, which contains: + - royaties percentage allocation per recipient address. + - and the index in the `LSP18Royalties[]` array. + +The data key `LSP18RoyaltiesMap` 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 LSP18DigitalAssetMetadata SHOULD have the following data keys: + +### ERC725Y Data Keys + + +#### LSP18Royalties[] + +An array of royalties recipients addresses (see [LSP-0-ERC725Account](./LSP-0-ERC725Account.md)). + + +```json +{ + "name": "LSP18Royalties[]", + "key": "0xe010b84f60176d2f466b495da33173c1a008950ed855b4b92282e356d009bd76", + "keyType": "Array", + "valueType": "address", + "valueContent": "Address" +} +``` + +For more info about how to access each index of the `LSP18Royalties[]` array, see [ERC725Y JSON Schema > `keyType`: `Array`](https://github.com/lukso-network/LIPs/blob/master/LSPs/LSP-2-ERC725YJSONSchema.md#array) + +#### LSP18RoyaltiesMap + +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 [`LSP18Royalties[]` Array](#lsp18royalties). +- `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": "LSP18RoyaltiesMap:
", + "key": "0xe5ea91873c26eca6dec90000
", + "keyType": "Mapping", + "valueType": "(bytes8,bytes2,bytes4)", + "valueContent": "(Number,Number,Number)" +} +``` + +## Rationale + +## Implementation + +ERC725Y JSON Schema `LSP18Royalties`: +```json +[ + { + "name": "LSP18Royalties[]", + "key": "0xe010b84f60176d2f466b495da33173c1a008950ed855b4b92282e356d009bd76", + "keyType": "Array", + "valueType": "address", + "valueContent": "Address" + }, + { + "name": "LSP18RoyaltiesMap:
", + "key": "0xe5ea91873c26eca6dec90000
", + "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 InvalidLSP18RoyaltiesArrayLength(bytes invalidValue, uint256 invalidValueLength); + +error InvalidLSP18RoyaltiesAmount(uint256 amount); + +bytes32 constant _LSP18_ROYALTIES_KEY = 0xe010b84f60176d2f466b495da33173c1a008950ed855b4b92282e356d009bd76; +string constant _LSP18_ROYALTIES_MAP_KEY_PREFIX = "LSP18RoyaltiesMap"; + +uint32 constant _LSP18_ROYALTIES_BASIS = 100_000; + +library Royalties { + function setRoyalties( + address asset, + address recipient, + uint256 amount + ) internal { + if (amount > _LSP18_ROYALTIES_BASIS) { + revert InvalidLSP18RoyaltiesAmount(amount); + } + bytes32 recipientMapKey = LSP2Utils.generateMappingKey(_LSP18_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(_LSP18_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(_LSP18_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(_LSP18_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(_LSP18_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 InvalidLSP18RoyaltiesArrayLength(encodedArrayLength, encodedArrayLength.length); + } + uint256 index = newArrayLength - 1; + keys = new bytes32[](3); + values = new bytes[](3); + keys[0] = _LSP18_ROYALTIES_KEY; + values[0] = bytes.concat(bytes32(newArrayLength)); + keys[1] = LSP2Utils.generateArrayElementKeyAtIndex(_LSP18_ROYALTIES_KEY, index); + values[1] = bytes.concat(bytes20(recipient)); + keys[2] = LSP2Utils.generateMappingKey(_LSP18_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(_LSP18_ROYALTIES_KEY); + uint256 arrayLength = abi.decode(encodedArrayLength, (uint256)); + uint256 newArrayLength = arrayLength - 1; + uint64 index = _extractIndexFromMap(recipientMapValue); + bytes32 recipientKey = LSP2Utils.generateArrayElementKeyAtIndex(_LSP18_ROYALTIES_KEY, index); + if (index == arrayLength - 1) { + keys = new bytes32[](3); + values = new bytes[](3); + keys[0] = _LSP18_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] = _LSP18_ROYALTIES_KEY; + values[0] = bytes.concat(bytes32(newArrayLength)); + keys[1] = recipientMapKey; + values[1] = ""; + + bytes32 lastRecipientKey = LSP2Utils.generateArrayElementKeyAtIndex(_LSP18_ROYALTIES_KEY, newArrayLength); + bytes memory lastRecipientValue = asset.getData(lastRecipientKey); + + bytes32 lastRecipientMapKey = LSP2Utils.generateMappingKey( + _LSP18_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/).