Repository: pcaversaccio/snekmate (language: python)

Issue:
💥 `ERC-2981` (NFT Royalty Standard) Implementation
@victor-ego proposes to implement [`ERC-2981`](https://eips.ethereum.org/EIPS/eip-2981) (NFT royalty standard) for 🐍 snekmate's [`ERC721`](https://github.com/pcaversaccio/snekmate/blob/main/src/tokens/ERC721.vy) contract.

This issue is opened to gather the discussion around whether such an implementation is of a value-add.

Personally, I have the following view:

- EIP-2981 only defines a way to show/signal royalty information BUT does **not** enforce the payment actually. Thus, you're actually relying on marketplaces to voluntarily pay the royalties. Also, I got the feeling that this standard is not yet widely supported (still an open issue on OpenSea: https://github.com/ProjectOpenSea/opensea-js/issues/482). So am not sure about the overall value-add and acceptance.

Let me know your thoughts.


Discussion / hints:
I believe that the value add is massive. The ways that NFTs can be designed and use cases drastically changes when you can customize royalty receivers, but it needs to be universally adopted to be viable. 

I am working on an ERC-1155 contract where anyone can be an NFT creator and then earn royalties from their creations. With opensea this is not possible without an insurmountable amount of off-chain work to manage creators and sales on opensea. It also requires that creators trust the contract owner to pay them, which completely goes against the purpose of trustless smart contracts. 

Creators should only have a trust relationship with marketplaces to pay royalties, not contract owners.

This has many business applications, my above example could be for any type of project. Where their is a single contract with hundreds or thousands of creators using it. This allows for the development of potentially massive specialized platforms built on-top of NFTs.  The division of creators and contract owners is the next step to make the NFT ecosystem grow. The average creator doesn't have the time, knowledge, or money to get a custom contract designed. They need the tech side to provide smart contract platforms that they can easily use with very little knowledge.
> Creators should only have a trust relationship with marketplaces to pay royalties, not contract owners.

I would even go further - the entire beauty of smart contracts should be trustlessness. Thus nobody should trust anyone by design. If it's not possible to enforce that trustlessness in the code, either the code or the standard interface is the bug and not the feature imo.

My issue is that I don't want to have dead code inside 🐍 snekmate's [ERC721](https://github.com/pcaversaccio/snekmate/blob/main/src/tokens/ERC721.vy) contract. Also, we want max composability and smart contract creators might want other future standards to be implemented.

What I could think of is the following: We create a directory `common` in the token directory (similar to what OpenZeppelin is doing) and put there an `ERC2981` implementation as a reference. In the code itself, we add proper comments on how integration into `ERC721` and `ERC1155` could look like (e.g. that you must include the `ERC165` identifier and also must clear the royalty information for the token if burned). Like that we provide a reference implementation and anyone can integrate it, but the main [ERC721](https://github.com/pcaversaccio/snekmate/blob/main/src/tokens/ERC721.vy) contract remains untouched. Thoughts?

In my opinion ERC2981 was definitely a step in the right direction and it has also found decent adoption by some of the largest marketplaces. At the same time it also falls short in certain critical areas.

As @pcaversaccio mentioned it’s only a standard for royalty signalling and as such it doesn’t actually enforce anything. It’s up to the marketplaces to support royalty payouts or not. On OpenSea the situation is even more complicated as signalled royalties are only enforced if a contract also implements the blacklisting of zero-fee marketplaces that do not respect creator royalties.

Furthermore, the deliberately simple design of ERC-2981 only allows you to define a single recipient. This limitation has prompted players like Manifold to introduce their own solutions that enable creators to split royalties between multiple recipients.

I generally think that the current solution as presented by ERC2981 is just not complete and future-proof enough to warrant an integration into the core ERC721 contract. I think providing a reference implementation for creators that want to deliberately adopt ERC2981 would be a great compromise in this case.
@schuma7 Its pretty easy to add a PaymentSplitter to 2981 to allow multiple recipients. I agree it shouldn't be in the core ERC721 contract though. Developers need the choice to implement it, but marketplaces should abide by it.

@pcaversaccio I wouldn't be concerned about ERC2981 being dead code, there are many marketplaces that do implement it already such as Rarible. Since you can't control which marketplace a user will use it is a good net to catch those that do support it. My plan is to implement it and set the royalties for 2981 at 5% and all other unsupported marketplaces at 10% in an attempt to dissuade selling on marketplace like opensea. On top of that I plan to use the non-2981 compliant royalties to fund a DAO since it would be a royal pain to try to distribute the royalties to the correct creators.

The entire intention of my plan is to make owners and creators dislike marketplaces that don't integrate EIP-2981 and put pressure on them to do so.

> @pcaversaccio I wouldn't be concerned about ERC2981 being dead code, there are many marketplaces that do implement it already such as Rarible.

When I referred to dead code, I meant that being part of my [ERC721](https://github.com/pcaversaccio/snekmate/blob/main/src/tokens/ERC721.vy) contract it's _most probably_ going to be dead code. Vyper doesn't allow for inheritance and will allow for [library modules](https://github.com/vyperlang/vyper/issues/2431) (most probably) starting with Vyper `0.4.0`. That being said, my current `ERC721` contract entails mostly a feature-complete set of extensions that the majority use, and thus people can simply copy+paste the contract for deployment (or you simply `pip install` my contracts directly locally 😃). If `ERC2981` is included in the core contract, my concern is that many folks don't need it and must refactor them themselves. Thus, my suggestion about the reference implementation which is not part of the core contract. This is also in line with my long-term vision (and was my underlying motivation to build 🐍 snekmate) of having a good extension system with feature-complete contracts that can be imported once libraries are live. Like that, we can achieve max composability while retaining ergonomic separation (IMHO) of different contracts.
Consider a scenario where renowned street artist Banksy decides to release an NFT collection on a marketplace that supports royalty payments. However, platforms like OpenSea, which are currently not enforcing royalties (even though they did a few months ago when testing this ERC721 experiment!), will be unable to facilitate the buying/selling of these NFTs. This is due to the integrated royalty enforcement mechanism in the ERC721 contract of Banksy's NFTs, making royalty payments mandatory.

One might speculate if OpenSea would risk missing out on the trading volume of such high-profile collections due to their non-compliance with royalty payments. The answer, in my opinion, highlights the importance of taking a stand to push marketplaces towards respecting artist royalties.

As artists and developers, while we're yet to claim victory in enforcing royalty payments across all marketplaces, one proactive step could be the creation of a `common` directory. This initiative could provide an alternative approach to enforce royalty payments, supporting artists in monetizing their work.

Working alongside @fubuloubu, we're [in the process ](https://github.com/ApeAcademy/ERC721/pull/55)of integrating these royalty functionalities into the contracts. We aim to build a trustless system that doesn't rely solely on the goodwill of marketplaces to apply a suggested 10% royalty, but rather enforces it on a contractual level. This represents a significant step towards a more equitable NFT ecosystem, where artists' rights to residual earnings are universally recognized and respected.

A current summary of progress made so far:

1. **Royalty Amount:** The contract contains a variable `minRoyaltyAmount` that stores a fixed minimum royalty amount to be paid in each transaction.

2. **Floor Price Oracle:** A mechanism has been created to update this `minRoyaltyAmount` based on previous transactions. If the contract receives an amount greater than `minRoyaltyAmount + threshold`, the `minRoyaltyAmount` is increased by a certain proportion. If the amount received is less than `minRoyaltyAmount + threshold`, then `minRoyaltyAmount` is decreased by a certain proportion. This creates a sort of oracle to adjust the floor price based on recent transaction activity.

3. **Balance Tracking:** The contract keeps track of the balance after each transaction using `lastBalance`. This is used in the aforementioned oracle to measure the amount sent in the last transaction.

4. **Royalty Enforcing:** The contract has a mechanism to enforce royalty payments. Before each transaction, it checks if the contract balance has increased by at least `minRoyaltyAmount` since the last transaction. If not, the transaction is reverted.

5. **Withdrawal Function:** There's a function to allow the contract owner to withdraw their funds from the contract.

6. **ERC721 Compliance:** The contract remains compliant with the ERC721 standard, while adding on these additional features.

7. **RoyaltyInfo Function:** This function returns the address of the recipient of the royalty and the calculated royalty amount for a given sale price.

8. **ERC2981 Interface Checking:** The contract includes a function to check if a given contract supports the ERC2981 royalty standard.

These features collectively ensure that the contract enforces the payment of royalties on each transaction and that this royalty amount adapts to the transaction history of the contract, providing a form of floor price oracle.

I´m sure there are many things not taken into account of that could be more of a bug than a feature. Will love to hear your thoughts and collaborate together. 
@victor-ego thanks for sharing this information. I think what you outline is a nice approach; some feedback:
- To increase the acceptance of such an approach, I recommend the drafting of an EIP. It is an unfortunate fact that EIPs nowadays serve as a marketing tool that influences acceptance;
- The price oracle can be easily manipulated if this is desired;
- "royalties on each transaction" -> I don't think this is the way to go; I would rather see a design where NFTs implement a dedicated `buy`/`sell` function that is used by marketplaces to enforce the royalties rather than abusing `transferFrom` and `safeTransferFrom`. We need to distinguish the on-chain semantics of a transfer and that's the fundamental issue with on-chain paid royalties;
- Let's say I have an upgradable contract setup: I could easily overwrite the logic whenever I want. Think about the scenario where you enforce royalty payments as you mentioned; what could happen is that people upgrade the logic for non-enforcement for eg OTC deals and re-implement the previous logic later to be allowed to get listed again;
- Overall there are multiple attack vectors that would allow to abuse the contract logic imho;

So coming back to the initial topic of this issue: I will release 🐍 snekmate `0.0.2` tomorrow and thereafter we can start working on the `ERC2981` implementation. After careful thinking I believe it's better to place the `ERC2981` contract in the `extensions` folder. @victor-ego would you like to collaborate on this contract with me? 
@pcaversaccio , I appreciate your insightful feedback and suggestions on our royalty implementation approach.

1. I completely agree with you regarding drafting an EIP. Although it might have transformed into a marketing tool, it is still the most recognized method to suggest protocol improvements.

2. On the point of price oracle manipulation, good point. The aim is to maintain the integrity of the price feeds while ensuring they remain beneficial for the artists.

3. As for the suggestion to introduce a dedicated buy/sell function instead of relying on `transferFrom` and `safeTransferFrom`, it's an interesting viewpoint. I concur that this could help delineate the semantics of a transfer better. The broader implication of this change, however, would be a shift in the current paradigm. This could potentially require significant updates from the marketplaces, but the merits certainly make it a worthwhile discussion.

4. Regarding the upgradable contract setup, it is indeed a valid concern. It's a delicate balance between maintaining the necessary flexibility for contract owners and ensuring the royalty mechanisms aren't bypassed. 

5. I appreciate your recognition of potential attack vectors, and that's precisely why having conversations like these is crucial. **We should aim to develop a robust solution that not only benefits artists but also ensures security and integrity.**

As for 🐍 snekmate `0.0.2`, I'm excited to see its release and the subsequent start on the ERC2981 implementation. I think positioning the ERC2981 contract in the extensions folder is a good idea.

Lastly, I would be absolutely thrilled to collaborate with you on this contract! Let's work together to create a robust, equitable, and universally accepted royalty standard for the NFT ecosystem. 🫡

> 5\. We should aim to develop a robust solution that not only benefits artists but also ensures security and integrity.

100%

> Lastly, I would be absolutely thrilled to collaborate with you on this contract! Let's work together to create a robust, equitable, and universally accepted royalty standard for the NFT ecosystem. 🫡

Awesome :-D - will reach out on Discord for the next steps. Anyone is invited to continue to share his/her/their thoughts about what can improved wrt on-chain royalties!
FYI, I just released 🐍 snekmate [`0.0.2`](https://github.com/pcaversaccio/snekmate/releases/tag/v0.0.2).

Fix patch:
diff --git a/.gas-snapshot b/.gas-snapshot
index 520939c4..5b4d0d83 100644
--- a/.gas-snapshot
+++ b/.gas-snapshot
@@ -7,9 +7,9 @@ AccessControlTest:testFuzzGrantRoleSuccess(address) (runs: 256, μ: 43947, ~: 43
 AccessControlTest:testFuzzRenounceRoleMultipleTimesSuccess(address) (runs: 256, μ: 45028, ~: 45017)
 AccessControlTest:testFuzzRenounceRoleNonMsgSender(address) (runs: 256, μ: 9678, ~: 9678)
 AccessControlTest:testFuzzRenounceRoleSuccess(address) (runs: 256, μ: 41353, ~: 41342)
-AccessControlTest:testFuzzRevokeRoleMultipleTimesSuccess(address) (runs: 256, μ: 41150, ~: 41138)
+AccessControlTest:testFuzzRevokeRoleMultipleTimesSuccess(address) (runs: 256, μ: 41151, ~: 41138)
 AccessControlTest:testFuzzRevokeRoleNonAdmin(address,address) (runs: 256, μ: 16620, ~: 16620)
-AccessControlTest:testFuzzRevokeRoleSuccess(address) (runs: 256, μ: 39879, ~: 39867)
+AccessControlTest:testFuzzRevokeRoleSuccess(address) (runs: 256, μ: 39880, ~: 39867)
 AccessControlTest:testFuzzSetRoleAdminPreviousAdminCallsGrantRole(address,address) (runs: 256, μ: 76163, ~: 76163)
 AccessControlTest:testFuzzSetRoleAdminPreviousAdminCallsRevokeRole(address,address) (runs: 256, μ: 101836, ~: 101836)
 AccessControlTest:testFuzzSetRoleAdminSuccess(address,address) (runs: 256, μ: 90452, ~: 90452)
@@ -55,62 +55,62 @@ BatchDistributorTest:testDistributeEtherRevertWithInsufficientFunds() (gas: 8271
 BatchDistributorTest:testDistributeEtherRevertWithNoFallbackFunctionForMsgSender() (gas: 158502)
 BatchDistributorTest:testDistributeEtherRevertWithNoFallbackFunctionForReceipt() (gas: 53776)
 BatchDistributorTest:testDistributeEtherSendsBackExcessiveEther() (gas: 187061)
-BatchDistributorTest:testDistributeTokenMultipleAddressesSuccess() (gas: 640354)
-BatchDistributorTest:testDistributeTokenOneAddressSuccess() (gas: 602683)
-BatchDistributorTest:testDistributeTokenRevertWithInsufficientAllowance() (gas: 596641)
-BatchDistributorTest:testDistributeTokenRevertWithInsufficientBalance() (gas: 599203)
-BatchDistributorTest:testFuzzDistributeEtherMultipleAddressesSuccess(((address,uint256)[]),uint256) (runs: 256, μ: 988880, ~: 925797)
-BatchDistributorTest:testFuzzDistributeTokenMultipleAddressesSuccess(((address,uint256)[]),address,uint256) (runs: 256, μ: 1347272, ~: 1324156)
-Create2AddressTest:testComputeAddress() (gas: 573558)
-Create2AddressTest:testComputeAddressSelf() (gas: 581858)
-Create2AddressTest:testFuzzComputeAddress(bytes32,address) (runs: 256, μ: 574086, ~: 574086)
-Create2AddressTest:testFuzzComputeAddressSelf(bytes32) (runs: 256, μ: 581879, ~: 581879)
+BatchDistributorTest:testDistributeTokenMultipleAddressesSuccess() (gas: 651107)
+BatchDistributorTest:testDistributeTokenOneAddressSuccess() (gas: 613436)
+BatchDistributorTest:testDistributeTokenRevertWithInsufficientAllowance() (gas: 609181)
+BatchDistributorTest:testDistributeTokenRevertWithInsufficientBalance() (gas: 609956)
+BatchDistributorTest:testFuzzDistributeEtherMultipleAddressesSuccess(((address,uint256)[]),uint256) (runs: 256, μ: 981720, ~: 914563)
+BatchDistributorTest:testFuzzDistributeTokenMultipleAddressesSuccess(((address,uint256)[]),address,uint256) (runs: 256, μ: 1356463, ~: 1320191)
+Create2AddressTest:testComputeAddress() (gas: 586317)
+Create2AddressTest:testComputeAddressSelf() (gas: 594617)
+Create2AddressTest:testFuzzComputeAddress(bytes32,address) (runs: 256, μ: 586845, ~: 586845)
+Create2AddressTest:testFuzzComputeAddressSelf(bytes32) (runs: 256, μ: 594638, ~: 594638)
 CreateAddressTest:testComputeAddressNonce0x00() (gas: 16188)
-CreateAddressTest:testComputeAddressNonce0x7f() (gas: 557375)
-CreateAddressTest:testComputeAddressNonceUint16() (gas: 557472)
-CreateAddressTest:testComputeAddressNonceUint24() (gas: 557522)
-CreateAddressTest:testComputeAddressNonceUint32() (gas: 557549)
-CreateAddressTest:testComputeAddressNonceUint40() (gas: 557534)
-CreateAddressTest:testComputeAddressNonceUint48() (gas: 557562)
-CreateAddressTest:testComputeAddressNonceUint56() (gas: 557612)
-CreateAddressTest:testComputeAddressNonceUint64() (gas: 557771)
-CreateAddressTest:testComputeAddressNonceUint8() (gas: 557467)
+CreateAddressTest:testComputeAddressNonce0x7f() (gas: 569805)
+CreateAddressTest:testComputeAddressNonceUint16() (gas: 569902)
+CreateAddressTest:testComputeAddressNonceUint24() (gas: 569952)
+CreateAddressTest:testComputeAddressNonceUint32() (gas: 569979)
+CreateAddressTest:testComputeAddressNonceUint40() (gas: 569964)
+CreateAddressTest:testComputeAddressNonceUint48() (gas: 569992)
+CreateAddressTest:testComputeAddressNonceUint56() (gas: 570042)
+CreateAddressTest:testComputeAddressNonceUint64() (gas: 570201)
+CreateAddressTest:testComputeAddressNonceUint8() (gas: 569897)
 CreateAddressTest:testComputeAddressRevertTooHighNonce() (gas: 10615)
-CreateAddressTest:testComputeAddressSelfNonce0x7f() (gas: 561233)
-CreateAddressTest:testComputeAddressSelfNonceUint16() (gas: 561432)
-CreateAddressTest:testComputeAddressSelfNonceUint24() (gas: 561461)
-CreateAddressTest:testComputeAddressSelfNonceUint32() (gas: 561557)
-CreateAddressTest:testComputeAddressSelfNonceUint40() (gas: 561586)
-CreateAddressTest:testComputeAddressSelfNonceUint48() (gas: 561614)
-CreateAddressTest:testComputeAddressSelfNonceUint56() (gas: 561665)
-CreateAddressTest:testComputeAddressSelfNonceUint64() (gas: 561760)
-CreateAddressTest:testComputeAddressSelfNonceUint8() (gas: 561410)
+CreateAddressTest:testComputeAddressSelfNonce0x7f() (gas: 573663)
+CreateAddressTest:testComputeAddressSelfNonceUint16() (gas: 573862)
+CreateAddressTest:testComputeAddressSelfNonceUint24() (gas: 573891)
+CreateAddressTest:testComputeAddressSelfNonceUint32() (gas: 573987)
+CreateAddressTest:testComputeAddressSelfNonceUint40() (gas: 574016)
+CreateAddressTest:testComputeAddressSelfNonceUint48() (gas: 574044)
+CreateAddressTest:testComputeAddressSelfNonceUint56() (gas: 574095)
+CreateAddressTest:testComputeAddressSelfNonceUint64() (gas: 574190)
+CreateAddressTest:testComputeAddressSelfNonceUint8() (gas: 573840)
 CreateAddressTest:testComputeAddressSelfRevertTooHighNonce() (gas: 8799)
-CreateAddressTest:testFuzzComputeAddressNonce0x7f(uint64,address) (runs: 256, μ: 560838, ~: 561013)
-CreateAddressTest:testFuzzComputeAddressNonceUint16(uint64,address) (runs: 256, μ: 560382, ~: 560627)
-CreateAddressTest:testFuzzComputeAddressNonceUint24(uint64,address) (runs: 256, μ: 560469, ~: 560582)
-CreateAddressTest:testFuzzComputeAddressNonceUint32(uint64,address) (runs: 256, μ: 560476, ~: 560611)
-CreateAddressTest:testFuzzComputeAddressNonceUint40(uint64,address) (runs: 256, μ: 560583, ~: 560681)
-CreateAddressTest:testFuzzComputeAddressNonceUint48(uint64,address) (runs: 256, μ: 560589, ~: 560687)
-CreateAddressTest:testFuzzComputeAddressNonceUint56(uint64,address) (runs: 256, μ: 560576, ~: 560694)
-CreateAddressTest:testFuzzComputeAddressNonceUint64(uint64,address) (runs: 256, μ: 560590, ~: 560798)
-CreateAddressTest:testFuzzComputeAddressNonceUint8(uint64,address) (runs: 256, μ: 560352, ~: 560456)
-CreateAddressTest:testFuzzComputeAddressRevertTooHighNonce(uint256,address) (runs: 256, μ: 13330, ~: 13303)
-CreateAddressTest:testFuzzComputeAddressSelfNonce0x7f(uint64) (runs: 256, μ: 566671, ~: 566807)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint16(uint64) (runs: 256, μ: 566007, ~: 566140)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint24(uint64) (runs: 256, μ: 566304, ~: 566407)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint32(uint64) (runs: 256, μ: 566319, ~: 566415)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint40(uint64) (runs: 256, μ: 566400, ~: 566486)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint48(uint64) (runs: 256, μ: 566364, ~: 566473)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint56(uint64) (runs: 256, μ: 566450, ~: 566543)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint64(uint64) (runs: 256, μ: 566476, ~: 566673)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint8(uint64) (runs: 256, μ: 566014, ~: 566110)
+CreateAddressTest:testFuzzComputeAddressNonce0x7f(uint64,address) (runs: 256, μ: 573286, ~: 573443)
+CreateAddressTest:testFuzzComputeAddressNonceUint16(uint64,address) (runs: 256, μ: 572785, ~: 572908)
+CreateAddressTest:testFuzzComputeAddressNonceUint24(uint64,address) (runs: 256, μ: 572898, ~: 573012)
+CreateAddressTest:testFuzzComputeAddressNonceUint32(uint64,address) (runs: 256, μ: 572904, ~: 573041)
+CreateAddressTest:testFuzzComputeAddressNonceUint40(uint64,address) (runs: 256, μ: 573011, ~: 573111)
+CreateAddressTest:testFuzzComputeAddressNonceUint48(uint64,address) (runs: 256, μ: 573014, ~: 573117)
+CreateAddressTest:testFuzzComputeAddressNonceUint56(uint64,address) (runs: 256, μ: 573017, ~: 573124)
+CreateAddressTest:testFuzzComputeAddressNonceUint64(uint64,address) (runs: 256, μ: 573013, ~: 573228)
+CreateAddressTest:testFuzzComputeAddressNonceUint8(uint64,address) (runs: 256, μ: 572772, ~: 572886)
+CreateAddressTest:testFuzzComputeAddressRevertTooHighNonce(uint256,address) (runs: 256, μ: 13326, ~: 13303)
+CreateAddressTest:testFuzzComputeAddressSelfNonce0x7f(uint64) (runs: 256, μ: 579120, ~: 579237)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint16(uint64) (runs: 256, μ: 578426, ~: 578570)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint24(uint64) (runs: 256, μ: 578733, ~: 578837)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint32(uint64) (runs: 256, μ: 578734, ~: 578845)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint40(uint64) (runs: 256, μ: 578832, ~: 578916)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint48(uint64) (runs: 256, μ: 578790, ~: 578903)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint56(uint64) (runs: 256, μ: 578882, ~: 578973)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint64(uint64) (runs: 256, μ: 578909, ~: 579103)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint8(uint64) (runs: 256, μ: 578450, ~: 578540)
 CreateAddressTest:testFuzzComputeAddressSelfRevertTooHighNonce(uint256) (runs: 256, μ: 13155, ~: 13142)
 ECDSATest:testEthSignedMessageHash() (gas: 5852)
 ECDSATest:testFuzzEthSignedMessageHash(string) (runs: 256, μ: 6442, ~: 6444)
 ECDSATest:testFuzzRecoverWithInvalidSignature(bytes,string) (runs: 256, μ: 15157, ~: 15158)
 ECDSATest:testFuzzRecoverWithTooLongSignature(bytes,string) (runs: 256, μ: 13118, ~: 13116)
-ECDSATest:testFuzzRecoverWithValidSignature(string,string) (runs: 256, μ: 21734, ~: 21743)
+ECDSATest:testFuzzRecoverWithValidSignature(string,string) (runs: 256, μ: 21730, ~: 21743)
 ECDSATest:testFuzzRecoverWithWrongMessage(string,string,bytes32) (runs: 256, μ: 21725, ~: 21734)
 ECDSATest:testFuzzToDataWithIntendedValidatorHash(address,bytes) (runs: 256, μ: 7368, ~: 7354)
 ECDSATest:testFuzzToDataWithIntendedValidatorHashSelf(bytes) (runs: 256, μ: 9368, ~: 9352)
@@ -131,8 +131,8 @@ ECDSATest:testToTypedDataHash() (gas: 5966)
 EIP712DomainSeparatorTest:testCachedDomainSeparatorV4() (gas: 7635)
 EIP712DomainSeparatorTest:testDomainSeparatorV4() (gas: 11278)
 EIP712DomainSeparatorTest:testEIP712Domain() (gas: 13644)
-EIP712DomainSeparatorTest:testFuzzDomainSeparatorV4(uint8) (runs: 256, μ: 11430, ~: 11453)
-EIP712DomainSeparatorTest:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 19179, ~: 19140)
+EIP712DomainSeparatorTest:testFuzzDomainSeparatorV4(uint8) (runs: 256, μ: 11429, ~: 11453)
+EIP712DomainSeparatorTest:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 19180, ~: 19140)
 EIP712DomainSeparatorTest:testFuzzHashTypedDataV4(address,address,uint256,uint256,uint64) (runs: 256, μ: 7701, ~: 7701)
 EIP712DomainSeparatorTest:testHashTypedDataV4() (gas: 13020)
 ERC1155Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3479)
@@ -162,35 +162,35 @@ ERC1155Test:testExistsAfterBatchMint() (gas: 144145)
 ERC1155Test:testExistsAfterSingleBurn() (gas: 130299)
 ERC1155Test:testExistsAfterSingleMint() (gas: 114273)
 ERC1155Test:testExistsBeforeMint() (gas: 7856)
-ERC1155Test:testFuzzBurnBatchSuccess(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 133125, ~: 133138)
+ERC1155Test:testFuzzBurnBatchSuccess(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 133133, ~: 133136)
 ERC1155Test:testFuzzBurnSuccess(address,address,uint256) (runs: 256, μ: 132445, ~: 132445)
 ERC1155Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 14320, ~: 14320)
 ERC1155Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 49074, ~: 49059)
-ERC1155Test:testFuzzSafeBatchTransferFromByApprovedOperator(address,address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 226917, ~: 226898)
-ERC1155Test:testFuzzSafeBatchTransferFromEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 191568, ~: 191533)
-ERC1155Test:testFuzzSafeBatchTransferFromNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 565281, ~: 565291)
-ERC1155Test:testFuzzSafeBatchTransferFromWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 565730, ~: 565339)
-ERC1155Test:testFuzzSafeMintBatchEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 152997, ~: 152999)
+ERC1155Test:testFuzzSafeBatchTransferFromByApprovedOperator(address,address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 226936, ~: 226898)
+ERC1155Test:testFuzzSafeBatchTransferFromEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 191549, ~: 191533)
+ERC1155Test:testFuzzSafeBatchTransferFromNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 565291, ~: 565291)
+ERC1155Test:testFuzzSafeBatchTransferFromWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 565736, ~: 565339)
+ERC1155Test:testFuzzSafeMintBatchEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 153017, ~: 152999)
 ERC1155Test:testFuzzSafeMintBatchNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 520561, ~: 520561)
 ERC1155Test:testFuzzSafeMintBatchNonMinter(address) (runs: 256, μ: 41288, ~: 41288)
