feat: Add ERC2981 royalties and totalSupply to LensHandles T-16847 T-16890

This commit is contained in:
vicnaum
2023-10-12 09:00:46 +02:00
parent aefe48f6f0
commit e36c4f2b48
3 changed files with 124 additions and 3 deletions

View File

@@ -55,4 +55,11 @@ interface ILensHandles is IERC721 {
* @return bool Whether the token exists.
*/
function exists(uint256 tokenId) external view returns (bool);
/**
* @notice Returns the amount of tokens in circulation.
*
* @return uint256 The current total supply of tokens.
*/
function totalSupply() external view returns (uint256);
}

View File

@@ -11,18 +11,20 @@ import {HandleTokenURILib} from 'contracts/libraries/token-uris/HandleTokenURILi
import {ILensHub} from 'contracts/interfaces/ILensHub.sol';
import {Address} from '@openzeppelin/contracts/utils/Address.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import {ERC2981CollectionRoyalties} from 'contracts/base/ERC2981CollectionRoyalties.sol';
import {IERC165} from '@openzeppelin/contracts/utils/introspection/IERC165.sol';
/**
* A handle is defined as a local name inside a namespace context. A handle is represented as the local name with its
* namespace applied as a prefix, using the slash symbol as separator.
*
* handle = /${namespace}/${localName}
* handle = namespace /@ localName
*
* Handle and local name can be used interchangeably once you are in a context of a namespace, as it became redundant.
*
* handle === ${localName} ; inside some namespace.
*/
contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
contract LensHandles is ERC721, ERC2981CollectionRoyalties, ImmutableOwnable, ILensHandles {
using Address for address;
// We used 31 to fit the handle in a single slot, with `.lens` that restricted localName to use 26 characters.
@@ -34,6 +36,9 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
uint256 public immutable TOKEN_GUARDIAN_COOLDOWN;
mapping(address => uint256) internal _tokenGuardianDisablingTimestamp;
uint256 internal _profileRoyaltiesBps; // Slot 7
uint256 private _totalSupply;
mapping(uint256 tokenId => string localName) internal _localNames;
modifier onlyOwnerOrWhitelistedProfileCreator() {
@@ -73,6 +78,10 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
return string.concat(NAMESPACE);
}
function totalSupply() external view virtual override returns (uint256) {
return _totalSupply;
}
/**
* @dev See {IERC721Metadata-tokenURI}.
*/
@@ -99,6 +108,7 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
if (msg.sender != ownerOf(tokenId)) {
revert HandlesErrors.NotOwner();
}
--_totalSupply;
_burn(tokenId);
delete _localNames[tokenId];
}
@@ -149,7 +159,7 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
super.setApprovalForAll(operator, approved);
}
function exists(uint256 tokenId) external view returns (bool) {
function exists(uint256 tokenId) external view override returns (bool) {
return _exists(tokenId);
}
@@ -182,12 +192,22 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
return _tokenGuardianDisablingTimestamp[wallet];
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(
bytes4 interfaceId
) public view virtual override(ERC721, ERC2981CollectionRoyalties, IERC165) returns (bool) {
return (ERC721.supportsInterface(interfaceId) || ERC2981CollectionRoyalties.supportsInterface(interfaceId));
}
//////////////////////////////////////
/// INTERNAL FUNCTIONS ///
//////////////////////////////////////
function _mintHandle(address to, string calldata localName) internal returns (uint256) {
uint256 tokenId = getTokenId(localName);
++_totalSupply;
_mint(to, tokenId);
_localNames[tokenId] = localName;
emit HandlesEvents.HandleMinted(localName, NAMESPACE, tokenId, to, block.timestamp);
@@ -260,6 +280,23 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
block.timestamp < _tokenGuardianDisablingTimestamp[wallet]);
}
function _getRoyaltiesInBasisPointsSlot() internal pure override returns (uint256 slot) {
assembly {
slot := _profileRoyaltiesBps.slot
}
}
function _getReceiver(uint256 /* tokenId */) internal view override returns (address) {
return ILensHub(LENS_HUB).getTreasury();
}
function _beforeRoyaltiesSet(uint256 /* royaltiesInBasisPoints */) internal view override {
if (msg.sender != OWNER) {
// TODO: test this - and decide if we want a separate/shared governance here instead
revert OnlyOwner();
}
}
function _beforeTokenTransfer(
address from,
address to,

View File

@@ -14,6 +14,8 @@ contract LensHandlesTest is TokenGuardianTest {
uint256 constant MAX_LOCAL_NAME_LENGTH = 26;
uint256 uniqueHandleCounter;
error OnlyOwner();
function _TOKEN_GUARDIAN_COOLDOWN() internal view override returns (uint256) {
return fork ? lensHandles.TOKEN_GUARDIAN_COOLDOWN() : HANDLE_GUARDIAN_COOLDOWN;
}
@@ -363,4 +365,79 @@ contract LensHandlesTest is TokenGuardianTest {
}
return str;
}
//////////////////////////////////////////////////////////
// ERC-2981 Royalties - Scenarios
//////////////////////////////////////////////////////////
function testSupportsErc2981Interface() public {
assertTrue(lensHandles.supportsInterface(bytes4(keccak256('royaltyInfo(uint256,uint256)'))));
}
function testDefaultRoyaltiesAreSetToZero(uint256 tokenId, uint256 salePrice) public {
(address receiver, uint256 royalties) = lensHandles.royaltyInfo(tokenId, salePrice);
assertEq(receiver, treasury);
assertEq(royalties, 0);
}
function testSetRoyalties(uint256 royaltiesInBasisPoints, uint256 tokenId, uint256 salePrice) public {
uint256 basisPoints = 10000;
royaltiesInBasisPoints = bound(royaltiesInBasisPoints, 0, basisPoints);
uint256 salePriceTimesRoyalties;
unchecked {
salePriceTimesRoyalties = salePrice * royaltiesInBasisPoints;
// Fuzz prices that does not generate overflow, otherwise royaltyInfo will revert
vm.assume(salePrice == 0 || salePriceTimesRoyalties / salePrice == royaltiesInBasisPoints);
}
vm.prank(lensHandles.OWNER());
lensHandles.setRoyalty(royaltiesInBasisPoints);
(address receiver, uint256 royalties) = lensHandles.royaltyInfo(tokenId, salePrice);
assertEq(receiver, treasury);
assertEq(royalties, salePriceTimesRoyalties / basisPoints);
}
function testTotalSupply(address to) public {
vm.assume(to != address(0));
uint256 currentTotalSupply = lensHandles.totalSupply();
uint256 tokenId = _mintERC721(to);
uint256 totalSupplyAfterMint = lensHandles.totalSupply();
assertEq(totalSupplyAfterMint, currentTotalSupply + 1);
_effectivelyDisableGuardian(address(lensHandles), to);
vm.prank(to);
lensHandles.burn(tokenId);
uint256 totalSupplyAfterBurn = lensHandles.totalSupply();
assertEq(totalSupplyAfterBurn, totalSupplyAfterMint - 1);
}
//////////////////////////////////////////////////////////
// ERC-2981 Royalties - Negatives
//////////////////////////////////////////////////////////
function testCannotSetRoyaltiesIf_NotOwner(address nonOwnerAddress, uint256 royaltiesInBasisPoints) public {
uint256 basisPoints = 10000;
royaltiesInBasisPoints = bound(royaltiesInBasisPoints, 0, basisPoints);
vm.assume(nonOwnerAddress != lensHandles.OWNER());
vm.assume(!_isLensHubProxyAdmin(nonOwnerAddress));
vm.prank(nonOwnerAddress);
vm.expectRevert(OnlyOwner.selector);
lensHandles.setRoyalty(royaltiesInBasisPoints);
}
function testCannotSetRoyaltiesIf_ExceedsBasisPoints(uint256 royaltiesInBasisPoints) public {
uint256 basisPoints = 10000;
vm.assume(royaltiesInBasisPoints > basisPoints);
vm.prank(governance);
vm.expectRevert(Errors.InvalidParameter.selector);
lensHandles.setRoyalty(royaltiesInBasisPoints);
}
}