Skip to content

Secondary Royalties on Rarible

Rarible works by using an on-chain OrderMatcher, which internally queries the token for secondary sales royalties. This is written, in code, over here:

https://github.com/rarible/protocol-contracts/blob/master/royalties/contracts/RoyaltiesV2.sol

We could install the Rarible Royalties Package, but at the time of writing, the package is written for Solidity < 0.8, which makes it incompatible.

Instead, we will mimic the interface, so that the Rarible DAO gets the right information!

Create a new folder structure "@rarible/royalties/contracts" in the contracts folder. Then add the following files:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./LibPart.sol";
interface IRoyaltiesProvider {
    function getRoyalties(address token, uint tokenId) external returns (LibPart.Part[] memory);
}
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

library LibPart {
    bytes32 public constant TYPE_HASH = keccak256("Part(address account,uint96 value)");

    struct Part {
        address payable account;
        uint96 value;
    }

    function hash(Part memory part) internal pure returns (bytes32) {
        return keccak256(abi.encode(TYPE_HASH, part.account, part.value));
    }
}
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

library LibRoyaltiesV2 {
    /*
    * bytes4(keccak256('getRoyalties(LibAsset.AssetType)')) == 0x44c74bcc
    */
    bytes4 constant _INTERFACE_ID_ROYALTIES = 0x44c74bcc;
}
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./LibPart.sol";

interface RoyaltiesV2 {
    event RoyaltiesSet(uint256 tokenId, LibPart.Part[] royalties);

    function getRaribleV2Royalties(uint256 id) external view returns (LibPart.Part[] memory);
}

Then add another folder inside called "impl" and add the following files in there:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../LibPart.sol";

abstract contract AbstractRoyalties {
    mapping (uint256 => LibPart.Part[]) public royalties;

    function _saveRoyalties(uint256 _id, LibPart.Part[] memory _royalties) internal {
        for (uint i = 0; i < _royalties.length; i++) {
            require(_royalties[i].account != address(0x0), "Recipient should be present");
            require(_royalties[i].value != 0, "Royalty value should be positive");
            royalties[_id].push(_royalties[i]);
        }
        _onRoyaltiesSet(_id, _royalties);
    }

    function _updateAccount(uint256 _id, address _from, address _to) internal {
        uint length = royalties[_id].length;
        for(uint i = 0; i < length; i++) {
            if (royalties[_id][i].account == _from) {
                royalties[_id][i].account = payable(address(uint160(_to)));
            }
        }
    }

    function _onRoyaltiesSet(uint256 _id, LibPart.Part[] memory _royalties) virtual internal;
}
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./AbstractRoyalties.sol";
import "../RoyaltiesV2.sol";

contract RoyaltiesV2Impl is AbstractRoyalties, RoyaltiesV2 {
    function getRaribleV2Royalties(uint256 id) override external view returns (LibPart.Part[] memory) {
        return royalties[id];
    }

    function _onRoyaltiesSet(uint256 _id, LibPart.Part[] memory _royalties) override internal {
        emit RoyaltiesSet(_id, _royalties);
    }
}

If you add everything into the project, then it should look something like this:

It is the same structure as in their official Github repository. But their contracts are, at the time of writing, solidity <0.8. So, it mimics the official structure with the hope that the contracts are updated to sol >=0.8 at some point, but having the same structure as before.

If you have a look at the RoyaltiesRegistry then you see that on every token transfer, the Rarible Protocol will "ask" the token, if there are any royalties to be paid out. That is done by calling getRaribleV2Royalties. So, all our contract needs to do is expose this function. And it does it if we are extending from RoyaltiesV2Impl. Let's improve our token:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "./@rarible/royalties/contracts/impl/RoyaltiesV2Impl.sol";
import "./@rarible/royalties/contracts/LibPart.sol";
import "./@rarible/royalties/contracts/LibRoyaltiesV2.sol";

contract MinimalERC721 is ERC721, Ownable, RoyaltiesV2Impl {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIdTracker;
    constructor() ERC721("Minimal", "MIN") {}

    function mint(address _to) public onlyOwner {
        super._mint(_to, _tokenIdTracker.current());
        _tokenIdTracker.increment();        
    }

    function setRoyalties(uint _tokenId, address payable _royaltiesReceipientAddress, uint96 _percentageBasisPoints) public onlyOwner {
        LibPart.Part[] memory _royalties = new LibPart.Part[](1);
        _royalties[0].value = _percentageBasisPoints;
        _royalties[0].account = _royaltiesReceipientAddress;
        _saveRoyalties(_tokenId, _royalties);
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721) returns (bool) {
        if(interfaceId == LibRoyaltiesV2._INTERFACE_ID_ROYALTIES) {
            return true;
        }
        return super.supportsInterface(interfaceId);
    } 

}

If you open this again on the truffle developer console, you can test it:

truffle develop

migrate

let token = await MinimalERC721.deployed()

token.mint(accounts[0]);

token.setRoyalties(0, accounts[0], 1000)

Info

The Percentage is given in percentage basis points. That means, 2.5% is 250

If you now execute

token.getRaribleV2Royalties(0);

It should output that account[0] gets 10% or 1000 basis points:

Perfect, but what about Mintable? Let's do that next!


Last update: August 19, 2021