mirror of
https://github.com/lens-protocol/core.git
synced 2026-04-22 03:02:03 -04:00
Merge pull request #150 from lens-protocol/feat/collect-nft-royalties
This commit is contained in:
@@ -3,10 +3,12 @@
|
||||
pragma solidity 0.8.10;
|
||||
|
||||
import {ICollectNFT} from '../interfaces/ICollectNFT.sol';
|
||||
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
|
||||
import {ILensHub} from '../interfaces/ILensHub.sol';
|
||||
import {Errors} from '../libraries/Errors.sol';
|
||||
import {Events} from '../libraries/Events.sol';
|
||||
import {LensNFTBase} from './base/LensNFTBase.sol';
|
||||
import {ERC721Enumerable} from './base/ERC721Enumerable.sol';
|
||||
|
||||
/**
|
||||
* @title CollectNFT
|
||||
@@ -24,6 +26,12 @@ contract CollectNFT is LensNFTBase, ICollectNFT {
|
||||
|
||||
bool private _initialized;
|
||||
|
||||
uint256 internal _royaltyBasisPoints;
|
||||
|
||||
// bytes4(keccak256('royaltyInfo(uint256,uint256)')) == 0x2a55205a
|
||||
bytes4 internal constant INTERFACE_ID_ERC2981 = 0x2a55205a;
|
||||
uint16 internal constant BASIS_POINTS = 10000;
|
||||
|
||||
// We create the CollectNFT with the pre-computed HUB address before deploying the hub proxy in order
|
||||
// to initialize the hub proxy at construction.
|
||||
constructor(address hub) {
|
||||
@@ -41,6 +49,7 @@ contract CollectNFT is LensNFTBase, ICollectNFT {
|
||||
) external override {
|
||||
if (_initialized) revert Errors.Initialized();
|
||||
_initialized = true;
|
||||
_royaltyBasisPoints = 1000; // 10% of royalties
|
||||
_profileId = profileId;
|
||||
_pubId = pubId;
|
||||
super._initialize(name, symbol);
|
||||
@@ -67,6 +76,55 @@ contract CollectNFT is LensNFTBase, ICollectNFT {
|
||||
return ILensHub(HUB).getContentURI(_profileId, _pubId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Changes the royalty percentage for secondary sales. Can only be called publication's
|
||||
* profile owner.
|
||||
*
|
||||
* @param royaltyBasisPoints The royalty percentage meassured in basis points. Each basis point
|
||||
* represents 0.01%.
|
||||
*/
|
||||
function setRoyalty(uint256 royaltyBasisPoints) external {
|
||||
if (IERC721(HUB).ownerOf(_profileId) == msg.sender) {
|
||||
if (royaltyBasisPoints > BASIS_POINTS) {
|
||||
revert Errors.InvalidParameter();
|
||||
} else {
|
||||
_royaltyBasisPoints = royaltyBasisPoints;
|
||||
}
|
||||
} else {
|
||||
revert Errors.NotProfileOwner();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Called with the sale price to determine how much royalty
|
||||
* is owed and to whom.
|
||||
*
|
||||
* @param tokenId The token ID of the NFT queried for royalty information.
|
||||
* @param salePrice The sale price of the NFT specified.
|
||||
* @return A tuple with the address who should receive the royalties and the royalty
|
||||
* payment amount for the given sale price.
|
||||
*/
|
||||
function royaltyInfo(uint256 tokenId, uint256 salePrice)
|
||||
external
|
||||
view
|
||||
returns (address, uint256)
|
||||
{
|
||||
return (IERC721(HUB).ownerOf(_profileId), (salePrice * _royaltyBasisPoints) / BASIS_POINTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC165-supportsInterface}.
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId)
|
||||
public
|
||||
view
|
||||
virtual
|
||||
override(ERC721Enumerable)
|
||||
returns (bool)
|
||||
{
|
||||
return interfaceId == INTERFACE_ID_ERC2981 || super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Upon transfers, we emit the transfer event in the hub.
|
||||
*/
|
||||
|
||||
@@ -34,6 +34,7 @@ library Errors {
|
||||
error ArrayMismatch();
|
||||
error CannotCommentOnSelf();
|
||||
error NotWhitelisted();
|
||||
error InvalidParameter();
|
||||
|
||||
// Module Errors
|
||||
error InitParamsInvalid();
|
||||
|
||||
@@ -47,4 +47,5 @@ export const ERRORS = {
|
||||
PUBLISHING_PAUSED: 'PublishingPaused()',
|
||||
NO_REASON_ABI_DECODE:
|
||||
"Transaction reverted and Hardhat couldn't infer the reason. Please report this to help us improve Hardhat.",
|
||||
INVALID_PARAMETER: 'InvalidParameter()',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { BigNumber } from 'ethers';
|
||||
import { CollectNFT, CollectNFT__factory } from '../../typechain-types';
|
||||
import { ZERO_ADDRESS } from '../helpers/constants';
|
||||
import { ERRORS } from '../helpers/errors';
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
userAddress,
|
||||
userTwo,
|
||||
abiCoder,
|
||||
userTwoAddress,
|
||||
} from '../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('Collect NFT', function () {
|
||||
@@ -73,6 +75,19 @@ makeSuiteCleanRoom('Collect NFT', function () {
|
||||
it('User should fail to get the URI for a token that does not exist', async function () {
|
||||
await expect(collectNFT.tokenURI(2)).to.be.revertedWith(ERRORS.TOKEN_DOES_NOT_EXIST);
|
||||
});
|
||||
|
||||
it('User should fail to change the royalty percentage if he is not the owner of the publication', async function () {
|
||||
await expect(collectNFT.connect(userTwo).setRoyalty(100)).to.be.revertedWith(
|
||||
ERRORS.NOT_PROFILE_OWNER
|
||||
);
|
||||
});
|
||||
|
||||
it('User should fail to change the royalty percentage if the value passed exceeds the royalty basis points', async function () {
|
||||
const royaltyBasisPoints = 10000;
|
||||
const newRoyalty = royaltyBasisPoints + 1;
|
||||
expect(newRoyalty).to.be.greaterThan(royaltyBasisPoints);
|
||||
await expect(collectNFT.setRoyalty(newRoyalty)).to.be.revertedWith(ERRORS.INVALID_PARAMETER);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
@@ -89,5 +104,45 @@ makeSuiteCleanRoom('Collect NFT', function () {
|
||||
it('User should burn their collect NFT', async function () {
|
||||
await expect(collectNFT.burn(1)).to.not.be.reverted;
|
||||
});
|
||||
|
||||
it('Default royalties are set to 10%', async function () {
|
||||
const royaltyInfo = await collectNFT.royaltyInfo(1, 6900);
|
||||
expect(royaltyInfo[0]).to.eq(userAddress);
|
||||
expect(royaltyInfo[1]).to.eq(BigNumber.from(690));
|
||||
});
|
||||
|
||||
it('User should be able to change the royalties if owns the profile and passes a valid royalty percentage in basis points', async function () {
|
||||
await expect(collectNFT.setRoyalty(5000)).to.not.be.reverted;
|
||||
const royaltyInfo = await collectNFT.royaltyInfo(1, 3000);
|
||||
expect(royaltyInfo[0]).to.eq(userAddress);
|
||||
expect(royaltyInfo[1]).to.eq(BigNumber.from(1500));
|
||||
});
|
||||
|
||||
it('User should be able to get the royalty info even over a token that does not exist yet', async function () {
|
||||
const unexistentTokenId = 69;
|
||||
await expect(collectNFT.tokenURI(unexistentTokenId)).to.be.revertedWith(
|
||||
ERRORS.TOKEN_DOES_NOT_EXIST
|
||||
);
|
||||
const royaltyInfo = await collectNFT.royaltyInfo(unexistentTokenId, 3000);
|
||||
expect(royaltyInfo[0]).to.eq(userAddress);
|
||||
expect(royaltyInfo[1]).to.eq(BigNumber.from(300));
|
||||
});
|
||||
|
||||
it('Publication owner should be able to remove royalties by setting them as zero', async function () {
|
||||
await expect(collectNFT.setRoyalty(0)).to.not.be.reverted;
|
||||
const royaltyInfo = await collectNFT.royaltyInfo(1, 3000);
|
||||
expect(royaltyInfo[0]).to.eq(userAddress);
|
||||
expect(royaltyInfo[1]).to.eq(BigNumber.from(0));
|
||||
});
|
||||
|
||||
it('If the profile authoring the publication is transferred the royalty info now returns the new owner as recipient', async function () {
|
||||
let royaltyInfo = await collectNFT.royaltyInfo(1, 69);
|
||||
expect(royaltyInfo[0]).to.eq(userAddress);
|
||||
await expect(
|
||||
lensHub.transferFrom(userAddress, userTwoAddress, FIRST_PROFILE_ID)
|
||||
).to.not.be.reverted;
|
||||
royaltyInfo = await collectNFT.royaltyInfo(1, 69);
|
||||
expect(royaltyInfo[0]).to.eq(userTwoAddress);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user