-ERC1155Test:testFuzzSafeMintBatchWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 520991, ~: 520764)
-ERC1155Test:testFuzzSafeMintEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 146294, ~: 146278)
+ERC1155Test:testFuzzSafeMintBatchWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 520987, ~: 520764)
+ERC1155Test:testFuzzSafeMintEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 146314, ~: 146278)
 ERC1155Test:testFuzzSafeMintNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 514858, ~: 514858)
 ERC1155Test:testFuzzSafeMintNonMinter(address) (runs: 256, μ: 33742, ~: 33742)
-ERC1155Test:testFuzzSafeMintWithData(address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 516228, ~: 515761)
-ERC1155Test:testFuzzSafeTransferFromByApprovedOperator(address,address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 197032, ~: 196994)
-ERC1155Test:testFuzzSafeTransferFromEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 161769, ~: 161716)
-ERC1155Test:testFuzzSafeTransferFromNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 596669, ~: 596679)
+ERC1155Test:testFuzzSafeMintWithData(address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 516230, ~: 515761)
+ERC1155Test:testFuzzSafeTransferFromByApprovedOperator(address,address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 197052, ~: 196994)
+ERC1155Test:testFuzzSafeTransferFromEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 161750, ~: 161716)
+ERC1155Test:testFuzzSafeTransferFromNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 596679, ~: 596679)
 ERC1155Test:testFuzzSafeTransferFromWithData(address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 598815, ~: 597987)
 ERC1155Test:testFuzzSetApprovalForAllRevoke(address,address) (runs: 256, μ: 31296, ~: 31270)
 ERC1155Test:testFuzzSetApprovalForAllSuccess(address,address) (runs: 256, μ: 44510, ~: 44510)
-ERC1155Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16201, ~: 16219)
-ERC1155Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34068, ~: 34064)
+ERC1155Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16204, ~: 16219)
+ERC1155Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34070, ~: 34064)
 ERC1155Test:testFuzzSetUriNonMinter(address) (runs: 256, μ: 14677, ~: 14677)
