mirror of
https://github.com/lens-protocol/core.git
synced 2026-04-22 03:02:03 -04:00
feat: Add ERC2981 royalties and totalSupply to LensHandles T-16847 T-16890
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user