Skip to content

Commit

Permalink
Merge pull request lens-protocol#150 from lens-protocol/feat/collect-…
Browse files Browse the repository at this point in the history
…nft-royalties
  • Loading branch information
Zer0dot authored Oct 10, 2022
2 parents 746bda9 + 21a0eb4 commit b678530
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 0 deletions.
58 changes: 58 additions & 0 deletions contracts/core/CollectNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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.
*/
Expand Down
1 change: 1 addition & 0 deletions contracts/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ library Errors {
error ArrayMismatch();
error CannotCommentOnSelf();
error NotWhitelisted();
error InvalidParameter();

// Module Errors
error InitParamsInvalid();
Expand Down
1 change: 1 addition & 0 deletions test/helpers/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()',
};
55 changes: 55 additions & 0 deletions test/nft/collect-nft.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,6 +18,7 @@ import {
userAddress,
userTwo,
abiCoder,
userTwoAddress,
} from '../__setup.spec';

makeSuiteCleanRoom('Collect NFT', function () {
Expand Down Expand Up @@ -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 () {
Expand All @@ -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);
});
});
});

0 comments on commit b678530

Please sign in to comment.