-ERC1155Test:testFuzzTotalSupplyAfterBatchBurn(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 126935, ~: 126948)
-ERC1155Test:testFuzzTotalSupplyAfterBatchMint(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 141338, ~: 141287)
-ERC1155Test:testFuzzTotalSupplyAfterSingleBurn(address,uint256,bytes) (runs: 256, μ: 130065, ~: 130039)
-ERC1155Test:testFuzzTotalSupplyAfterSingleMint(uint256,uint256,bytes) (runs: 256, μ: 86318, ~: 114494)
+ERC1155Test:testFuzzTotalSupplyAfterBatchBurn(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 126943, ~: 126945)
+ERC1155Test:testFuzzTotalSupplyAfterBatchMint(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 141344, ~: 141287)
+ERC1155Test:testFuzzTotalSupplyAfterSingleBurn(address,uint256,bytes) (runs: 256, μ: 130064, ~: 130039)
+ERC1155Test:testFuzzTotalSupplyAfterSingleMint(uint256,uint256,bytes) (runs: 256, μ: 85619, ~: 114494)
 ERC1155Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14438, ~: 14438)
 ERC1155Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 74694, ~: 74671)
 ERC1155Test:testHasOwner() (gas: 9822)
@@ -296,29 +296,29 @@ ERC20Test:testDecreaseAllowanceToZeroAddress() (gas: 19065)
 ERC20Test:testDomainSeparator() (gas: 11945)
 ERC20Test:testEIP712Domain() (gas: 14645)
 ERC20Test:testFuzzApproveSuccess(address,uint256) (runs: 256, μ: 39782, ~: 40638)
-ERC20Test:testFuzzBurnFromInsufficientAllowance(address,uint256,uint8) (runs: 256, μ: 179320, ~: 180013)
-ERC20Test:testFuzzBurnFromSuccess(address,uint256) (runs: 256, μ: 288570, ~: 289561)
+ERC20Test:testFuzzBurnFromInsufficientAllowance(address,uint256,uint8) (runs: 256, μ: 179128, ~: 179674)
+ERC20Test:testFuzzBurnFromSuccess(address,uint256) (runs: 256, μ: 288626, ~: 289561)
 ERC20Test:testFuzzBurnInvalidAmount(address,uint256) (runs: 256, μ: 16803, ~: 16803)
-ERC20Test:testFuzzBurnSuccessCase(uint256) (runs: 256, μ: 264161, ~: 263942)
+ERC20Test:testFuzzBurnSuccessCase(uint256) (runs: 256, μ: 264160, ~: 263942)
 ERC20Test:testFuzzDecreaseAllowanceInvalidAmount(address,uint256,uint256) (runs: 256, μ: 41241, ~: 43029)
 ERC20Test:testFuzzDecreaseAllowanceSuccess(address,uint256,uint256) (runs: 256, μ: 47365, ~: 47882)
 ERC20Test:testFuzzDomainSeparator(uint8) (runs: 256, μ: 12003, ~: 12026)
 ERC20Test:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 20356, ~: 20317)
-ERC20Test:testFuzzIncreaseAllowanceSuccess(address,uint256) (runs: 256, μ: 43872, ~: 44822)
-ERC20Test:testFuzzMintNonMinter(string,uint256) (runs: 256, μ: 13580, ~: 13601)
-ERC20Test:testFuzzMintSuccess(string,uint256) (runs: 256, μ: 55003, ~: 56158)
-ERC20Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 46330, ~: 46364)
-ERC20Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 71563, ~: 71594)
+ERC20Test:testFuzzIncreaseAllowanceSuccess(address,uint256) (runs: 256, μ: 43961, ~: 44822)
+ERC20Test:testFuzzMintNonMinter(string,uint256) (runs: 256, μ: 13583, ~: 13601)
+ERC20Test:testFuzzMintSuccess(string,uint256) (runs: 256, μ: 55173, ~: 56158)
+ERC20Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 46331, ~: 46364)
+ERC20Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 71567, ~: 71594)
 ERC20Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 14391, ~: 14391)
-ERC20Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 49184, ~: 49225)
-ERC20Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16224, ~: 16246)
-ERC20Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34234, ~: 34230)
-ERC20Test:testFuzzTransferFromInsufficientAllowance(address,address,uint256,uint8) (runs: 256, μ: 177523, ~: 178096)
-ERC20Test:testFuzzTransferFromSuccess(address,address,uint256) (runs: 256, μ: 205501, ~: 206974)
+ERC20Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 49239, ~: 49225)
+ERC20Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16227, ~: 16246)
+ERC20Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34235, ~: 34228)
+ERC20Test:testFuzzTransferFromInsufficientAllowance(address,address,uint256,uint8) (runs: 256, μ: 177632, ~: 178094)
+ERC20Test:testFuzzTransferFromSuccess(address,address,uint256) (runs: 256, μ: 205494, ~: 206974)
 ERC20Test:testFuzzTransferInvalidAmount(address,address,uint256) (runs: 256, μ: 17061, ~: 17061)
 ERC20Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14512, ~: 14512)
-ERC20Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 75283, ~: 75258)
-ERC20Test:testFuzzTransferSuccess(address,uint256) (runs: 256, μ: 177076, ~: 177764)
+ERC20Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 75282, ~: 75258)
+ERC20Test:testFuzzTransferSuccess(address,uint256) (runs: 256, μ: 177077, ~: 177762)
 ERC20Test:testHasOwner() (gas: 9825)
 ERC20Test:testIncreaseAllowanceExceedingBalanceCase1() (gas: 40663)
 ERC20Test:testIncreaseAllowanceExceedingBalanceCase2() (gas: 47486)
@@ -359,66 +359,109 @@ ERC20Test:testTransferOwnershipToZeroAddress() (gas: 16052)
 ERC20Test:testTransferSuccess() (gas: 42469)
 ERC20Test:testTransferToZeroAddress() (gas: 16677)
 ERC20Test:testTransferZeroTokens() (gas: 24664)
-ERC4626VaultInvariants:invariantTotalAssets() (runs: 256, calls: 3840, reverts: 2978)
-ERC4626VaultInvariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 2978)
+ERC2981Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3840)
+ERC2981Test:testDeleteDefaultRoyaltyNonOwner() (gas: 10604)
+ERC2981Test:testFuzzDeleteDefaultRoyaltyNonOwner(address) (runs: 256, μ: 13316, ~: 13316)
+ERC2981Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 14064, ~: 14064)
+ERC2981Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 24807, ~: 24807)
+ERC2981Test:testFuzzResetTokenRoyalty(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 93027, ~: 95761)
+ERC2981Test:testFuzzResetTokenRoyaltyNonOwner(address) (runs: 256, μ: 13302, ~: 13302)
+ERC2981Test:testFuzzRoyaltyInfoDefaultRoyalty(address,uint256,uint256,uint96,uint256) (runs: 256, μ: 78341, ~: 80271)
+ERC2981Test:testFuzzRoyaltyInfoDeleteDefaultRoyalty(address,uint256,uint256,uint96,uint256) (runs: 256, μ: 62302, ~: 63538)
+ERC2981Test:testFuzzRoyaltyInfoSetTokenRoyalty(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 117777, ~: 122195)
+ERC2981Test:testFuzzRoyaltyInfoSetTokenRoyaltyUpdate(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 160197, ~: 166791)
+ERC2981Test:testFuzzRoyaltyInfoUpdateDefaultRoyalty(address,address,uint256,uint256,uint96,uint256) (runs: 256, μ: 84035, ~: 85970)
+ERC2981Test:testFuzzSetDefaultRoyaltyNonOwner(address) (runs: 256, μ: 15534, ~: 15534)
+ERC2981Test:testFuzzSetDefaultRoyaltyTooHighFeeNumerator(uint96) (runs: 256, μ: 21874, ~: 21665)
+ERC2981Test:testFuzzSetTokenRoyaltyInvalidReceiver(address) (runs: 256, μ: 18725, ~: 18725)
+ERC2981Test:testFuzzSetTokenRoyaltyNonOwner(address) (runs: 256, μ: 15598, ~: 15598)
+ERC2981Test:testFuzzSetTokenRoyaltyTooHighFeeNumerator(uint96) (runs: 256, μ: 21916, ~: 21707)
+ERC2981Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14226, ~: 14226)
+ERC2981Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 29773, ~: 29773)
+ERC2981Test:testHasOwner() (gas: 9663)
+ERC2981Test:testInitialSetup() (gas: 459383)
+ERC2981Test:testRenounceOwnershipNonOwner() (gas: 11007)
+ERC2981Test:testRenounceOwnershipSuccess() (gas: 17804)
+ERC2981Test:testResetTokenRoyalty() (gas: 91421)
+ERC2981Test:testResetTokenRoyaltyNonOwner() (gas: 10720)
+ERC2981Test:testRoyaltyInfoDefaultRoyalty() (gas: 72983)
+ERC2981Test:testRoyaltyInfoDeleteDefaultRoyalty() (gas: 59691)
+ERC2981Test:testRoyaltyInfoRoyaltyAmountOverflow() (gas: 66923)
+ERC2981Test:testRoyaltyInfoSetTokenRoyalty() (gas: 116338)
+ERC2981Test:testRoyaltyInfoSetTokenRoyaltyUpdate() (gas: 160955)
+ERC2981Test:testRoyaltyInfoUpdateDefaultRoyalty() (gas: 79946)
+ERC2981Test:testSetDefaultRoyaltyInvalidReceiver() (gas: 18025)
+ERC2981Test:testSetDefaultRoyaltyNonOwner() (gas: 12888)
+ERC2981Test:testSetDefaultRoyaltyTooHighFeeNumerator() (gas: 17520)
+ERC2981Test:testSetTokenRoyaltyInvalidReceiver() (gas: 18035)
+ERC2981Test:testSetTokenRoyaltyNonOwner() (gas: 12929)
+ERC2981Test:testSetTokenRoyaltyTooHighFeeNumerator() (gas: 17629)
+ERC2981Test:testSupportsInterfaceGasCost() (gas: 5635)
+ERC2981Test:testSupportsInterfaceInvalidInterfaceId() (gas: 5511)
+ERC2981Test:testSupportsInterfaceSuccess() (gas: 6263)
+ERC2981Test:testTransferOwnershipNonOwner() (gas: 12772)
+ERC2981Test:testTransferOwnershipSuccess() (gas: 22189)
+ERC2981Test:testTransferOwnershipToZeroAddress() (gas: 15680)
+ERC4626VaultInvariants:invariantTotalAssets() (runs: 256, calls: 3840, reverts: 2983)
+ERC4626VaultInvariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 2983)
 ERC4626VaultTest:testCachedDomainSeparator() (gas: 7981)
-ERC4626VaultTest:testDepositInsufficientAllowance() (gas: 82835)
+ERC4626VaultTest:testDepositInsufficientAllowance() (gas: 82945)
 ERC4626VaultTest:testDepositWithNoApproval() (gas: 25247)
-ERC4626VaultTest:testDepositZero() (gas: 40402)
+ERC4626VaultTest:testDepositZero() (gas: 38615)
 ERC4626VaultTest:testDomainSeparator() (gas: 11854)
 ERC4626VaultTest:testEIP712Domain() (gas: 14391)
-ERC4626VaultTest:testEmptyVaultDeposit() (gas: 565800)
-ERC4626VaultTest:testEmptyVaultMint() (gas: 567225)
+ERC4626VaultTest:testEmptyVaultDeposit() (gas: 566240)
+ERC4626VaultTest:testEmptyVaultMint() (gas: 567665)
 ERC4626VaultTest:testEmptyVaultRedeem() (gas: 203519)
 ERC4626VaultTest:testEmptyVaultwithdraw() (gas: 215354)
-ERC4626VaultTest:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 566830, ~: 568420)
-ERC4626VaultTest:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 568847, ~: 570226)
+ERC4626VaultTest:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 560712, ~: 562152)
+ERC4626VaultTest:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 562623, ~: 563958)
 ERC4626VaultTest:testFuzzDomainSeparator(uint8) (runs: 256, μ: 11870, ~: 11892)
-ERC4626VaultTest:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 20029, ~: 19999)
-ERC4626VaultTest:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 46006, ~: 46032)
-ERC4626VaultTest:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 71214, ~: 71238)
+ERC4626VaultTest:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 20030, ~: 19999)
+ERC4626VaultTest:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 45990, ~: 45954)
+ERC4626VaultTest:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 71204, ~: 71238)
 ERC4626VaultTest:testInitialSetup() (gas: 5843513)
 ERC4626VaultTest:testMintWithNoApproval() (gas: 25329)
-ERC4626VaultTest:testMintZero() (gas: 40561)
-ERC4626VaultTest:testMultipleMintDepositRedeemWithdraw() (gas: 379362)
+ERC4626VaultTest:testMintZero() (gas: 38774)
+ERC4626VaultTest:testMultipleMintDepositRedeemWithdraw() (gas: 373335)
 ERC4626VaultTest:testPermitBadChainId() (gas: 44249)
 ERC4626VaultTest:testPermitBadNonce() (gas: 41462)
 ERC4626VaultTest:testPermitExpiredDeadline() (gas: 18044)
 ERC4626VaultTest:testPermitOtherSignature() (gas: 42555)
 ERC4626VaultTest:testPermitReplaySignature() (gas: 74658)
 ERC4626VaultTest:testPermitSuccess() (gas: 70165)
-ERC4626VaultTest:testRedeemInsufficientShares() (gas: 133959)
+ERC4626VaultTest:testRedeemInsufficientShares() (gas: 132282)
 ERC4626VaultTest:testRedeemWithNoShares() (gas: 14016)
-ERC4626VaultTest:testSingleDepositWithdraw() (gas: 178763)
-ERC4626VaultTest:testSingleMintRedeem() (gas: 177680)
-ERC4626VaultTest:testVaultInteractionsForSomeoneElse() (gas: 225963)
-ERC4626VaultTest:testWithdrawInsufficientAllowance() (gas: 124763)
-ERC4626VaultTest:testWithdrawInsufficientAssets() (gas: 120057)
+ERC4626VaultTest:testSingleDepositWithdraw() (gas: 177421)
+ERC4626VaultTest:testSingleMintRedeem() (gas: 176339)
+ERC4626VaultTest:testVaultInteractionsForSomeoneElse() (gas: 223280)
+ERC4626VaultTest:testWithdrawInsufficientAllowance() (gas: 123421)
+ERC4626VaultTest:testWithdrawInsufficientAssets() (gas: 118716)
 ERC4626VaultTest:testWithdrawWithNoAssets() (gas: 21937)
