How to create multiple royalties on OpenSea

Here is the code for the QQL Collection, I have been researching this collection on the main-net and I discovered that it used 2 royalties at once even though OpenSea uses EIP-2981 which takes only one royalty. See link:


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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

import "./ERC721TokenUriDelegate.sol";
import "./ERC721OperatorFilter.sol";
import "./MintPass.sol";

contract QQL is
    MintPass immutable pass_;
    uint256 nextTokenId_ = 1;
    mapping(uint256 => bytes32) tokenSeed_;
    mapping(bytes32 => uint256) seedToTokenId_;
    mapping(uint256 => string) scriptPieces_;

    /// By default, an artist has the right to mint all of their seeds. However,
    /// they may irrevocably transfer that right, at which point the current owner
    /// of the right has exclusive opportunity to mint it.
    mapping(bytes32 => address) seedOwners_;
    /// If seed approval is given, then the approved party may claim rights for any
    /// seed.
    mapping(address => mapping(address => bool)) approvalForAllSeeds_;

    mapping(uint256 => address payable) tokenRoyaltyRecipient_;
    address payable projectRoyaltyRecipient_;
    uint256 constant PROJECT_ROYALTY_BPS = 500; // 5%
    uint256 constant TOKEN_ROYALTY_BPS = 200; // 2%
    uint256 immutable unlockTimestamp_;
    uint256 immutable maxPremintPassId_;

    event SeedTransfer(
        address indexed from,
        address indexed to,
        bytes32 indexed seed
    event ApprovalForAllSeeds(
        address indexed owner,
        address indexed operator,
        bool approved

    event TokenRoyaltyRecipientChange(
        uint256 indexed tokenId,
        address indexed newRecipient

    event ProjectRoyaltyRecipientChange(address indexed newRecipient);

        MintPass pass,
        uint256 maxPremintPassId,
        uint256 unlockTimestamp
    ) ERC721("", "") {
        pass_ = pass;
        maxPremintPassId_ = maxPremintPassId;
        unlockTimestamp_ = unlockTimestamp;

    function name() public pure override returns (string memory) {
        return "QQL";

    function symbol() public pure override returns (string memory) {
        return "QQL";

    function setScriptPiece(uint256 id, string memory data) external onlyOwner {
        if (bytes(scriptPieces_[id]).length != 0)
            revert("QQL: script pieces are immutable");

        scriptPieces_[id] = data;

    function scriptPiece(uint256 id) external view returns (string memory) {
        return scriptPieces_[id];

    function transferSeed(
        address from,
        address to,
        bytes32 seed
    ) external {
        if (!isApprovedOrOwnerForSeed(msg.sender, seed))
            revert("QQL: unauthorized for seed");
        if (ownerOfSeed(seed) != from) revert("QQL: wrong owner for seed");
        if (to == address(0)) revert("QQL: can't send seed to zero address");
        emit SeedTransfer(from, to, seed);
        seedOwners_[seed] = to;

    function ownerOfSeed(bytes32 seed) public view returns (address) {
        address explicitOwner = seedOwners_[seed];
        if (explicitOwner == address(0)) {
            return address(bytes20(seed));
        return explicitOwner;

    function approveForAllSeeds(address operator, bool approved) external {
        address artist = msg.sender;
        approvalForAllSeeds_[artist][operator] = approved;
        emit ApprovalForAllSeeds(msg.sender, operator, approved);

    function isApprovedForAllSeeds(address owner, address operator)
        returns (bool)
        return approvalForAllSeeds_[owner][operator];

    function isApprovedOrOwnerForSeed(address operator, bytes32 seed)
        returns (bool)
        address seedOwner = ownerOfSeed(seed);
        if (seedOwner == operator) {
            return true;
        return approvalForAllSeeds_[seedOwner][operator];

    function mint(uint256 mintPassId, bytes32 seed) external returns (uint256) {
        return mintTo(mintPassId, seed, msg.sender);

    /// Consumes the specified mint pass to mint a QQL with the specified seed,
    /// which will be owned by the specified recipient. The royalty stream will
    /// be owned by the original parametric artist (the address embedded in the
    /// seed).
    /// The caller must be authorized by the owner of the mint pass to operate
    /// the mint pass, and the recipient must be authorized by the owner of the
    /// seed to operate the seed.
    /// Returns the ID of the newly minted QQL token.
    function mintTo(
        uint256 mintPassId,
        bytes32 seed,
        address recipient
    ) public returns (uint256) {
        if (!isApprovedOrOwnerForSeed(recipient, seed))
            revert("QQL: unauthorized for seed");
        if (!pass_.isApprovedOrOwner(msg.sender, mintPassId))
            revert("QQL: unauthorized for pass");
        if (seedToTokenId_[seed] != 0) revert("QQL: seed already used");
        if (
            block.timestamp < unlockTimestamp_ && mintPassId > maxPremintPassId_
        ) revert("QQL: mint pass not yet unlocked");

        uint256 tokenId = nextTokenId_++;
        tokenSeed_[tokenId] = seed;
        seedToTokenId_[seed] = tokenId;
        // Royalty recipient is always the original artist, which may be
        // distinct from the minter (`msg.sender`).
        tokenRoyaltyRecipient_[tokenId] = payable(address(bytes20(seed)));
        _safeMint(recipient, tokenId);
        return tokenId;

    function parametricArtist(uint256 tokenId) external view returns (address) {
        bytes32 seed = tokenSeed_[tokenId];
        if (seed == bytes32(0)) revert("QQL: token does not exist");
        return address(bytes20(seed));

    function setProjectRoyaltyRecipient(address payable recipient)
        projectRoyaltyRecipient_ = recipient;
        emit ProjectRoyaltyRecipientChange(recipient);

    function projectRoyaltyRecipient() external view returns (address payable) {
        return projectRoyaltyRecipient_;

    function tokenRoyaltyRecipient(uint256 tokenId)
        returns (address)
        return tokenRoyaltyRecipient_[tokenId];

    function changeTokenRoyaltyRecipient(
        uint256 tokenId,
        address payable newRecipient
    ) external {
        if (tokenRoyaltyRecipient_[tokenId] != msg.sender) {
            revert("QQL: unauthorized");
        if (newRecipient == address(0)) {
            revert("QQL: can't set zero address as token royalty recipient");
        emit TokenRoyaltyRecipientChange(tokenId, newRecipient);
        tokenRoyaltyRecipient_[tokenId] = newRecipient;

    function getRoyalties(uint256 tokenId)
        returns (address payable[] memory recipients, uint256[] memory bps)
        recipients = new address payable[](2);
        bps = new uint256[](2);
        recipients[0] = projectRoyaltyRecipient_;
        recipients[1] = tokenRoyaltyRecipient_[tokenId];
        if (recipients[1] == address(0)) {
            revert("QQL: royalty for nonexistent token");
        bps[0] = PROJECT_ROYALTY_BPS;
        bps[1] = TOKEN_ROYALTY_BPS;

    /// Returns the seed associated with the given QQL token. Returns
    /// `bytes32(0)` if and only if the token does not exist.
    function tokenSeed(uint256 tokenId) external view returns (bytes32) {
        return tokenSeed_[tokenId];

    /// Returns the token ID associated with the given seed. Returns 0 if
    /// and only if no token was ever minted with that seed.
    function seedToTokenId(bytes32 seed) external view returns (uint256) {
        return seedToTokenId_[seed];

    function supportsInterface(bytes4 interfaceId)
        override(ERC721Enumerable, ERC721)
        returns (bool)
        return super.supportsInterface(interfaceId);

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
        override(ERC721, ERC721Enumerable, ERC721OperatorFilter)
        super._beforeTokenTransfer(from, to, tokenId);

    function tokenURI(uint256 tokenId)
        override(ERC721TokenUriDelegate, ERC721)
        returns (string memory)
        return super.tokenURI(tokenId);

    function unlockTimestamp() public view returns (uint256) {
        return unlockTimestamp_;

    function maxPremintPassId() public view returns (uint256) {
        return maxPremintPassId_;

Now I want to update my coding to ensure that both royalties are added. One is 0.69% and the other is user choice between 0 and 10%. So here is my code below:

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

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/interfaces/IERC2981.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@limitbreak/creator-token-contracts/contracts/erc721c/ERC721C.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./SSTORE2.sol";
import "./Base64.sol";
import "./.deps/DefaultOperatorFilterer.sol";

contract SVGEditorNFTV4 is ERC721C, IERC2981, Ownable, DefaultOperatorFilterer {
    using Strings for uint256;
    struct TokenData {
        uint256 royalties;
        address royaltyRecipient;
        address attributesPointer;

    mapping(uint256 => TokenData) private _tokenData;

    uint96 private _platformRoyalty = 69;
    uint256 private _payment = 96000000000000000;
    bool public paused = false;
    uint256 private _currentTokenId = 0;

    constructor() ERC721OpenZeppelin("svg", "SVG") {}

    modifier whenNotPaused() {
        require(!paused, "Contract is paused.");

    // Implement the _requireCallerIsContractOwner function
    function _requireCallerIsContractOwner() internal view override {
        require(owner() == _msgSender(), "Caller is not the contract owner");

    function setPayment(uint256 x) external onlyOwner whenNotPaused {
        _payment = x;

    function returnPayment() external view returns (uint256) {
        return _payment;

    function setPlatformRoyalty(uint96 x) external onlyOwner whenNotPaused {
        _platformRoyalty = x;

    function platformRoyalty() external view returns (uint96) {
        return _platformRoyalty;

    function _setTokenRoyalty(uint256 tokenId, address recipient, uint256 royaltyPercentage) internal whenNotPaused {
        require(royaltyPercentage <= 10000, "SVG: Royalty percentage must not exceed 10000");
        _tokenData[tokenId].royalties = royaltyPercentage;
        _tokenData[tokenId].royaltyRecipient = recipient;

    function division(uint256 decimalPlaces, uint256 numerator, uint256 denominator) pure public returns(string memory result) {
        uint256 factor = 10**decimalPlaces;
        uint256 quotient  = numerator / denominator;
        bool rounding = 2 * ((numerator * factor) % denominator) >= denominator;
        uint256 remainder = (numerator * factor / denominator) % factor;
        if (rounding) {
            remainder += 1;
        result = string(abi.encodePacked(quotient.toString(), '.', numToFixedLengthStr(decimalPlaces, remainder)));

    function numToFixedLengthStr(uint256 decimalPlaces, uint256 num) pure internal returns(string memory result) {
        bytes memory byteString;
        for (uint256 i = 0; i < decimalPlaces; i++) {
            uint256 remainder = num % 10;
            byteString = abi.encodePacked(remainder.toString(), byteString);
            num = num/10;
        result = string(byteString);

    function mint(
        string calldata svg,
        string calldata name,
        uint96 percent
    ) external whenNotPaused {
        uint256 newTokenId = _currentTokenId;
        _safeMint(msg.sender, newTokenId);
        // Convert percentage to string and append "%"
        string memory percentString = division(2,percent,100);
        //string memory percentWithSymbol = string(abi.encodePacked(percentString, "%"));
        _tokenData[newTokenId].attributesPointer = SSTORE2.write(abi.encode(svg, name, percentString, msg.sender));

     * @dev See {IERC721-transferFrom}.
     *      In this example the added modifier ensures that the operator is allowed by the OperatorFilterRegistry.
    function transferFrom(address from, address to, uint256 tokenId) public override onlyAllowedOperator(from) {
        super.transferFrom(from, to, tokenId);

     * @dev See {IERC721-safeTransferFrom}.
     *      In this example the added modifier ensures that the operator is allowed by the OperatorFilterRegistry.
    function safeTransferFrom(address from, address to, uint256 tokenId) public override onlyAllowedOperator(from) {
        super.safeTransferFrom(from, to, tokenId);

     * @dev See {IERC721-safeTransferFrom}.
     *      In this example the added modifier ensures that the operator is allowed by the OperatorFilterRegistry.
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
        super.safeTransferFrom(from, to, tokenId, data);

    function tokenURI(uint256 tokenId) public whenNotPaused view virtual override returns (string memory) {
        (string memory svg, string memory name, string memory percent, address minter) = abi.decode([tokenId].attributesPointer), (string, string, string, address));

        // Convert minter address to string
        string memory minterString = _toString(minter);

        string memory json = Base64.encode(
                '{"name":"', name, '",',
                '"image_data":"', svg, '",',
                '"attributes":[{"trait_type":"Minter Fees (%)", "value": "', percent, '"},',  // Notice that I've added double quotes around the value.
                '{"trait_type":"Minter","value":"', minterString, '"}]',
        return string(abi.encodePacked("data:application/json;base64,", json));

    // Function to convert address to string
    function _toString(address _addr) private pure returns(string memory) {
        bytes32 value = bytes32(uint256(uint160(_addr)));
        bytes memory alphabet = "0123456789abcdef";

        bytes memory str = new bytes(42);
        str[0] = '0';
        str[1] = 'x';
        for (uint256 i = 0; i < 20; i++) {
            str[2+i*2] = alphabet[uint8(value[i + 12] >> 4)];
            str[3+i*2] = alphabet[uint8(value[i + 12] & 0x0f)];
        return string(str);

    function royaltyInfo(uint256 tokenId, uint256 value) external view virtual override returns (address receiver, uint256 royaltyAmount) {
        receiver = _tokenData[tokenId].royaltyRecipient;
        royaltyAmount = value * _tokenData[tokenId].royalties / 10000;

    function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721C, IERC165) returns (bool) {
        return super.supportsInterface(interfaceId) || interfaceId == type(IERC2981).interfaceId;

    function pause() external onlyOwner {
        paused = !paused;
        emit Paused(paused);

    event Paused(bool isPaused);

I have tried loads of things. _setTokenRoyalty twice just overwrites. Cannot make the transfer functions payable to transfer royalties when someone buys an NFT. Is there any way that you can help me mint multiple royalties with this please? Also, can I ensure that the royalties are different for each tokenID. E.g. item 1 is 3.69% and item 2 is 5.69% etc.

I would also like to ask which buy and sell functions operate with OpenSea? Thank you.

1 Like