Skip to content

Commit

Permalink
Update royalties to use compact byte array to encode small number of …
Browse files Browse the repository at this point in the history
…royalties recipients
  • Loading branch information
lykhonis committed Oct 26, 2023
1 parent f009c49 commit 9e86026
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 160 deletions.
11 changes: 11 additions & 0 deletions artifacts/abi/marketplace/lsp7/LSP7Marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@
"name": "InvalidBeneficiary",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "invalidValue",
"type": "bytes"
}
],
"name": "InvalidLSP18RoyaltiesData",
"type": "error"
},
{
"inputs": [
{
Expand Down
11 changes: 11 additions & 0 deletions artifacts/abi/marketplace/lsp8/LSP8Marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@
"name": "InvalidBeneficiary",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "invalidValue",
"type": "bytes"
}
],
"name": "InvalidLSP18RoyaltiesData",
"type": "error"
},
{
"inputs": [],
"name": "NoPendingSale",
Expand Down
251 changes: 114 additions & 137 deletions src/common/Royalties.sol
Original file line number Diff line number Diff line change
@@ -1,174 +1,151 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

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";
import {Points} from "./Points.sol";

error InvalidLSP18RoyaltiesArrayLength(bytes invalidValue, uint256 invalidValueLength);
error InvalidLSP18RoyaltiesPoints(uint32 points);
error InvalidLSP18RoyaltiesData(bytes invalidValue);

bytes32 constant _LSP18_ROYALTIES_RECIPIENTS_KEY = 0xfdd4e98ba62fdcf79cfde4cfe031a71195ae21ff3d0e29f79db24f2fe9ceb59b;
string constant _LSP18_ROYALTIES_RECIPIENTS_MAP_KEY_PREFIX = "LSP18RoyaltiesRecipientsMap";
bytes32 constant _LSP18_ROYALTIES_RECIPIENTS_KEY = 0xc0569ca6c9180acc2c3590f36330a36ae19015a19f4e85c28a7631e3317e6b9d;
bytes32 constant _LSP18_ROYALTIES_ENFORCE_PAYMENT = 0x580d62ad353782eca17b89e5900e7df3b13b6f4ca9bbc2f8af8bceb0c3d1ecc6;
uint32 constant _LSP18_ROYALTIES_BASIS = Points.BASIS;

bytes4 constant _LSP18_DEFAULT_STANDARD_INTERFACE_ID = 0xffffffff;
struct RoyaltiesInfo {
bytes4 interfaceId;
address recipient;
uint32 points;
}

library Royalties {
function setRoyalties(address asset, bytes4 interfaceId, address recipient, uint32 points) internal {
if (points > _LSP18_ROYALTIES_BASIS) {
revert InvalidLSP18RoyaltiesPoints(points);
function setRoyalties(address asset, RoyaltiesInfo memory info) internal {
if (info.points > _LSP18_ROYALTIES_BASIS) {
revert InvalidLSP18RoyaltiesPoints(info.points);
}
bytes32 recipientMapKey = LSP2Utils.generateMappingKey(_LSP18_ROYALTIES_RECIPIENTS_MAP_KEY_PREFIX, recipient);
bytes memory recipientMapValue = IERC725Y(asset).getData(recipientMapKey);
bytes32[] memory keys;
bytes[] memory values;
// removing royalties by setting it to 0
if (points == 0) {
// already removed
if (recipientMapValue.length == 0) {
return;
}
(keys, values) = _removeRoyaltiesEntry(IERC725Y(asset), recipientMapKey, recipientMapValue);
} else if (recipientMapValue.length == 0) {
// setting up royalties for the first time
(keys, values) = _addRoyaltiesEntry(IERC725Y(asset), interfaceId, recipient, points);
bytes memory entriesData = IERC725Y(asset).getData(_LSP18_ROYALTIES_RECIPIENTS_KEY);
RoyaltiesInfo[] memory entries = _decodeRoyalties(entriesData);

// find and update existing entry if any
int256 entryIndex = _indexOfRoyaltiesEntry(entries, info.recipient);

if (entryIndex < 0) {
// adding new entry
entriesData = bytes.concat(entriesData, _encodeRoyaltiesEntry(info));
} else {
// updating existing royalties
(keys, values) = _setRoyaltiesEntry(points, interfaceId, recipientMapKey, recipientMapValue);
// updating existing entry
entries[uint256(entryIndex)] = info;
entriesData = _encodeRoyalties(entries);
}
IERC725Y(asset).setDataBatch(keys, values);

IERC725Y(asset).setData(_LSP18_ROYALTIES_RECIPIENTS_KEY, entriesData);
}

function royaltiesOf(address asset, address recipient) internal view returns (bytes4 interfaceId, uint32 points) {
bytes32 key = LSP2Utils.generateMappingKey(_LSP18_ROYALTIES_RECIPIENTS_MAP_KEY_PREFIX, recipient);
bytes memory data = IERC725Y(asset).getData(key);
interfaceId = bytes4(BytesLib.slice(data, 0, 4));
points = BytesLib.toUint32(data, 20);
function royalties(address asset) internal view returns (RoyaltiesInfo[] memory) {
bytes memory value = IERC725Y(asset).getData(_LSP18_ROYALTIES_RECIPIENTS_KEY);
return _decodeRoyalties(value);
}

function royaltiesRecipients(address asset)
internal
view
returns (address[] memory recipients, uint32[] memory points)
{
bytes memory encodedLength = IERC725Y(asset).getData(_LSP18_ROYALTIES_RECIPIENTS_KEY);
// if key is not set or invalid (uint128)
if (encodedLength.length != 16) {
return (new address[](0), new uint32[](0));
}
uint128 arrayLength = BytesLib.toUint128(encodedLength, 0);
bytes32[] memory keys = new bytes32[](arrayLength);
for (uint128 i = 0; i < arrayLength;) {
keys[i] = LSP2Utils.generateArrayElementKeyAtIndex(_LSP18_ROYALTIES_RECIPIENTS_KEY, i);
function royaltiesPaymentEnforced(address asset) internal view returns (bool) {
bytes memory value = IERC725Y(asset).getData(_LSP18_ROYALTIES_ENFORCE_PAYMENT);
return value.length > 0 && value[0] != 0;
}

function _indexOfRoyaltiesEntry(RoyaltiesInfo[] memory entries, address recipient) private pure returns (int256) {
uint256 entriesCount = entries.length;
for (uint256 i = 0; i < entriesCount;) {
RoyaltiesInfo memory entry = entries[i];
if (entry.recipient == recipient) {
return int256(i);
}
unchecked {
i++;
}
}
bytes[] memory values = IERC725Y(asset).getDataBatch(keys);
recipients = new address[](arrayLength);
points = new uint32[](arrayLength);
for (uint128 i = 0; i < arrayLength;) {
recipients[i] = BytesLib.toAddress(values[i], 0);
points[i] = BytesLib.toUint32(values[i], 20);
return -1;
}

function _decodeRoyalties(bytes memory data) private pure returns (RoyaltiesInfo[] memory) {
uint256 dataLength = data.length;

// count number of entries
uint256 count = 0;
for (uint256 i = 0; i < dataLength;) {
uint16 length;
assembly {
length := mload(add(add(data, 0x2), i))
}
if (length < 4 /* interfaceId */ + 20 /* recipient */ ) {
revert InvalidLSP18RoyaltiesData(data);
}
unchecked {
i++;
i += 2 /* length */ + length;
count++;
}
}
}

function royaltiesPaymentEnforced(address asset) internal view returns (bool) {
bytes memory value = IERC725Y(asset).getData(_LSP18_ROYALTIES_ENFORCE_PAYMENT);
return value.length > 0 && value[0] != 0;
}
RoyaltiesInfo[] memory result = new RoyaltiesInfo[](count);
uint256 i = 0;
uint256 j = 0;

while (i < dataLength) {
uint16 length;
bytes4 interfaceId;
address recipient;
uint32 points;

assembly {
length := mload(add(add(data, 0x2), i))
interfaceId := mload(add(add(data, 0x4), add(i, 2)))
recipient := div(mload(add(add(data, 0x20), add(i, 6))), 0x1000000000000000000000000)
}

// optional points
if (length >= 4 /* interfaceId */ + 20 /* recipient */ + 4 /* points */ ) {
assembly {
points := mload(add(add(data, 0x4), add(i, 26)))
}
}

// asign entry
result[j] = RoyaltiesInfo(interfaceId, recipient, points);

// skip any remaining bytes as unsupported
unchecked {
i += 2 /* length */ + length;
j++;
}
}

function _setRoyaltiesEntry(
uint32 points,
bytes4 interfaceId,
bytes32 recipientMapKey,
bytes memory recipientsMapValue
) private pure returns (bytes32[] memory keys, bytes[] memory values) {
keys = new bytes32[](1);
values = new bytes[](1);
uint128 index = BytesLib.toUint128(recipientsMapValue, 0);
keys[0] = recipientMapKey;
values[0] = bytes.concat(interfaceId, bytes16(index), bytes4(points));
return result;
}

function _addRoyaltiesEntry(IERC725Y asset, bytes4 interfaceId, address recipient, uint32 points)
private
view
returns (bytes32[] memory keys, bytes[] memory values)
{
bytes memory encodedArrayLength = asset.getData(_LSP18_ROYALTIES_RECIPIENTS_KEY);
uint128 newArrayLength = 1;
if (encodedArrayLength.length == 0) {
newArrayLength = 1;
} else if (encodedArrayLength.length == 16) {
uint128 arrayLength = BytesLib.toUint128(encodedArrayLength, 0);
newArrayLength = arrayLength + 1;
} else {
revert InvalidLSP18RoyaltiesArrayLength(encodedArrayLength, encodedArrayLength.length);
function _encodeRoyaltiesEntry(RoyaltiesInfo memory entry) private pure returns (bytes memory result) {
// determine entry length
uint16 length = 4 /* interfaceId */ + 20; /* recipient */
if (entry.points > 0) {
unchecked {
length += 4; /* points */
}
}

// encode entry
result = bytes.concat(bytes2(length), entry.interfaceId, bytes20(entry.recipient));

// optional points
if (entry.points > 0) {
result = bytes.concat(result, bytes4(entry.points));
}
uint128 index = newArrayLength - 1;
keys = new bytes32[](3);
values = new bytes[](3);
keys[0] = _LSP18_ROYALTIES_RECIPIENTS_KEY;
values[0] = bytes.concat(bytes16(newArrayLength));
keys[1] = LSP2Utils.generateArrayElementKeyAtIndex(_LSP18_ROYALTIES_RECIPIENTS_KEY, index);
values[1] = bytes.concat(bytes20(recipient), bytes4(points));
keys[2] = LSP2Utils.generateMappingKey(_LSP18_ROYALTIES_RECIPIENTS_MAP_KEY_PREFIX, recipient);
values[2] = bytes.concat(interfaceId, bytes16(index), bytes4(points));
}

function _removeRoyaltiesEntry(IERC725Y asset, bytes32 recipientMapKey, bytes memory recipientsMapValue)
private
view
returns (bytes32[] memory keys, bytes[] memory values)
{
bytes memory encodedArrayLength = asset.getData(_LSP18_ROYALTIES_RECIPIENTS_KEY);
uint128 arrayLength = BytesLib.toUint128(encodedArrayLength, 0);
uint128 newArrayLength = arrayLength - 1;
uint128 index = BytesLib.toUint128(recipientsMapValue, 4);
bytes32 recipientKey = LSP2Utils.generateArrayElementKeyAtIndex(_LSP18_ROYALTIES_RECIPIENTS_KEY, index);
if (index == arrayLength - 1) {
keys = new bytes32[](3);
values = new bytes[](3);
keys[0] = _LSP18_ROYALTIES_RECIPIENTS_KEY;
values[0] = bytes.concat(bytes16(newArrayLength));
keys[1] = recipientMapKey;
values[1] = "";
keys[2] = recipientKey;
values[2] = "";
} else {
keys = new bytes32[](5);
values = new bytes[](5);
keys[0] = _LSP18_ROYALTIES_RECIPIENTS_KEY;
values[0] = bytes.concat(bytes16(newArrayLength));
keys[1] = recipientMapKey;
values[1] = "";

bytes32 lastRecipientKey =
LSP2Utils.generateArrayElementKeyAtIndex(_LSP18_ROYALTIES_RECIPIENTS_KEY, newArrayLength);
bytes memory lastRecipientValue = asset.getData(lastRecipientKey);

bytes32 lastRecipientMapKey = LSP2Utils.generateMappingKey(
_LSP18_ROYALTIES_RECIPIENTS_MAP_KEY_PREFIX, BytesLib.toAddress(lastRecipientValue, 0)
);
bytes memory lastRecipientsMapValue = asset.getData(lastRecipientMapKey);

keys[2] = recipientKey;
values[2] = lastRecipientValue;
keys[3] = lastRecipientKey;
values[3] = "";
keys[4] = lastRecipientMapKey;
values[4] = bytes.concat(
BytesLib.slice(lastRecipientsMapValue, 0, 4),
bytes16(index),
BytesLib.slice(lastRecipientsMapValue, 20, 4)
);
function _encodeRoyalties(RoyaltiesInfo[] memory entries) private pure returns (bytes memory) {
bytes memory result = new bytes(0);
uint256 count = entries.length;
for (uint256 i = 0; i < count;) {
result = bytes.concat(result, _encodeRoyaltiesEntry(entries[i]));
unchecked {
i++;
}
}
return result;
}
}
15 changes: 8 additions & 7 deletions src/marketplace/common/Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/se
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import {Withdrawable} from "../../common/Withdrawable.sol";
import {Points} from "../../common/Points.sol";
import {Royalties} from "../../common/Royalties.sol";
import {Royalties, RoyaltiesInfo} from "../../common/Royalties.sol";
import {IParticipant} from "../IParticipant.sol";

abstract contract Base is OwnableUnset, ReentrancyGuardUpgradeable, PausableUpgradeable, Withdrawable {
Expand Down Expand Up @@ -77,13 +77,14 @@ abstract contract Base is OwnableUnset, ReentrancyGuardUpgradeable, PausableUpgr
returns (uint256 totalAmount, address[] memory recipients, uint256[] memory amounts)
{
totalAmount = 0;
(address[] memory addresses, uint32[] memory points) = Royalties.royaltiesRecipients(asset);
recipients = addresses;
amounts = new uint256[](points.length);
uint256 count = points.length;
RoyaltiesInfo[] memory royalties = Royalties.royalties(asset);
recipients = new address[](royalties.length);
amounts = new uint256[](royalties.length);
uint256 count = royalties.length;
for (uint256 i = 0; i < count;) {
assert(Points.isValid(points[i]));
uint256 amount = Points.realize(totalPrice, points[i]);
assert(Points.isValid(royalties[i].points));
uint256 amount = Points.realize(totalPrice, royalties[i].points);
recipients[i] = royalties[i].recipient;
amounts[i] = amount;
totalAmount += amount;
unchecked {
Expand Down
13 changes: 5 additions & 8 deletions test/marketplace/lsp7/LSP7Marketplace.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import {
ITransparentUpgradeableProxy,
TransparentUpgradeableProxy
} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {_INTERFACEID_LSP0} from "@lukso/lsp-smart-contracts/contracts/LSP0ERC725Account/LSP0Constants.sol";
import {UniversalProfile} from "@lukso/lsp-smart-contracts/contracts/UniversalProfile.sol";
import {Module, MARKETPLACE_ROLE} from "../../../src/marketplace/common/Module.sol";
import {Points} from "../../../src/common/Points.sol";
import {Royalties, _LSP18_DEFAULT_STANDARD_INTERFACE_ID} from "../../../src/common/Royalties.sol";
import {Royalties, RoyaltiesInfo} from "../../../src/common/Royalties.sol";
import {Base} from "../../../src/marketplace/common/Base.sol";
import {LSP7Listings} from "../../../src/marketplace/lsp7/LSP7Listings.sol";
import {LSP7Offers} from "../../../src/marketplace/lsp7/LSP7Offers.sol";
Expand Down Expand Up @@ -263,12 +264,8 @@ contract LSP7MarketplaceTest is Test {
address royaltiesRecipient1 = vm.addr(101);

vm.startPrank(owner);
Royalties.setRoyalties(
address(asset), _LSP18_DEFAULT_STANDARD_INTERFACE_ID, royaltiesRecipient0, royaltiesPoints0
);
Royalties.setRoyalties(
address(asset), _LSP18_DEFAULT_STANDARD_INTERFACE_ID, royaltiesRecipient1, royaltiesPoints1
);
Royalties.setRoyalties(address(asset), RoyaltiesInfo(_INTERFACEID_LSP0, royaltiesRecipient0, royaltiesPoints0));
Royalties.setRoyalties(address(asset), RoyaltiesInfo(_INTERFACEID_LSP0, royaltiesRecipient1, royaltiesPoints1));
vm.stopPrank();

(UniversalProfile alice,) = deployProfile();
Expand Down Expand Up @@ -348,7 +345,7 @@ contract LSP7MarketplaceTest is Test {
function testFuzz_Revert_BuyIfRoyltiesExceedThreshold() public {
vm.startPrank(owner);
marketplace.setRoyaltiesThresholdPoints(1);
Royalties.setRoyalties(address(asset), _LSP18_DEFAULT_STANDARD_INTERFACE_ID, address(100), 2);
Royalties.setRoyalties(address(asset), RoyaltiesInfo(_INTERFACEID_LSP0, address(100), 2));
vm.stopPrank();

(UniversalProfile alice,) = deployProfile();
Expand Down
Loading

0 comments on commit 9e86026

Please sign in to comment.