-ERC4626VaultTest:test_RT_deposit_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 480909, ~: 484371)
-ERC4626VaultTest:test_RT_deposit_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 482411, ~: 486228)
-ERC4626VaultTest:test_RT_mint_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 480620, ~: 484703)
-ERC4626VaultTest:test_RT_mint_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 482442, ~: 486330)
-ERC4626VaultTest:test_RT_redeem_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 481202, ~: 484851)
-ERC4626VaultTest:test_RT_redeem_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 481629, ~: 485057)
-ERC4626VaultTest:test_RT_withdraw_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 482872, ~: 486389)
-ERC4626VaultTest:test_RT_withdraw_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 483866, ~: 486605)
-ERC4626VaultTest:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 429216, ~: 434937)
-ERC4626VaultTest:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 436730, ~: 440657)
-ERC4626VaultTest:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 435882, ~: 440347)
-ERC4626VaultTest:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 478601, ~: 481856)
-ERC4626VaultTest:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 429542, ~: 435261)
-ERC4626VaultTest:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 429605, ~: 435324)
-ERC4626VaultTest:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 429830, ~: 435549)
-ERC4626VaultTest:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 431344, ~: 437049)
-ERC4626VaultTest:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 478294, ~: 482140)
-ERC4626VaultTest:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 473063, ~: 476949)
-ERC4626VaultTest:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 472900, ~: 477289)
-ERC4626VaultTest:test_previewRedeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 475875, ~: 479295)
-ERC4626VaultTest:test_previewWithdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 477330, ~: 480963)
-ERC4626VaultTest:test_redeem((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 480287, ~: 483549)
-ERC4626VaultTest:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 430070, ~: 435788)
-ERC4626VaultTest:test_withdraw((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 482381, ~: 485145)
+ERC4626VaultTest:test_RT_deposit_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 475339, ~: 479793)
+ERC4626VaultTest:test_RT_deposit_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 477813, ~: 481376)
+ERC4626VaultTest:test_RT_mint_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 475841, ~: 479890)
+ERC4626VaultTest:test_RT_mint_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 478247, ~: 481501)
+ERC4626VaultTest:test_RT_redeem_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 475647, ~: 479983)
+ERC4626VaultTest:test_RT_redeem_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 476614, ~: 480264)
+ERC4626VaultTest:test_RT_withdraw_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 477929, ~: 481485)
+ERC4626VaultTest:test_RT_withdraw_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 478871, ~: 481931)
+ERC4626VaultTest:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 423836, ~: 429938)
+ERC4626VaultTest:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 432007, ~: 435671)
+ERC4626VaultTest:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 431159, ~: 435521)
+ERC4626VaultTest:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 471454, ~: 475616)
+ERC4626VaultTest:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 424161, ~: 430262)
+ERC4626VaultTest:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 424224, ~: 430325)
+ERC4626VaultTest:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 424449, ~: 430550)
+ERC4626VaultTest:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 425956, ~: 432083)
+ERC4626VaultTest:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 472174, ~: 475821)
+ERC4626VaultTest:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 468670, ~: 472171)
+ERC4626VaultTest:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 468166, ~: 472433)
+ERC4626VaultTest:test_previewRedeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 470672, ~: 474248)
+ERC4626VaultTest:test_previewWithdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 472211, ~: 475846)
+ERC4626VaultTest:test_redeem((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 474784, ~: 478614)
+ERC4626VaultTest:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 424689, ~: 430789)
+ERC4626VaultTest:test_withdraw((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 477076, ~: 480128)
 ERC721Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3449)
 ERC721Invariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 3449)
 ERC721Test:testApproveClearingApprovalWithNoPriorApproval() (gas: 178025)
@@ -446,25 +489,25 @@ ERC721Test:testFuzzApproveFromNonOwner(address) (runs: 256, μ: 173322, ~: 17332
 ERC721Test:testFuzzApproveFromOperatorAddress(address,address,address) (runs: 256, μ: 223971, ~: 223971)
 ERC721Test:testFuzzApproveWithNoPriorApproval(address,address) (runs: 256, μ: 197520, ~: 197530)
 ERC721Test:testFuzzApproveWithPriorApproval(address,address) (runs: 256, μ: 204880, ~: 204890)
-ERC721Test:testFuzzBurnSuccess(address) (runs: 256, μ: 306760, ~: 306747)
-ERC721Test:testFuzzDomainSeparator(uint8) (runs: 256, μ: 12096, ~: 12118)
+ERC721Test:testFuzzBurnSuccess(address) (runs: 256, μ: 306751, ~: 306747)
+ERC721Test:testFuzzDomainSeparator(uint8) (runs: 256, μ: 12093, ~: 12118)
 ERC721Test:testFuzzEIP712Domain(bytes1,uint8,bytes32,uint256[]) (runs: 256, μ: 20561, ~: 20524)
 ERC721Test:testFuzzGetApprovedApprovedTokenId(address,address) (runs: 256, μ: 195062, ~: 195072)
-ERC721Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 203400, ~: 203429)
-ERC721Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 229345, ~: 229375)
+ERC721Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 203395, ~: 203429)
+ERC721Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 229349, ~: 229375)
 ERC721Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 14527, ~: 14527)
-ERC721Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 49370, ~: 49356)
+ERC721Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 49369, ~: 49356)
 ERC721Test:testFuzzSafeMintNonMinter(address) (runs: 256, μ: 16303, ~: 16303)
-ERC721Test:testFuzzSafeMintSuccess(address[]) (runs: 256, μ: 21568751, ~: 21412946)
-ERC721Test:testFuzzSafeTransferFromWithData(address,address,address,bytes) (runs: 256, μ: 1418355, ~: 1419344)
+ERC721Test:testFuzzSafeMintSuccess(address[]) (runs: 256, μ: 21625897, ~: 22104148)
+ERC721Test:testFuzzSafeTransferFromWithData(address,address,address,bytes) (runs: 256, μ: 1418301, ~: 1419335)
 ERC721Test:testFuzzSetApprovalForAllSuccess(address,address) (runs: 256, μ: 190169, ~: 190169)
-ERC721Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16313, ~: 16338)
-ERC721Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34417, ~: 34412)
-ERC721Test:testFuzzTokenByIndex(address,string[]) (runs: 256, μ: 24584621, ~: 25408685)
-ERC721Test:testFuzzTotalSupply(address,string[]) (runs: 256, μ: 24396479, ~: 25216053)
-ERC721Test:testFuzzTransferFrom(address,address,address) (runs: 256, μ: 561244, ~: 561226)
+ERC721Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16319, ~: 16338)
+ERC721Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34417, ~: 34414)
+ERC721Test:testFuzzTokenByIndex(address,string[]) (runs: 256, μ: 24596255, ~: 25418340)
+ERC721Test:testFuzzTotalSupply(address,string[]) (runs: 256, μ: 24407996, ~: 25224270)
+ERC721Test:testFuzzTransferFrom(address,address,address) (runs: 256, μ: 561241, ~: 561226)
 ERC721Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14626, ~: 14626)
-ERC721Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 75541, ~: 75517)
+ERC721Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 75541, ~: 75516)
 ERC721Test:testGetApprovedApprovedTokenId() (gas: 195041)
 ERC721Test:testGetApprovedInvalidTokenId() (gas: 11379)
 ERC721Test:testGetApprovedNotApprovedTokenId() (gas: 171330)
@@ -526,19 +569,19 @@ ERC721Test:testTransferOwnershipToZeroAddress() (gas: 16166)
 MathTest:testCbrtRoundDown() (gas: 44669)
 MathTest:testCbrtRoundUp() (gas: 45449)
 MathTest:testCeilDiv() (gas: 12699)
-MathTest:testFuzzCbrt(uint256,bool) (runs: 256, μ: 19498, ~: 19192)
+MathTest:testFuzzCbrt(uint256,bool) (runs: 256, μ: 19502, ~: 19192)
 MathTest:testFuzzCeilDiv(uint256,uint256) (runs: 256, μ: 9013, ~: 9030)
 MathTest:testFuzzInt256Average(int256,int256) (runs: 256, μ: 5578, ~: 5578)
 MathTest:testFuzzIsNegative(int256) (runs: 256, μ: 5698, ~: 5698)
 MathTest:testFuzzLog10(uint256,bool) (runs: 256, μ: 7180, ~: 7405)
-MathTest:testFuzzLog2(uint256,bool) (runs: 256, μ: 7013, ~: 7153)
-MathTest:testFuzzLog256(uint256,bool) (runs: 256, μ: 7088, ~: 7347)
-MathTest:testFuzzMulDiv(uint256,uint256,uint256) (runs: 256, μ: 13025, ~: 12773)
-MathTest:testFuzzMulDivDomain(uint256,uint256,uint256) (runs: 256, μ: 10633, ~: 10710)
+MathTest:testFuzzLog2(uint256,bool) (runs: 256, μ: 7015, ~: 7162)
+MathTest:testFuzzLog256(uint256,bool) (runs: 256, μ: 7096, ~: 7347)
+MathTest:testFuzzMulDiv(uint256,uint256,uint256) (runs: 256, μ: 13021, ~: 12773)
+MathTest:testFuzzMulDivDomain(uint256,uint256,uint256) (runs: 256, μ: 10636, ~: 10710)
 MathTest:testFuzzUint256Average(uint256,uint256) (runs: 256, μ: 5768, ~: 5768)
-MathTest:testFuzzWadCbrt(uint256) (runs: 256, μ: 19213, ~: 18938)
-MathTest:testFuzzWadExp(int256) (runs: 256, μ: 15387, ~: 15457)
-MathTest:testFuzzWadLn(int256) (runs: 256, μ: 16909, ~: 16707)
+MathTest:testFuzzWadCbrt(uint256) (runs: 256, μ: 19220, ~: 18938)
+MathTest:testFuzzWadExp(int256) (runs: 256, μ: 15388, ~: 15457)
+MathTest:testFuzzWadLn(int256) (runs: 256, μ: 16905, ~: 16703)
 MathTest:testInt256Average() (gas: 11958)
 MathTest:testIsNegative() (gas: 10644)
 MathTest:testLog10RoundDown() (gas: 19280)
@@ -559,9 +602,9 @@ MathTest:testWadExp() (gas: 26657)
 MathTest:testWadExpOverflow() (gas: 10670)
 MathTest:testWadLn() (gas: 25780)
 MathTest:testWadLnNegativeValues() (gas: 10559)
-MerkleProofVerificationTest:testFuzzMultiProofVerifySingleLeaf(bytes32[],uint256) (runs: 256, μ: 1649976903, ~: 1649973660)
-MerkleProofVerificationTest:testFuzzVerify(bytes32[],uint256) (runs: 256, μ: 135975584, ~: 135972312)
-MerkleProofVerificationTest:testFuzzVerifyMultiProofMultipleLeaves(bool,bool,bool) (runs: 256, μ: 412472433, ~: 412472367)
+MerkleProofVerificationTest:testFuzzMultiProofVerifySingleLeaf(bytes32[],uint256) (runs: 256, μ: 1649976908, ~: 1649973650)
+MerkleProofVerificationTest:testFuzzVerify(bytes32[],uint256) (runs: 256, μ: 135975589, ~: 135972302)
+MerkleProofVerificationTest:testFuzzVerifyMultiProofMultipleLeaves(bool,bool,bool) (runs: 256, μ: 412472432, ~: 412472367)
 MerkleProofVerificationTest:testInvalidMerkleMultiProof() (gas: 412478445)
 MerkleProofVerificationTest:testInvalidMerkleProof() (gas: 33970386)
 MerkleProofVerificationTest:testInvalidMerkleProofLength() (gas: 33972544)
@@ -588,7 +631,7 @@ Ownable2StepInvariants:invariantPendingOwner() (runs: 256, calls: 3840, reverts:
 Ownable2StepTest:testAcceptOwnershipNonPendingOwner() (gas: 46640)
 Ownable2StepTest:testAcceptOwnershipSuccess() (gas: 39408)
 Ownable2StepTest:testFuzzAcceptOwnershipNonPendingOwner(address) (runs: 256, μ: 45872, ~: 45872)
-Ownable2StepTest:testFuzzAcceptOwnershipSuccess(address,address) (runs: 256, μ: 65123, ~: 65097)
+Ownable2StepTest:testFuzzAcceptOwnershipSuccess(address,address) (runs: 256, μ: 65122, ~: 65097)
 Ownable2StepTest:testFuzzPendingOwnerResetAfterRenounceOwnership(address) (runs: 256, μ: 38769, ~: 38754)
 Ownable2StepTest:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 13996, ~: 13996)
 Ownable2StepTest:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 43699, ~: 43684)
@@ -625,8 +668,8 @@ SignatureCheckerTest:testEOAWithInvalidSignature2() (gas: 20268)
 SignatureCheckerTest:testEOAWithInvalidSigner() (gas: 20501)
 SignatureCheckerTest:testEOAWithTooHighSValue() (gas: 17257)
 SignatureCheckerTest:testEOAWithValidSignature() (gas: 19600)
-SignatureCheckerTest:testFuzzEIP1271WithInvalidSigner(string,string) (runs: 256, μ: 35943, ~: 35950)
+SignatureCheckerTest:testFuzzEIP1271WithInvalidSigner(string,string) (runs: 256, μ: 35938, ~: 35950)
 SignatureCheckerTest:testFuzzEIP1271WithValidSignature(string) (runs: 256, μ: 34486, ~: 34488)
 SignatureCheckerTest:testFuzzEOAWithInvalidSignature(bytes,string) (runs: 256, μ: 16905, ~: 16900)
-SignatureCheckerTest:testFuzzEOAWithInvalidSigner(string,string) (runs: 256, μ: 21582, ~: 21581)
-SignatureCheckerTest:testFuzzEOAWithValidSignature(string,string) (runs: 256, μ: 20697, ~: 20696)
\ No newline at end of file
+SignatureCheckerTest:testFuzzEOAWithInvalidSigner(string,string) (runs: 256, μ: 21581, ~: 21593)
+SignatureCheckerTest:testFuzzEOAWithValidSignature(string,string) (runs: 256, μ: 20696, ~: 20708)
\ No newline at end of file
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 440edfc6..7e18bfc9 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -104,4 +104,5 @@ jobs:
           awesome_bot ./*.md src/**/*.vy src/**/interfaces/*.vy \
           test/**/*.sol test/**/interfaces/*.sol test/**/mocks/*.sol \
           test/**/scripts/*.js scripts/*.py --allow-dupe --allow-redirect \
-          --request-delay 0.4 --white-list https://www.wagmi.xyz,https://github.com/pcaversaccio/snekmate/releases/tag/v0.0.3,https://github.com/pcaversaccio/snekmate/blob/v0.0.3
+          --request-delay 0.4 \
+          --white-list https://www.wagmi.xyz,https://github.com/pcaversaccio/snekmate/releases/tag/v0.0.3,https://github.com/pcaversaccio/snekmate/blob/v0.0.3,https://github.com/pcaversaccio/snekmate/compare/v0.0.2...v0.0.3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9fa047ea..60260d4c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,11 +2,20 @@
 
 ## [`0.0.3`](https://github.com/pcaversaccio/snekmate/releases/tag/v0.0.3) (Unreleased)
 
+### 💥 New Features
+
+- **Extensions**
+  - [`ERC2981`](https://github.com/pcaversaccio/snekmate/blob/v0.0.3/src/extensions/ERC2981.vy): An [`ERC-721`](https://eips.ethereum.org/EIPS/eip-721) and [`ERC-1155`](https://eips.ethereum.org/EIPS/eip-1155) compatible [`ERC-2981`](https://eips.ethereum.org/EIPS/eip-2981) reference implementation. ([#138](https://github.com/pcaversaccio/snekmate/pull/138))
+
 ### 🥢 Test Coverage
 
 - **Utility Functions**
   - [`MerkleProofVerificationTest`](https://github.com/pcaversaccio/snekmate/blob/v0.0.3/test/utils/MerkleProofVerification.t.sol): Add an additional test for a possible `multi_proof_verify` invariant violation. ([#137](https://github.com/pcaversaccio/snekmate/pull/137))
 
+### 👀 Full Changelog
+
+- [`v0.0.2...v0.0.3`](https://github.com/pcaversaccio/snekmate/compare/v0.0.2...v0.0.3)
+
 ## [`0.0.2`](https://github.com/pcaversaccio/snekmate/releases/tag/v0.0.2) (07-06-2023)
 
 ### 💥 New Features
diff --git a/README.md b/README.md
index 41d2738b..1799a1fa 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ src
 │  ├─ Ownable2Step — "2-Step Ownership Transfer Functions"
 │  ├─ AccessControl — "Multi-Role-Based Access Control Functions"
 ├─ extensions
+│  ├─ ERC2981 — "ERC-721 and ERC-1155 Compatible ERC-2981 Reference Implementation"
 │  ├─ ERC4626 — "Modern and Gas-Efficient ERC-4626 Tokenised Vault Implementation"
 ├─ tokens
 │  ├─ ERC20 — "Modern and Gas-Efficient ERC-20 + EIP-2612 Implementation"
@@ -83,6 +84,7 @@ This repository contains [Foundry](https://github.com/foundry-rs/foundry)-based
 | `Ownable`                 |       ✅       |            ✅            |         ✅          |
 | `Ownable2Step`            |       ✅       |            ✅            |         ✅          |
 | `AccessControl`           |       ✅       |            ✅            |         ✅          |
+| `ERC2981`                 |       ✅       |            ✅            |         ✅          |
 | `ERC4626`                 |       ✅       |            ✅            |         ✅          |
 | `ERC20`                   |       ✅       |            ✅            |         ✅          |
 | `ERC721`                  |       ✅       |            ✅            |         ✅          |
diff --git a/lib/create-util b/lib/create-util
index c9da441d..6ff50b8c 160000
--- a/lib/create-util
+++ b/lib/create-util
@@ -1,1 +1,1 @@
-Subproject commit c9da441d34c826f2902780c0cb8fe4543bfd754c
+Subproject commit 6ff50b8c54ed3cae54ad9ec50510fe7601379927
diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts
index 1a77a508..1e0e4e20 160000
--- a/lib/openzeppelin-contracts
+++ b/lib/openzeppelin-contracts
@@ -1,1 +1,1 @@
-Subproject commit 1a77a508f93e2df058cb082def4753a060aefa8f
+Subproject commit 1e0e4e20bb3a73436d3b02a9a9d3e607ea89a931
diff --git a/lib/solady b/lib/solady
index 50cbe190..8c751db0 160000
--- a/lib/solady
+++ b/lib/solady
@@ -1,1 +1,1 @@
-Subproject commit 50cbe1909e773b7e4ba76049c75a203e626d55ba
+Subproject commit 8c751db06e614e26e6acb9b385b463feef5e3a9f
diff --git a/package.json b/package.json
index 2d6efdc1..33d9635f 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
     "CHANGELOG.md"
   ],
   "devDependencies": {
-    "@openzeppelin/merkle-tree": "^1.0.4",
+    "@openzeppelin/merkle-tree": "^1.0.5",
     "eslint": "^8.43.0",
     "eslint-config-prettier": "^8.8.0",
     "ethers": "^6.6.0",
diff --git a/src/extensions/ERC2981.vy b/src/extensions/ERC2981.vy
new file mode 100644
index 00000000..ec712789
--- /dev/null
+++ b/src/extensions/ERC2981.vy
@@ -0,0 +1,340 @@
+# @version ^0.3.9
+"""
+@title ERC-721 and ERC-1155 Compatible ERC-2981 Reference Implementation
+@custom:contract-name ERC2981
+@license GNU Affero General Public License v3.0 only
+@author pcaversaccio
+@notice Reference implementation of the non-fungible token (NFT) royalty
+        standard EIP-2981 (https://eips.ethereum.org/EIPS/eip-2981), a
+        standardised way to retrieve on-chain royalty payment information.
+        This implementation can be seamlessly integrated into an ERC-721 or
+        ERC-1155 contract. The royalty information can be specified globally
+        for all token IDs via the access-restricted function `set_default_royalty`,
+        and/or individually for specific token IDs via the access-restricted
+        function `set_token_royalty`. The latter takes precedence over the
+        first. Furthermore, the royalty is specified as a fraction of the sale
+        price. The `internal` state variable `_fee_denominator` defaults to
+        `10_000`, meaning the fee is specified in basis points by default.
+
+        IMPORTANT: The ERC-2981 standard only specifies a way to signal royalty
+        information and does not enforce its payment:
+        https://eips.ethereum.org/EIPS/eip-2981#rationale.
+        Marketplaces are expected to voluntarily pay royalties together with sales,
+        but note that this standard is not yet widely supported.
+
+        The implementation is inspired by OpenZeppelin's implementation here:
+        https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/common/ERC2981.sol.
+@custom:security If you integrate this contract with an ERC-721 contract, please
+                 consider clearing the royalty information from storage on calling
+                 `burn` (to avoid any NatSpec parsing error, no `@` character is added
+                 to the visibility decorator `@internal` in the following example;
+                 please add it accordingly):
+                 ```vy
+                 internal
+                 def _burn(token_id: uint256):
+                     ...
+                     self._reset_token_royalty(token_id)
+                 ```
+
+                 Due to the fungibility of ERC-1155 tokens, the implementation of
+                 this mechanism with ERC-1155 tokens is not recommended.
+"""
+
+
+# @dev We import and implement the `ERC165` interface,
+# which is a built-in interface of the Vyper compiler.
+from vyper.interfaces import ERC165
+implements: ERC165
+
+
+# @dev We import and implement the `IERC2981` interface,
+# which is written using standard Vyper syntax.
+import interfaces.IERC2981 as IERC2981
+implements: IERC2981
+
+
+# @dev Stores the ERC-165 interface identifier for each
+# imported interface. The ERC-165 interface identifier
+# is defined as the XOR of all function selectors in the
+# interface.
+# @notice If you are integrating this contract with an
+# ERC-721 or ERC-1155 contract, please ensure you include
+# the additional ERC-165 interface identifiers.
+_SUPPORTED_INTERFACES: constant(bytes4[2]) = [
+    0x01FFC9A7, # The ERC-165 identifier for ERC-165.
+    0x2A55205A, # The ERC-165 identifier for ERC-2981.
+]
+
+
+# @dev Returns the address of the current owner.
+owner: public(address)
+
+
+# @dev Tightly packed royalty information struct. Note that
+# Vyper does not currently support tight packing, but will
+# do so in the near future.
+struct RoyaltyInfo:
+    receiver: address
+    royalty_fraction: uint96
+
+
+# @dev Default royalty information struct.
+_default_royalty_info: RoyaltyInfo
+
+
+# @dev Mapping from token ID to the `RoyaltyInfo` struct.
+_token_royalty_info: HashMap[uint256, RoyaltyInfo]
+
+
+# @dev The denominator with which to interpret the fee set
+# in `_set_token_royalty` and `_set_default_royalty` as a
+# fraction of the sale price. Defaults to `10_000` so fees
+# are expressed in basis points.
+_fee_denominator: uint256
+
+
+# @dev Emitted when the ownership is transferred
+# from `previous_owner` to `new_owner`.
+event OwnershipTransferred:
+    previous_owner: indexed(address)
+    new_owner: indexed(address)
+
+
+@external
+@payable
+def __init__():
+    """
+    @dev To omit the opcodes for checking the `msg.value`
+         in the creation-time EVM bytecode, the constructor
+         is declared as `payable`.
+    @notice We set the default value of `_fee_denominator`
+            to `10_000` so that the fee is in basis points by
+            default. Also, the `owner` role will be assigned
+            to the `msg.sender`.
+
+            IMPORTANT: The `_default_royalty_info` is set to
+            the EVM default values `receiver = empty(address)`
+            and `royalty_fraction = empty(uint96)`. If you want
+            to set your own default values during contract creation,
+            you can call `self._default_royalty(receiver, fee_numerator)`
+            in the constructor.
+    """
+    self._fee_denominator = 10_000
+    self._transfer_ownership(msg.sender)
+
+
+@external
+@view
+def supportsInterface(interface_id: bytes4) -> bool:
+    """
+    @dev Returns `True` if this contract implements the
+         interface defined by `interface_id`.
+    @param interface_id The 4-byte interface identifier.
+    @return bool The verification whether the contract
+            implements the interface or not.
+    """
+    return interface_id in _SUPPORTED_INTERFACES
+
+
+@external
+@view
+def royaltyInfo(token_id: uint256, sale_price: uint256) -> (address, uint256):
+    """
+    @dev Returns how much royalty is owed and to whom, based
+         on a sale price that may be denominated in any unit
+         of exchange. The royalty amount is denominated and
+         should be paid in that same unit of exchange.
+    @param token_id The 32-byte identifier of the token.
+    @param sale_price The 32-byte sale price of the NFT asset
+           specified by `token_id`.
+    @return address The 20-byte address of the recipient of
+            the royalty payment.
+    @return uint256 The 32-byte royalty payment amount for
+            `sale_price`.
+    """
+    royalty: RoyaltyInfo = self._token_royalty_info[token_id]
+    if (royalty.receiver == empty(address)):
+        royalty = self._default_royalty_info
+
+    # The following line uses intentionally checked arithmetic to
+    # prevent a theoretically possible overflow.
+    royalty_amount: uint256 = (sale_price * convert(royalty.royalty_fraction, uint256)) / self._fee_denominator
+
+    return (royalty.receiver, royalty_amount)
+
+
+@external
+def set_default_royalty(receiver: address, fee_numerator: uint96):
+    """
+    @dev Sets the royalty information that all IDs in this
+         contract will default to. This function can only be
+         called by the current `owner`.
+    @notice Note that the `receiver` cannot be the zero address.
+            Also, `fee_numerator` cannot be greater than the fee
+            denominator.
+    @param receiver The 20-byte address of the recipient of
+           the royalty payment.
+    @param fee_numerator The 12-byte fee numerator used to calculate
+           the royalty fraction.
+    """
+    self._check_owner()
+    self._set_default_royalty(receiver, fee_numerator)
+
+
+@external
+def delete_default_royalty():
+    """
+    @dev Removes the default royalty information. This function can only
+         be called by the current `owner`.
+    """
+    self._check_owner()
+    self._delete_default_royalty()
+
+
+@external
+def set_token_royalty(token_id: uint256, receiver: address, fee_numerator: uint96):
+    """
+    @dev Sets the royalty information for a specific token ID,
+         overriding the global default. This function can only
+         be called by the current `owner`.
+    @notice Note that the `receiver` cannot be the zero address.
+            Also, `fee_numerator` cannot be greater than the fee
+            denominator.
+    @param token_id The 32-byte identifier of the token.
+    @param receiver The 20-byte address of the recipient of
+           the royalty payment.
+    @param fee_numerator The 12-byte fee numerator used to calculate
+           the royalty fraction.
+    """
+    self._check_owner()
+    self._set_token_royalty(token_id, receiver, fee_numerator)
+
+
+@external
+def reset_token_royalty(token_id: uint256):
+    """
+    @dev Resets the royalty information for the token ID back to
+         the global default. This function can only be called by
+         the current `owner`.
+    @param token_id The 32-byte identifier of the token.
+    """
+    self._check_owner()
+    self._reset_token_royalty(token_id)
+
+
+@external
+def transfer_ownership(new_owner: address):
+    """
+    @dev Sourced from {Ownable-transfer_ownership}.
+    @notice See {Ownable-transfer_ownership} for
+            the function docstring.
+    @custom:security If you integrate this implementation into an
+                     ERC-721 or ERC-1155 contract that implements
+                     an `is_minter` role, ensure that the previous
+                     owner's minter role is also removed and the
+                     minter role is assigned to the `new_owner`
+                     accordingly.
+    """
+    self._check_owner()
+    assert new_owner != empty(address), "Ownable: new owner is the zero address"
+    self._transfer_ownership(new_owner)
+
+
+@external
+def renounce_ownership():
+    """
+    @dev Sourced from {Ownable-renounce_ownership}.
+    @notice See {Ownable-renounce_ownership} for
+            the function docstring.
+    @custom:security If you integrate this implementation into an
+                     ERC-721 or ERC-1155 contract that implements
+                     an `is_minter` role, ensure that the previous
+                     owner's minter role as well as all non-owner
+                     minter addresses are also removed before calling
+                     `renounce_ownership`.
+    """
+    self._check_owner()
+    self._transfer_ownership(empty(address))
+
+
+@internal
+def _set_default_royalty(receiver: address, fee_numerator: uint96):
+    """
+    @dev Sets the royalty information that all IDs in this
+         contract will default to.
+    @notice This is an `internal` function without access restriction.
+            Note that the `receiver` cannot be the zero address.
+            Also, `fee_numerator` cannot be greater than the fee
+            denominator.
+    @param receiver The 20-byte address of the recipient of
+           the royalty payment.
+    @param fee_numerator The 12-byte fee numerator used to calculate
+           the royalty fraction.
+    """
+    denominator: uint256 = self._fee_denominator
+    assert convert(fee_numerator, uint256) <= denominator, "ERC2981: royalty fee will exceed sale_price"
+    assert receiver != empty(address), "ERC2981: invalid receiver"
+    self._default_royalty_info = RoyaltyInfo({receiver: receiver, royalty_fraction: fee_numerator})
+
+
+@internal
+def _delete_default_royalty():
+    """
+    @dev Removes the default royalty information.
+    @notice This is an `internal` function without access restriction.
+    """
+    self._default_royalty_info = RoyaltyInfo({receiver: empty(address), royalty_fraction: empty(uint96)})
+
+
+@internal
+def _set_token_royalty(token_id: uint256, receiver: address, fee_numerator: uint96):
+    """
+    @dev Sets the royalty information for a specific token ID,
+         overriding the global default.
+    @notice This is an `internal` function without access restriction.
+            Note that the `receiver` cannot be the zero address.
+            Also, `fee_numerator` cannot be greater than the fee
+            denominator.
+    @param token_id The 32-byte identifier of the token.
+    @param receiver The 20-byte address of the recipient of
+           the royalty payment.
+    @param fee_numerator The 12-byte fee numerator used to calculate
+           the royalty fraction.
+    """
+    denominator: uint256 = self._fee_denominator
+    assert convert(fee_numerator, uint256) <= denominator, "ERC2981: royalty fee will exceed sale_price"
+    assert receiver != empty(address), "ERC2981: invalid receiver"
+    self._token_royalty_info[token_id] = RoyaltyInfo({receiver: receiver, royalty_fraction: fee_numerator})
+
+
+@internal
+def _reset_token_royalty(token_id: uint256):
+    """
+    @dev Resets the royalty information for the token ID back to
+         the global default.
+    @notice This is an `internal` function without access restriction.
+    @param token_id The 32-byte identifier of the token.
+    """
+    self._token_royalty_info[token_id] = RoyaltyInfo({receiver: empty(address), royalty_fraction: empty(uint96)})
+
+
+@internal
+def _check_owner():
+    """
+    @dev Sourced from {Ownable-_check_owner}.
+    @notice See {Ownable-_check_owner} for
+            the function docstring.
+    """
+    assert msg.sender == self.owner, "Ownable: caller is not the owner"
+
+
+@internal
+def _transfer_ownership(new_owner: address):
+    """
+    @dev Sourced from {Ownable-_transfer_ownership}.
+    @notice See {Ownable-_transfer_ownership} for
+            the function docstring.
+    """
+    old_owner: address = self.owner
+    self.owner = new_owner
+    log OwnershipTransferred(old_owner, new_owner)
diff --git a/src/extensions/interfaces/IERC2981.vy b/src/extensions/interfaces/IERC2981.vy
new file mode 100644
index 00000000..a1ffe466
--- /dev/null
+++ b/src/extensions/interfaces/IERC2981.vy
@@ -0,0 +1,58 @@
+# @version ^0.3.9
+"""
+@title EIP-2981 Interface Definition
+@custom:contract-name IERC2981
+@license GNU Affero General Public License v3.0 only
+@author pcaversaccio
+@notice The ERC-2981 introduces a standardised way to retrieve
+        royalty payment information for non-fungible tokens (NFTs)
+        to enable universal support for royalty payments across
+        all NFT marketplaces and ecosystem participants. The ERC-165
+        identifier for this interface is `0x2A55205A`. For more
+        details, please refer to:
+        https://eips.ethereum.org/EIPS/eip-2981.
+
+        On how to use interfaces in Vyper, please visit:
+        https://vyper.readthedocs.io/en/latest/interfaces.html#interfaces.
+"""
+
+
+# @dev We import and implement the `ERC165` interface,
+# which is a built-in interface of the Vyper compiler.
+from vyper.interfaces import ERC165
+implements: ERC165
+
+
+@external
+@view
+def supportsInterface(interfaceId: bytes4) -> bool:
+    """
+    @dev Returns `True` if this contract implements the
+         interface defined by `interfaceId`.
+    @notice For more details on how these identifiers are
+            created, please refer to:
+            https://eips.ethereum.org/EIPS/eip-165.
+    @param interfaceId The 4-byte interface identifier.
+    @return bool The verification whether the contract
+            implements the interface or not.
+    """
+    return empty(bool)
+
+
+@external
+@view
+def royaltyInfo(_tokenId: uint256, _salePrice: uint256) -> (address, uint256):
+    """
+    @dev Returns how much royalty is owed and to whom, based
+         on a sale price that may be denominated in any unit
+         of exchange. The royalty amount is denominated and
+         should be paid in that same unit of exchange.
+    @param _tokenId The 32-byte identifier of the token.
+    @param _salePrice The 32-byte sale price of the NFT asset
+           specified by `_tokenId`.
+    @return address The 20-byte address of the recipient of
+            the royalty payment.
+    @return uint256 The 32-byte royalty payment amount for
+            `_salePrice`.
+    """
+    return (empty(address), empty(uint256))
diff --git a/src/tokens/interfaces/IERC1155.vy b/src/tokens/interfaces/IERC1155.vy
index d55e9a6f..8b44f089 100644
--- a/src/tokens/interfaces/IERC1155.vy
+++ b/src/tokens/interfaces/IERC1155.vy
@@ -73,14 +73,14 @@ event URI:
 
 @external
 @view
-def supportsInterface(interface_id: bytes4) -> bool:
+def supportsInterface(interfaceId: bytes4) -> bool:
     """
     @dev Returns `True` if this contract implements the
-         interface defined by `interface_id`.
+         interface defined by `interfaceId`.
     @notice For more details on how these identifiers are
             created, please refer to:
             https://eips.ethereum.org/EIPS/eip-165.
-    @param interface_id The 4-byte interface identifier.
+    @param interfaceId The 4-byte interface identifier.
     @return bool The verification whether the contract
             implements the interface or not.
     """
diff --git a/src/tokens/interfaces/IERC1155MetadataURI.vy b/src/tokens/interfaces/IERC1155MetadataURI.vy
index 906aff15..1a5498ad 100644
--- a/src/tokens/interfaces/IERC1155MetadataURI.vy
+++ b/src/tokens/interfaces/IERC1155MetadataURI.vy
@@ -29,16 +29,28 @@ from vyper.interfaces import ERC165
 implements: ERC165
 
 
+# @dev We import the `IERC1155` interface, which is written
+# using standard Vyper syntax, to highlight the association
+# of the custom `IERC1155MetadataURI` interface with the custom
+# `IERC1155` interface.
+# @notice The interface `IERC1155MetadataURI` must be used in
+# conjunction with the custom interface `IERC1155` to be EIP-1155
+# compatible. If you want to use this interface as a stand-alone
+# interface, you must add `implements: IERC1155` to this interface
+# and implement all required events and functions accordingly.
+import IERC1155 as IERC1155
+
+
 @external
 @view
-def supportsInterface(interface_id: bytes4) -> bool:
+def supportsInterface(interfaceId: bytes4) -> bool:
     """
     @dev Returns `True` if this contract implements the
-         interface defined by `interface_id`.
+         interface defined by `interfaceId`.
     @notice For more details on how these identifiers are
             created, please refer to:
             https://eips.ethereum.org/EIPS/eip-165.
-    @param interface_id The 4-byte interface identifier.
+    @param interfaceId The 4-byte interface identifier.
     @return bool The verification whether the contract
             implements the interface or not.
     """
diff --git a/src/tokens/interfaces/IERC1155Receiver.vy b/src/tokens/interfaces/IERC1155Receiver.vy
index 92112530..51ee9857 100644
--- a/src/tokens/interfaces/IERC1155Receiver.vy
+++ b/src/tokens/interfaces/IERC1155Receiver.vy
@@ -23,14 +23,14 @@ implements: ERC165
 
 @external
 @view
-def supportsInterface(interface_id: bytes4) -> bool:
+def supportsInterface(interfaceId: bytes4) -> bool:
     """
     @dev Returns `True` if this contract implements the
-         interface defined by `interface_id`.
+         interface defined by `interfaceId`.
     @notice For more details on how these identifiers are
             created, please refer to:
             https://eips.ethereum.org/EIPS/eip-165.
-    @param interface_id The 4-byte interface identifier.
+    @param interfaceId The 4-byte interface identifier.
     @return bool The verification whether the contract
             implements the interface or not.
     """
diff --git a/src/tokens/interfaces/IERC4906.vy b/src/tokens/interfaces/IERC4906.vy
index 1140427c..7663e931 100644
--- a/src/tokens/interfaces/IERC4906.vy
+++ b/src/tokens/interfaces/IERC4906.vy
@@ -21,6 +21,18 @@ from vyper.interfaces import ERC165
 implements: ERC165
 
 
+# @dev We import the `ERC721` interface, which is a built-in
+# interface of the Vyper compiler, to highlight the association
+# of the custom `IERC4906` interface with the built-in `ERC721`
+# interface.
+# @notice The interface `IERC4906` must be used in conjunction
+# with the built-in interface `ERC721` to be EIP-721 compatible.
+# If you want to use this interface as a stand-alone interface,
+# you must add `implements: ERC721` to this interface and implement
+# all required events and functions accordingly.
+from vyper.interfaces import ERC721
+
+
 # @dev Emitted when the metadata of a token is changed.
 # Thus, third-party platforms, such as NFT marketplaces,
 # can update the images and associated attributes of the
@@ -40,14 +52,14 @@ event BatchMetadataUpdate:
 
 @external
 @view
-def supportsInterface(interface_id: bytes4) -> bool:
+def supportsInterface(interfaceId: bytes4) -> bool:
     """
     @dev Returns `True` if this contract implements the
-         interface defined by `interface_id`.
+         interface defined by `interfaceId`.
     @notice For more details on how these identifiers are
             created, please refer to:
             https://eips.ethereum.org/EIPS/eip-165.
-    @param interface_id The 4-byte interface identifier.
+    @param interfaceId The 4-byte interface identifier.
     @return bool The verification whether the contract
             implements the interface or not.
     """
diff --git a/src/tokens/interfaces/IERC721Enumerable.vy b/src/tokens/interfaces/IERC721Enumerable.vy
index 7192623e..7b2baf60 100644
--- a/src/tokens/interfaces/IERC721Enumerable.vy
+++ b/src/tokens/interfaces/IERC721Enumerable.vy
@@ -21,16 +21,28 @@ from vyper.interfaces import ERC165
 implements: ERC165
 
 
+# @dev We import the `ERC721` interface, which is a built-in
+# interface of the Vyper compiler, to highlight the association
+# of the custom `IERC721Enumerable` interface with the built-in
+# `ERC721` interface.
+# @notice The interface `IERC721Enumerable` must be used in conjunction
+# with the built-in interface `ERC721` to be EIP-721 compatible.
+# If you want to use this interface as a stand-alone interface,
+# you must add `implements: ERC721` to this interface and implement
+# all required events and functions accordingly.
+from vyper.interfaces import ERC721
+
+
 @external
 @view
-def supportsInterface(interface_id: bytes4) -> bool:
+def supportsInterface(interfaceId: bytes4) -> bool:
     """
     @dev Returns `True` if this contract implements the
-         interface defined by `interface_id`.
+         interface defined by `interfaceId`.
     @notice For more details on how these identifiers are
             created, please refer to:
             https://eips.ethereum.org/EIPS/eip-165.
-    @param interface_id The 4-byte interface identifier.
+    @param interfaceId The 4-byte interface identifier.
     @return bool The verification whether the contract
             implements the interface or not.
     """
diff --git a/src/tokens/interfaces/IERC721Metadata.vy b/src/tokens/interfaces/IERC721Metadata.vy
index 6c00665e..b22b02c2 100644
--- a/src/tokens/interfaces/IERC721Metadata.vy
+++ b/src/tokens/interfaces/IERC721Metadata.vy
@@ -29,16 +29,28 @@ from vyper.interfaces import ERC165
 implements: ERC165
 
 
+# @dev We import the `ERC721` interface, which is a built-in
+# interface of the Vyper compiler, to highlight the association
+# of the custom `IERC721Metadata` interface with the built-in
+# `ERC721` interface.
+# @notice The interface `IERC721Metadata` must be used in conjunction
+# with the built-in interface `ERC721` to be EIP-721 compatible.
+# If you want to use this interface as a stand-alone interface,
+# you must add `implements: ERC721` to this interface and implement
+# all required events and functions accordingly.
+from vyper.interfaces import ERC721
+
+
 @external
 @view
-def supportsInterface(interface_id: bytes4) -> bool:
+def supportsInterface(interfaceId: bytes4) -> bool:
     """
     @dev Returns `True` if this contract implements the
-         interface defined by `interface_id`.
+         interface defined by `interfaceId`.
     @notice For more details on how these identifiers are
             created, please refer to:
             https://eips.ethereum.org/EIPS/eip-165.
-    @param interface_id The 4-byte interface identifier.
+    @param interfaceId The 4-byte interface identifier.
     @return bool The verification whether the contract
             implements the interface or not.
     """
diff --git a/src/tokens/interfaces/IERC721Permit.vy b/src/tokens/interfaces/IERC721Permit.vy
index 0ed3f3be..ad9c4887 100644
--- a/src/tokens/interfaces/IERC721Permit.vy
+++ b/src/tokens/interfaces/IERC721Permit.vy
@@ -36,14 +36,14 @@ implements: ERC165
 
 @external
 @view
-def supportsInterface(interface_id: bytes4) -> bool:
+def supportsInterface(interfaceId: bytes4) -> bool:
     """
     @dev Returns `True` if this contract implements the
-         interface defined by `interface_id`.
+         interface defined by `interfaceId`.
     @notice For more details on how these identifiers are
             created, please refer to:
             https://eips.ethereum.org/EIPS/eip-165.
-    @param interface_id The 4-byte interface identifier.
+    @param interfaceId The 4-byte interface identifier.
     @return bool The verification whether the contract
             implements the interface or not.
     """
diff --git a/src/utils/CreateAddress.vy b/src/utils/CreateAddress.vy
index 4a84e68f..b035e346 100644
--- a/src/utils/CreateAddress.vy
+++ b/src/utils/CreateAddress.vy
@@ -68,7 +68,7 @@ def _compute_address_rlp(deployer: address, nonce: uint256) -> address:
          `CREATE` opcode. For the specification of the Recursive Length
          Prefix (RLP) encoding scheme, please refer to p. 19 of the Ethereum
          Yellow Paper (https://ethereum.github.io/yellowpaper/paper.pdf)
-         and the Ethereum Wiki (https://eth.wiki/fundamentals/rlp).
+         and the Ethereum Wiki (https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp).
          For further insights also, see the following issue:
          https://github.com/transmissions11/solmate/issues/207.
 
diff --git a/yarn.lock b/yarn.lock
index 6c0a8737..ee2aeede 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -271,10 +271,10 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
-"@openzeppelin/merkle-tree@^1.0.4":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@openzeppelin/merkle-tree/-/merkle-tree-1.0.4.tgz#9019c2990ffdb56fca232c210d1e1132cbd68924"
-  integrity sha512-gNMYRgIn7mkPWpE2sBGfE12fFM5pfbGrDr0leaMdCkmFyccQlqWYL4p0FedcVqN6X0ETJGTa+UKF1fUWAzv9VQ==
+"@openzeppelin/merkle-tree@^1.0.5":
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/@openzeppelin/merkle-tree/-/merkle-tree-1.0.5.tgz#4836d377777a7e39f31674f06ec3d6909def7913"
+  integrity sha512-JkwG2ysdHeIphrScNxYagPy6jZeNONgDRyqU6lbFgE8HKCZFSkcP8r6AjZs+3HZk4uRNV0kNBBzuWhKQ3YV7Kw==
   dependencies:
     "@ethersproject/abi" "^5.7.0"
     ethereum-cryptography "^1.1.2"


Test patch:
diff --git a/lib/prb-test b/lib/prb-test
index a245c71e..dfa0c02e 160000
--- a/lib/prb-test
+++ b/lib/prb-test
@@ -1,1 +1,1 @@
-Subproject commit a245c71edc07643aedfe27df4cc7b21048aa8824
+Subproject commit dfa0c02e1b593ced4cc823a8b852111890290429
diff --git a/test/extensions/ERC2981.t.sol b/test/extensions/ERC2981.t.sol
new file mode 100644
index 00000000..81ade777
--- /dev/null
+++ b/test/extensions/ERC2981.t.sol
@@ -0,0 +1,824 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {VyperDeployer} from "utils/VyperDeployer.sol";
+
+import {IERC165} from "openzeppelin/utils/introspection/IERC165.sol";
+import {IERC2981} from "openzeppelin/interfaces/IERC2981.sol";
+
+import {IERC2981Extended} from "./interfaces/IERC2981Extended.sol";
+
+contract ERC2981Test is Test {
+    VyperDeployer private vyperDeployer = new VyperDeployer();
+
+    /* solhint-disable var-name-mixedcase */
+    IERC2981Extended private ERC2981Extended;
+    IERC2981Extended private ERC2981ExtendedInitialEvent;
+    /* solhint-enable var-name-mixedcase */
+
+    address private deployer = address(vyperDeployer);
+    address private zeroAddress = address(0);
+
+    event OwnershipTransferred(
+        address indexed previousOwner,
+        address indexed newOwner
+    );
+
+    function setUp() public {
+        ERC2981Extended = IERC2981Extended(
+            vyperDeployer.deployContract("src/extensions/", "ERC2981")
+        );
+    }
+
+    function testInitialSetup() public {
+        uint256 tokenId = 1;
+        uint256 salePrice = 1_000;
+        (address receiver, uint256 royaltyAmount) = ERC2981Extended.royaltyInfo(
+            tokenId,
+            salePrice
+        );
+        assertEq(ERC2981Extended.owner(), deployer);
+        assertEq(receiver, zeroAddress);
+        assertEq(royaltyAmount, 0);
+
+        vm.expectEmit(true, true, false, false);
+        emit OwnershipTransferred(zeroAddress, deployer);
+        ERC2981ExtendedInitialEvent = IERC2981Extended(
+            vyperDeployer.deployContract("src/extensions/", "ERC2981")
+        );
+        (
+            address receiverInitialSetup,
+            uint256 royaltyAmountInitialSetup
+        ) = ERC2981ExtendedInitialEvent.royaltyInfo(tokenId, salePrice);
+        assertEq(ERC2981ExtendedInitialEvent.owner(), deployer);
+        assertEq(receiverInitialSetup, zeroAddress);
+        assertEq(royaltyAmountInitialSetup, 0);
+    }
+
+    function testSupportsInterfaceSuccess() public {
+        assertTrue(
+            ERC2981Extended.supportsInterface(type(IERC165).interfaceId)
+        );
+        assertTrue(
+            ERC2981Extended.supportsInterface(type(IERC2981).interfaceId)
+        );
+    }
+
+    function testSupportsInterfaceGasCost() public {
+        uint256 startGas = gasleft();
+        ERC2981Extended.supportsInterface(type(IERC165).interfaceId);
+        uint256 gasUsed = startGas - gasleft();
+        assertTrue(gasUsed <= 30_000);
+    }
+
+    function testSupportsInterfaceInvalidInterfaceId() public {
+        assertTrue(!ERC2981Extended.supportsInterface(0x0011bbff));
+    }
+
+    function testRoyaltyInfoDefaultRoyalty() public {
+        address owner = deployer;
+        address receiver = makeAddr("receiver");
+        uint256 tokenId1 = 1;
+        uint256 tokenId2 = 2;
+        uint96 royaltyFraction = 10;
+        uint256 salePrice = 1_000;
+        uint256 royalty = (salePrice * royaltyFraction) / 10_000;
+        vm.startPrank(owner);
+        ERC2981Extended.set_default_royalty(receiver, royaltyFraction);
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr1, receiverAddr2);
+        assertEq(royaltyAmount1, royaltyAmount2);
+        assertEq(receiver, receiverAddr1);
+        assertEq(royalty, royaltyAmount1);
+        assertEq(receiver, receiverAddr2);
+        assertEq(royalty, royaltyAmount2);
+        vm.stopPrank();
+    }
+
+    function testRoyaltyInfoUpdateDefaultRoyalty() public {
+        address owner = deployer;
+        address receiver1 = makeAddr("receiver1");
+        address receiver2 = makeAddr("receiver2");
+        uint256 tokenId1 = 1;
+        uint256 tokenId2 = 2;
+        uint96 royaltyFraction = 10;
+        uint256 salePrice = 1_000;
+        uint256 royalty1 = (salePrice * royaltyFraction) / 10_000;
+        uint256 royalty2 = (salePrice * royaltyFraction * 2) / 10_000;
+        vm.startPrank(owner);
+        ERC2981Extended.set_default_royalty(receiver1, royaltyFraction);
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr1, receiverAddr2);
+        assertEq(royaltyAmount1, royaltyAmount2);
+        assertEq(receiver1, receiverAddr1);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount1);
+        assertEq(royalty1, royaltyAmount2);
+
+        ERC2981Extended.set_default_royalty(receiver2, royaltyFraction * 2);
+        (address receiverAddr3, uint256 royaltyAmount3) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr4, uint256 royaltyAmount4) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr3, receiverAddr4);
+        assertEq(royaltyAmount3, royaltyAmount4);
+        assertEq(receiver2, receiverAddr3);
+        assertEq(receiver2, receiverAddr4);
+        assertEq(royalty2, royaltyAmount3);
+        assertEq(royalty2, royaltyAmount4);
+        vm.stopPrank();
+    }
+
+    function testRoyaltyInfoDeleteDefaultRoyalty() public {
+        address owner = deployer;
+        address receiver = makeAddr("receiver");
+        uint256 tokenId1 = 1;
+        uint256 tokenId2 = 2;
+        uint96 royaltyFraction = 10;
+        uint256 salePrice = 1_000;
+        uint256 royalty = (salePrice * royaltyFraction) / 10_000;
+        vm.startPrank(owner);
+        ERC2981Extended.set_default_royalty(receiver, royaltyFraction);
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr1, receiverAddr2);
+        assertEq(royaltyAmount1, royaltyAmount2);
+        assertEq(receiver, receiverAddr1);
+        assertEq(royalty, royaltyAmount1);
+        assertEq(receiver, receiverAddr2);
+        assertEq(royalty, royaltyAmount2);
+
+        ERC2981Extended.delete_default_royalty();
+        (address receiverAddr3, uint256 royaltyAmount3) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr4, uint256 royaltyAmount4) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr3, receiverAddr4);
+        assertEq(royaltyAmount3, royaltyAmount4);
+        assertEq(receiverAddr3, zeroAddress);
+        assertEq(royaltyAmount3, 0);
+        assertEq(receiverAddr3, zeroAddress);
+        assertEq(royaltyAmount4, 0);
+        vm.stopPrank();
+    }
+
+    function testRoyaltyInfoSetTokenRoyalty() public {
+        address owner = deployer;
+        address receiver1 = makeAddr("receiver1");
+        address receiver2 = makeAddr("receiver2");
+        uint256 tokenId1 = 1;
+        uint256 tokenId2 = 2;
+        uint96 royaltyFraction = 10;
+        uint256 salePrice = 1_000;
+        uint256 royalty1 = (salePrice * royaltyFraction) / 10_000;
+        uint256 royalty2 = (salePrice * royaltyFraction * 2) / 10_000;
+        vm.startPrank(owner);
+        ERC2981Extended.set_default_royalty(receiver1, royaltyFraction);
+        ERC2981Extended.set_token_royalty(
+            tokenId1,
+            receiver2,
+            royaltyFraction * 2
+        );
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertTrue(receiverAddr1 != receiverAddr2);
+        assertTrue(royaltyAmount1 != royaltyAmount2);
+        assertEq(receiver2, receiverAddr1);
+        assertEq(royalty2, royaltyAmount1);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount2);
+        vm.stopPrank();
+    }
+
+    function testRoyaltyInfoSetTokenRoyaltyUpdate() public {
+        address owner = deployer;
+        address receiver1 = makeAddr("receiver1");
+        address receiver2 = makeAddr("receiver2");
+        uint256 tokenId1 = 1;
+        uint256 tokenId2 = 2;
+        uint96 royaltyFraction = 10;
+        uint256 salePrice = 1_000;
+        uint256 royalty1 = (salePrice * royaltyFraction) / 10_000;
+        uint256 royalty2 = (salePrice * royaltyFraction * 2) / 10_000;
+        vm.startPrank(owner);
+        ERC2981Extended.set_default_royalty(receiver1, royaltyFraction);
+        ERC2981Extended.set_token_royalty(
+            tokenId1,
+            receiver2,
+            royaltyFraction * 2
+        );
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertTrue(receiverAddr1 != receiverAddr2);
+        assertTrue(royaltyAmount1 != royaltyAmount2);
+        assertEq(receiver2, receiverAddr1);
+        assertEq(royalty2, royaltyAmount1);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount2);
+
+        ERC2981Extended.set_token_royalty(
+            tokenId2,
+            receiver2,
+            royaltyFraction * 2
+        );
+        (address receiverAddr3, uint256 royaltyAmount3) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr4, uint256 royaltyAmount4) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr3, receiverAddr4);
+        assertEq(royaltyAmount3, royaltyAmount4);
+        assertEq(receiver2, receiverAddr3);
+        assertEq(royalty2, royaltyAmount3);
+        assertEq(receiver2, receiverAddr4);
+        assertEq(royalty2, royaltyAmount4);
+        vm.stopPrank();
+    }
+
+    function testResetTokenRoyalty() public {
+        address owner = deployer;
+        address receiver1 = makeAddr("receiver1");
+        address receiver2 = makeAddr("receiver2");
+        uint256 tokenId1 = 1;
+        uint256 tokenId2 = 2;
+        uint96 royaltyFraction = 10;
+        uint256 salePrice = 1_000;
+        uint256 royalty1 = (salePrice * royaltyFraction) / 10_000;
+        uint256 royalty2 = (salePrice * royaltyFraction * 2) / 10_000;
+        vm.startPrank(owner);
+        ERC2981Extended.set_default_royalty(receiver1, royaltyFraction);
+        ERC2981Extended.set_token_royalty(
+            tokenId1,
+            receiver2,
+            royaltyFraction * 2
+        );
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertTrue(receiverAddr1 != receiverAddr2);
+        assertTrue(royaltyAmount1 != royaltyAmount2);
+        assertEq(receiver2, receiverAddr1);
+        assertEq(royalty2, royaltyAmount1);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount2);
+
+        ERC2981Extended.reset_token_royalty(tokenId1);
+        (address receiverAddr3, uint256 royaltyAmount3) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        assertEq(receiverAddr3, receiverAddr2);
+        assertEq(royaltyAmount3, royaltyAmount2);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount2);
+        assertEq(receiver1, receiverAddr3);
+        assertEq(royalty1, royaltyAmount3);
+        vm.stopPrank();
+    }
+
+    function testRoyaltyInfoRoyaltyAmountOverflow() public {
+        address owner = deployer;
+        vm.startPrank(owner);
+        ERC2981Extended.set_default_royalty(makeAddr("receiver"), 10);
+        vm.expectRevert();
+        ERC2981Extended.royaltyInfo(1, type(uint256).max);
+        vm.stopPrank();
+    }
+
+    function testSetDefaultRoyaltyTooHighFeeNumerator() public {
+        address owner = deployer;
+        vm.startPrank(owner);
+        vm.expectRevert("ERC2981: royalty fee will exceed sale_price");
+        ERC2981Extended.set_default_royalty(makeAddr("receiver"), 11_000);
+        vm.stopPrank();
+    }
+
+    function testSetDefaultRoyaltyInvalidReceiver() public {
+        address owner = deployer;
+        vm.startPrank(owner);
+        vm.expectRevert("ERC2981: invalid receiver");
+        ERC2981Extended.set_default_royalty(zeroAddress, 10);
+        vm.stopPrank();
+    }
+
+    function testSetDefaultRoyaltyNonOwner() public {
+        vm.expectRevert("Ownable: caller is not the owner");
+        ERC2981Extended.set_default_royalty(zeroAddress, 10);
+    }
+
+    function testDeleteDefaultRoyaltyNonOwner() public {
+        vm.expectRevert("Ownable: caller is not the owner");
+        ERC2981Extended.delete_default_royalty();
+    }
+
+    function testSetTokenRoyaltyTooHighFeeNumerator() public {
+        address owner = deployer;
+        vm.startPrank(owner);
+        vm.expectRevert("ERC2981: royalty fee will exceed sale_price");
+        ERC2981Extended.set_token_royalty(1, makeAddr("receiver"), 11_000);
+        vm.stopPrank();
+    }
+
+    function testSetTokenRoyaltyInvalidReceiver() public {
+        address owner = deployer;
+        vm.startPrank(owner);
+        vm.expectRevert("ERC2981: invalid receiver");
+        ERC2981Extended.set_token_royalty(1, zeroAddress, 10);
+        vm.stopPrank();
+    }
+
+    function testSetTokenRoyaltyNonOwner() public {
+        vm.expectRevert("Ownable: caller is not the owner");
+        ERC2981Extended.set_token_royalty(1, zeroAddress, 10);
+    }
+
+    function testResetTokenRoyaltyNonOwner() public {
+        vm.expectRevert("Ownable: caller is not the owner");
+        ERC2981Extended.reset_token_royalty(1);
+    }
+
+    function testHasOwner() public {
+        assertEq(ERC2981Extended.owner(), deployer);
+    }
+
+    function testTransferOwnershipSuccess() public {
+        address oldOwner = deployer;
+        address newOwner = makeAddr("newOwner");
+        vm.startPrank(oldOwner);
+        vm.expectEmit(true, true, false, false);
+        emit OwnershipTransferred(oldOwner, newOwner);
+        ERC2981Extended.transfer_ownership(newOwner);
+        assertEq(ERC2981Extended.owner(), newOwner);
+        vm.stopPrank();
+    }
+
+    function testTransferOwnershipNonOwner() public {
+        vm.expectRevert(bytes("Ownable: caller is not the owner"));
+        ERC2981Extended.transfer_ownership(makeAddr("newOwner"));
+    }
+
+    function testTransferOwnershipToZeroAddress() public {
+        vm.prank(deployer);
+        vm.expectRevert(bytes("Ownable: new owner is the zero address"));
+        ERC2981Extended.transfer_ownership(zeroAddress);
+    }
+
+    function testRenounceOwnershipSuccess() public {
+        address oldOwner = deployer;
+        address newOwner = zeroAddress;
+        vm.startPrank(oldOwner);
+        vm.expectEmit(true, true, false, false);
+        emit OwnershipTransferred(oldOwner, newOwner);
+        ERC2981Extended.renounce_ownership();
+        assertEq(ERC2981Extended.owner(), newOwner);
+        vm.stopPrank();
+    }
+
+    function testRenounceOwnershipNonOwner() public {
+        vm.expectRevert(bytes("Ownable: caller is not the owner"));
+        ERC2981Extended.renounce_ownership();
+    }
+
+    function testFuzzRoyaltyInfoDefaultRoyalty(
+        address receiver,
+        uint256 tokenId1,
+        uint256 tokenId2,
+        uint96 royaltyFraction,
+        uint256 salePrice
+    ) public {
+        vm.assume(receiver != zeroAddress && tokenId1 != tokenId2);
+        royaltyFraction = uint96(bound(royaltyFraction, 0, 5_000));
+        salePrice = bound(salePrice, 0, type(uint240).max);
+        uint256 royalty = (salePrice * royaltyFraction) / 10_000;
+        vm.startPrank(deployer);
+        ERC2981Extended.set_default_royalty(receiver, royaltyFraction);
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr1, receiverAddr2);
+        assertEq(royaltyAmount1, royaltyAmount2);
+        assertEq(receiver, receiverAddr1);
+        assertEq(royalty, royaltyAmount1);
+        assertEq(receiver, receiverAddr2);
+        assertEq(royalty, royaltyAmount2);
+        vm.stopPrank();
+    }
+
+    function testFuzzRoyaltyInfoUpdateDefaultRoyalty(
+        address receiver1,
+        address receiver2,
+        uint256 tokenId1,
+        uint256 tokenId2,
+        uint96 royaltyFraction,
+        uint256 salePrice
+    ) public {
+        vm.assume(
+            receiver1 != zeroAddress &&
+                receiver2 != zeroAddress &&
+                receiver1 != receiver2 &&
+                tokenId1 != tokenId2
+        );
+        royaltyFraction = uint96(bound(royaltyFraction, 0, 5_000));
+        salePrice = bound(salePrice, 0, type(uint240).max);
+        uint256 royalty1 = (salePrice * royaltyFraction) / 10_000;
+        uint256 royalty2 = (salePrice * royaltyFraction * 2) / 10_000;
+        vm.startPrank(deployer);
+        ERC2981Extended.set_default_royalty(receiver1, royaltyFraction);
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr1, receiverAddr2);
+        assertEq(royaltyAmount1, royaltyAmount2);
+        assertEq(receiver1, receiverAddr1);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount1);
+        assertEq(royalty1, royaltyAmount2);
+
+        ERC2981Extended.set_default_royalty(receiver2, royaltyFraction * 2);
+        (address receiverAddr3, uint256 royaltyAmount3) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr4, uint256 royaltyAmount4) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr3, receiverAddr4);
+        assertEq(royaltyAmount3, royaltyAmount4);
+        assertEq(receiver2, receiverAddr3);
+        assertEq(receiver2, receiverAddr4);
+        assertEq(royalty2, royaltyAmount3);
+        assertEq(royalty2, royaltyAmount4);
+        vm.stopPrank();
+    }
+
+    function testFuzzRoyaltyInfoDeleteDefaultRoyalty(
+        address receiver,
+        uint256 tokenId1,
+        uint256 tokenId2,
+        uint96 royaltyFraction,
+        uint256 salePrice
+    ) public {
+        vm.assume(receiver != zeroAddress && tokenId1 != tokenId2);
+        royaltyFraction = uint96(bound(royaltyFraction, 0, 5_000));
+        salePrice = bound(salePrice, 0, type(uint240).max);
+        uint256 royalty = (salePrice * royaltyFraction) / 10_000;
+        vm.startPrank(deployer);
+        ERC2981Extended.set_default_royalty(receiver, royaltyFraction);
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr1, receiverAddr2);
+        assertEq(royaltyAmount1, royaltyAmount2);
+        assertEq(receiver, receiverAddr1);
+        assertEq(royalty, royaltyAmount1);
+        assertEq(receiver, receiverAddr2);
+        assertEq(royalty, royaltyAmount2);
+
+        ERC2981Extended.delete_default_royalty();
+        (address receiverAddr3, uint256 royaltyAmount3) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr4, uint256 royaltyAmount4) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr3, receiverAddr4);
+        assertEq(royaltyAmount3, royaltyAmount4);
+        assertEq(receiverAddr3, zeroAddress);
+        assertEq(royaltyAmount3, 0);
+        assertEq(receiverAddr3, zeroAddress);
+        assertEq(royaltyAmount4, 0);
+        vm.stopPrank();
+    }
+
+    function testFuzzRoyaltyInfoSetTokenRoyalty(
+        address receiver1,
+        address receiver2,
+        uint256 tokenId1,
+        uint256 tokenId2,
+        uint96 royaltyFraction,
+        uint256 salePrice
+    ) public {
+        vm.assume(
+            receiver1 != zeroAddress &&
+                receiver2 != zeroAddress &&
+                receiver1 != receiver2 &&
+                tokenId1 != tokenId2
+        );
+        royaltyFraction = uint96(bound(royaltyFraction, 0, 5_000));
+        salePrice = bound(salePrice, 0, type(uint240).max);
+        uint256 royalty1 = (salePrice * royaltyFraction) / 10_000;
+        uint256 royalty2 = (salePrice * royaltyFraction * 2) / 10_000;
+        vm.startPrank(deployer);
+        ERC2981Extended.set_default_royalty(receiver1, royaltyFraction);
+        ERC2981Extended.set_token_royalty(
+            tokenId1,
+            receiver2,
+            royaltyFraction * 2
+        );
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertTrue(receiverAddr1 != receiverAddr2);
+        assertEq(receiver2, receiverAddr1);
+        assertEq(royalty2, royaltyAmount1);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount2);
+        vm.stopPrank();
+    }
+
+    function testFuzzRoyaltyInfoSetTokenRoyaltyUpdate(
+        address receiver1,
+        address receiver2,
+        uint256 tokenId1,
+        uint256 tokenId2,
+        uint96 royaltyFraction,
+        uint256 salePrice
+    ) public {
+        vm.assume(
+            receiver1 != zeroAddress &&
+                receiver2 != zeroAddress &&
+                receiver1 != receiver2 &&
+                tokenId1 != tokenId2
+        );
+        royaltyFraction = uint96(bound(royaltyFraction, 0, 5_000));
+        salePrice = bound(salePrice, 0, type(uint240).max);
+        uint256 royalty1 = (salePrice * royaltyFraction) / 10_000;
+        uint256 royalty2 = (salePrice * royaltyFraction * 2) / 10_000;
+        vm.startPrank(deployer);
+        ERC2981Extended.set_default_royalty(receiver1, royaltyFraction);
+        ERC2981Extended.set_token_royalty(
+            tokenId1,
+            receiver2,
+            royaltyFraction * 2
+        );
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertTrue(receiverAddr1 != receiverAddr2);
+        assertEq(receiver2, receiverAddr1);
+        assertEq(royalty2, royaltyAmount1);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount2);
+
+        ERC2981Extended.set_token_royalty(
+            tokenId2,
+            receiver2,
+            royaltyFraction * 2
+        );
+        (address receiverAddr3, uint256 royaltyAmount3) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr4, uint256 royaltyAmount4) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertEq(receiverAddr3, receiverAddr4);
+        assertEq(royaltyAmount3, royaltyAmount4);
+        assertEq(receiver2, receiverAddr3);
+        assertEq(royalty2, royaltyAmount3);
+        assertEq(receiver2, receiverAddr4);
+        assertEq(royalty2, royaltyAmount4);
+        vm.stopPrank();
+    }
+
+    function testFuzzResetTokenRoyalty(
+        address receiver1,
+        address receiver2,
+        uint256 tokenId1,
+        uint256 tokenId2,
+        uint96 royaltyFraction,
+        uint256 salePrice
+    ) public {
+        vm.assume(
+            receiver1 != zeroAddress &&
+                receiver2 != zeroAddress &&
+                receiver1 != receiver2 &&
+                tokenId1 != tokenId2
+        );
+        royaltyFraction = uint96(bound(royaltyFraction, 0, 5_000));
+        salePrice = bound(salePrice, 0, type(uint240).max);
+        uint256 royalty1 = (salePrice * royaltyFraction) / 10_000;
+        uint256 royalty2 = (salePrice * royaltyFraction * 2) / 10_000;
+        vm.startPrank(deployer);
+        ERC2981Extended.set_default_royalty(receiver1, royaltyFraction);
+        ERC2981Extended.set_token_royalty(
+            tokenId1,
+            receiver2,
+            royaltyFraction * 2
+        );
+        (address receiverAddr1, uint256 royaltyAmount1) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        (address receiverAddr2, uint256 royaltyAmount2) = ERC2981Extended
+            .royaltyInfo(tokenId2, salePrice);
+        assertTrue(receiverAddr1 != receiverAddr2);
+        assertEq(receiver2, receiverAddr1);
+        assertEq(royalty2, royaltyAmount1);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount2);
+
+        ERC2981Extended.reset_token_royalty(tokenId1);
+        (address receiverAddr3, uint256 royaltyAmount3) = ERC2981Extended
+            .royaltyInfo(tokenId1, salePrice);
+        assertEq(receiverAddr3, receiverAddr2);
+        assertEq(royaltyAmount3, royaltyAmount2);
+        assertEq(receiver1, receiverAddr2);
+        assertEq(royalty1, royaltyAmount2);
+        assertEq(receiver1, receiverAddr3);
+        assertEq(royalty1, royaltyAmount3);
+        vm.stopPrank();
+    }
+
+    function testFuzzSetDefaultRoyaltyTooHighFeeNumerator(
+        uint96 feeNumerator
+    ) public {
+        feeNumerator = uint96(
+            bound(uint256(feeNumerator), 10_001, type(uint96).max)
+        );
+        address owner = deployer;
+        vm.startPrank(owner);
+        vm.expectRevert("ERC2981: royalty fee will exceed sale_price");
+        ERC2981Extended.set_default_royalty(makeAddr("receiver"), feeNumerator);
+        vm.stopPrank();
+    }
+
+    function testFuzzSetDefaultRoyaltyNonOwner(address msgSender) public {
+        vm.assume(msgSender != deployer);
+        vm.expectRevert("Ownable: caller is not the owner");
+        ERC2981Extended.set_default_royalty(zeroAddress, 10);
+    }
+
+    function testFuzzDeleteDefaultRoyaltyNonOwner(address msgSender) public {
+        vm.assume(msgSender != deployer);
+        vm.expectRevert("Ownable: caller is not the owner");
+        ERC2981Extended.delete_default_royalty();
+    }
+
+    function testFuzzSetTokenRoyaltyTooHighFeeNumerator(
+        uint96 feeNumerator
+    ) public {
+        feeNumerator = uint96(
+            bound(uint256(feeNumerator), 10_001, type(uint96).max)
+        );
+        address owner = deployer;
+        vm.startPrank(owner);
+        vm.expectRevert("ERC2981: royalty fee will exceed sale_price");
+        ERC2981Extended.set_token_royalty(
+            1,
+            makeAddr("receiver"),
+            feeNumerator
+        );
+        vm.stopPrank();
+    }
+
+    function testFuzzSetTokenRoyaltyInvalidReceiver(address msgSender) public {
+        vm.assume(msgSender != deployer);
+        address owner = deployer;
+        vm.startPrank(owner);
+        vm.expectRevert("ERC2981: invalid receiver");
+        ERC2981Extended.set_token_royalty(1, zeroAddress, 10);
+        vm.stopPrank();
+    }
+
+    function testFuzzSetTokenRoyaltyNonOwner(address msgSender) public {
+        vm.assume(msgSender != deployer);
+        vm.expectRevert("Ownable: caller is not the owner");
+        ERC2981Extended.set_token_royalty(1, zeroAddress, 10);
+    }
+
+    function testFuzzResetTokenRoyaltyNonOwner(address msgSender) public {
+        vm.assume(msgSender != deployer);
+        vm.expectRevert("Ownable: caller is not the owner");
+        ERC2981Extended.reset_token_royalty(1);
+    }
+
+    function testFuzzTransferOwnershipSuccess(
+        address newOwner1,
+        address newOwner2
+    ) public {
+        vm.assume(newOwner1 != zeroAddress && newOwner2 != zeroAddress);
+        address oldOwner = deployer;
+        vm.startPrank(oldOwner);
+        vm.expectEmit(true, true, false, false);
+        emit OwnershipTransferred(oldOwner, newOwner1);
+        ERC2981Extended.transfer_ownership(newOwner1);
+        assertEq(ERC2981Extended.owner(), newOwner1);
+        vm.stopPrank();
+
+        vm.startPrank(newOwner1);
+        vm.expectEmit(true, true, false, false);
+        emit OwnershipTransferred(newOwner1, newOwner2);
+        ERC2981Extended.transfer_ownership(newOwner2);
+        assertEq(ERC2981Extended.owner(), newOwner2);
+        vm.stopPrank();
+    }
+
+    function testFuzzTransferOwnershipNonOwner(
+        address nonOwner,
+        address newOwner
+    ) public {
+        vm.assume(nonOwner != deployer);
+        vm.prank(nonOwner);
+        vm.expectRevert(bytes("Ownable: caller is not the owner"));
+        ERC2981Extended.transfer_ownership(newOwner);
+    }
+
+    function testFuzzRenounceOwnershipSuccess(address newOwner) public {
+        vm.assume(newOwner != zeroAddress);
+        address oldOwner = deployer;
+        address renounceAddress = zeroAddress;
+        vm.startPrank(oldOwner);
+        vm.expectEmit(true, true, false, false);
+        emit OwnershipTransferred(oldOwner, newOwner);
+        ERC2981Extended.transfer_ownership(newOwner);
+        assertEq(ERC2981Extended.owner(), newOwner);
+        vm.stopPrank();
+
+        vm.startPrank(newOwner);
+        vm.expectEmit(true, true, false, false);
+        emit OwnershipTransferred(newOwner, renounceAddress);
+        ERC2981Extended.renounce_ownership();
+        assertEq(ERC2981Extended.owner(), renounceAddress);
+        vm.stopPrank();
+    }
+
+    function testFuzzRenounceOwnershipNonOwner(address nonOwner) public {
+        vm.assume(nonOwner != deployer);
+        vm.prank(nonOwner);
+        vm.expectRevert(bytes("Ownable: caller is not the owner"));
+        ERC2981Extended.renounce_ownership();
+    }
+}
+
+contract ERC2981Invariants is Test {
+    VyperDeployer private vyperDeployer = new VyperDeployer();
+
+    // solhint-disable-next-line var-name-mixedcase
+    IERC2981Extended private ERC2981Extended;
+    ERC2981Handler private erc2981Handler;
+
+    address private deployer = address(vyperDeployer);
+
+    function setUp() public {
+        ERC2981Extended = IERC2981Extended(
+            vyperDeployer.deployContract("src/extensions/", "ERC2981")
+        );
+        erc2981Handler = new ERC2981Handler(ERC2981Extended, deployer);
+        targetContract(address(erc2981Handler));
+        targetSender(deployer);
+    }
+
+    function invariantOwner() public {
+        assertEq(ERC2981Extended.owner(), erc2981Handler.owner());
+    }
+}
+
+contract ERC2981Handler {
+    address public owner;
+
+    IERC2981Extended private erc2981;
+
+    address private zeroAddress = address(0);
+
+    constructor(IERC2981Extended erc2981_, address owner_) {
+        erc2981 = erc2981_;
+        owner = owner_;
+    }
+
+    function set_default_royalty(address receiver, uint96 feeNumerator) public {
+        erc2981.set_default_royalty(receiver, feeNumerator);
+    }
+
+    function delete_default_royalty() public {
+        erc2981.delete_default_royalty();
+    }
+
+    function set_token_royalty(
+        uint256 tokenId,
+        address receiver,
+        uint96 feeNumerator
+    ) public {
+        erc2981.set_token_royalty(tokenId, receiver, feeNumerator);
+    }
+
+    function reset_token_royalty(uint256 tokenId) public {
+        erc2981.reset_token_royalty(tokenId);
+    }
+
+    function transfer_ownership(address newOwner) public {
+        erc2981.transfer_ownership(newOwner);
+        owner = newOwner;
+    }
+
+    function renounce_ownership() public {
+        erc2981.renounce_ownership();
+        owner = zeroAddress;
+    }
+}
diff --git a/test/extensions/interfaces/IERC2981Extended.sol b/test/extensions/interfaces/IERC2981Extended.sol
new file mode 100644
index 00000000..0ae0fb31
--- /dev/null
+++ b/test/extensions/interfaces/IERC2981Extended.sol
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.20;
+
+import {IERC2981} from "openzeppelin/interfaces/IERC2981.sol";
+
+interface IERC2981Extended is IERC2981 {
+    function set_default_royalty(
+        address receiver,
+        uint96 feeNumerator
+    ) external;
+
+    function delete_default_royalty() external;
+
+    function set_token_royalty(
+        uint256 tokenId,
+        address receiver,
+        uint96 feeNumerator
+    ) external;
+
+    function reset_token_royalty(uint256 tokenId) external;
+
+    function owner() external view returns (address);
+
+    function transfer_ownership(address newOwner) external;
+
+    function renounce_ownership() external;
+}
diff --git a/test/utils/BatchDistributor.t.sol b/test/utils/BatchDistributor.t.sol
index d36a1fa8..a969deb3 100644
--- a/test/utils/BatchDistributor.t.sol
+++ b/test/utils/BatchDistributor.t.sol
@@ -383,7 +383,10 @@ contract BatchDistributorTest is Test {
         address initialAccount,
         uint256 initialAmount
     ) public {
-        vm.assume(initialAccount != zeroAddress);
+        vm.assume(
+            initialAccount != zeroAddress &&
+                initialAccount != batchDistributorAddr
+        );
         initialAmount = bound(
             initialAmount,
             type(uint16).max,