mirror of
https://github.com/lens-protocol/core.git
synced 2026-01-10 14:48:15 -05:00
327 lines
12 KiB
Solidity
327 lines
12 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
|
|
pragma solidity ^0.8.18;
|
|
|
|
import {ERC721} from '@openzeppelin/contracts/token/ERC721/ERC721.sol';
|
|
import {ImmutableOwnable} from 'contracts/misc/ImmutableOwnable.sol';
|
|
import {ILensHandles} from 'contracts/interfaces/ILensHandles.sol';
|
|
import {HandlesEvents} from 'contracts/namespaces/constants/Events.sol';
|
|
import {HandlesErrors} from 'contracts/namespaces/constants/Errors.sol';
|
|
import {IHandleTokenURI} from 'contracts/interfaces/IHandleTokenURI.sol';
|
|
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 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.
|
|
*
|
|
* @custom:upgradeable Transparent upgradeable proxy without initializer.
|
|
*/
|
|
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.
|
|
// Can be extended later if needed.
|
|
uint256 internal constant MAX_LOCAL_NAME_LENGTH = 26;
|
|
string public constant NAMESPACE = 'lens';
|
|
uint256 internal immutable NAMESPACE_LENGTH = bytes(NAMESPACE).length;
|
|
bytes32 public constant NAMESPACE_HASH = keccak256(bytes(NAMESPACE));
|
|
uint256 public immutable TOKEN_GUARDIAN_COOLDOWN;
|
|
uint256 internal constant GUARDIAN_ENABLED = type(uint256).max;
|
|
mapping(address => uint256) internal _tokenGuardianDisablingTimestamp;
|
|
|
|
uint256 internal _profileRoyaltiesBps; // Slot 7
|
|
uint256 private _totalSupply;
|
|
|
|
mapping(uint256 tokenId => string localName) internal _localNames;
|
|
|
|
address internal _handleTokenURIContract;
|
|
|
|
modifier onlyOwnerOrWhitelistedProfileCreator() {
|
|
if (msg.sender != OWNER && !ILensHub(LENS_HUB).isProfileCreatorWhitelisted(msg.sender)) {
|
|
revert HandlesErrors.NotOwnerNorWhitelisted();
|
|
}
|
|
_;
|
|
}
|
|
|
|
modifier onlyEOA() {
|
|
if (msg.sender.isContract()) {
|
|
revert HandlesErrors.NotEOA();
|
|
}
|
|
_;
|
|
}
|
|
|
|
modifier onlyHub() {
|
|
if (msg.sender != LENS_HUB) {
|
|
revert HandlesErrors.NotHub();
|
|
}
|
|
_;
|
|
}
|
|
|
|
constructor(
|
|
address owner,
|
|
address lensHub,
|
|
uint256 tokenGuardianCooldown
|
|
) ERC721('', '') ImmutableOwnable(owner, lensHub) {
|
|
TOKEN_GUARDIAN_COOLDOWN = tokenGuardianCooldown;
|
|
}
|
|
|
|
function name() public pure override returns (string memory) {
|
|
return 'Lens Handles';
|
|
}
|
|
|
|
function symbol() public pure override returns (string memory) {
|
|
return 'LH';
|
|
}
|
|
|
|
function totalSupply() external view virtual override returns (uint256) {
|
|
return _totalSupply;
|
|
}
|
|
|
|
function setHandleTokenURIContract(address handleTokenURIContract) external override onlyOwner {
|
|
_handleTokenURIContract = handleTokenURIContract;
|
|
emit HandlesEvents.BatchMetadataUpdate({fromTokenId: 0, toTokenId: type(uint256).max});
|
|
}
|
|
|
|
function getHandleTokenURIContract() external view override returns (address) {
|
|
return _handleTokenURIContract;
|
|
}
|
|
|
|
/**
|
|
* @dev See {IERC721Metadata-tokenURI}.
|
|
*/
|
|
function tokenURI(uint256 tokenId) public view override returns (string memory) {
|
|
_requireMinted(tokenId);
|
|
return IHandleTokenURI(_handleTokenURIContract).getTokenURI(tokenId, _localNames[tokenId], NAMESPACE);
|
|
}
|
|
|
|
/// @inheritdoc ILensHandles
|
|
function mintHandle(
|
|
address to,
|
|
string calldata localName
|
|
) external onlyOwnerOrWhitelistedProfileCreator returns (uint256) {
|
|
_validateLocalName(localName);
|
|
return _mintHandle(to, localName);
|
|
}
|
|
|
|
function migrateHandle(address to, string calldata localName) external onlyHub returns (uint256) {
|
|
_validateLocalNameMigration(localName);
|
|
return _mintHandle(to, localName);
|
|
}
|
|
|
|
function burn(uint256 tokenId) external {
|
|
if (msg.sender != ownerOf(tokenId)) {
|
|
revert HandlesErrors.NotOwner();
|
|
}
|
|
--_totalSupply;
|
|
_burn(tokenId);
|
|
delete _localNames[tokenId];
|
|
}
|
|
|
|
/// ************************************
|
|
/// **** TOKEN GUARDIAN FUNCTIONS ****
|
|
/// ************************************
|
|
|
|
function DANGER__disableTokenGuardian() external override onlyEOA {
|
|
if (_tokenGuardianDisablingTimestamp[msg.sender] != GUARDIAN_ENABLED) {
|
|
revert HandlesErrors.DisablingAlreadyTriggered();
|
|
}
|
|
_tokenGuardianDisablingTimestamp[msg.sender] = block.timestamp + TOKEN_GUARDIAN_COOLDOWN;
|
|
emit HandlesEvents.TokenGuardianStateChanged({
|
|
wallet: msg.sender,
|
|
enabled: false,
|
|
tokenGuardianDisablingTimestamp: block.timestamp + TOKEN_GUARDIAN_COOLDOWN,
|
|
timestamp: block.timestamp
|
|
});
|
|
}
|
|
|
|
function enableTokenGuardian() external override onlyEOA {
|
|
if (_tokenGuardianDisablingTimestamp[msg.sender] == GUARDIAN_ENABLED) {
|
|
revert HandlesErrors.AlreadyEnabled();
|
|
}
|
|
_tokenGuardianDisablingTimestamp[msg.sender] = GUARDIAN_ENABLED;
|
|
emit HandlesEvents.TokenGuardianStateChanged({
|
|
wallet: msg.sender,
|
|
enabled: true,
|
|
tokenGuardianDisablingTimestamp: GUARDIAN_ENABLED,
|
|
timestamp: block.timestamp
|
|
});
|
|
}
|
|
|
|
function approve(address to, uint256 tokenId) public override(IERC721, ERC721) {
|
|
// We allow removing approvals even if the wallet has the token guardian enabled
|
|
if (to != address(0) && _hasTokenGuardianEnabled(ownerOf(tokenId))) {
|
|
revert HandlesErrors.GuardianEnabled();
|
|
}
|
|
super.approve(to, tokenId);
|
|
}
|
|
|
|
function setApprovalForAll(address operator, bool approved) public override(IERC721, ERC721) {
|
|
// We allow removing approvals even if the wallet has the token guardian enabled
|
|
if (approved && _hasTokenGuardianEnabled(msg.sender)) {
|
|
revert HandlesErrors.GuardianEnabled();
|
|
}
|
|
super.setApprovalForAll(operator, approved);
|
|
}
|
|
|
|
function exists(uint256 tokenId) external view override returns (bool) {
|
|
return _exists(tokenId);
|
|
}
|
|
|
|
function getNamespace() external pure returns (string memory) {
|
|
return NAMESPACE;
|
|
}
|
|
|
|
function getNamespaceHash() external pure returns (bytes32) {
|
|
return NAMESPACE_HASH;
|
|
}
|
|
|
|
function getLocalName(uint256 tokenId) public view returns (string memory) {
|
|
string memory localName = _localNames[tokenId];
|
|
if (bytes(localName).length == 0) {
|
|
revert HandlesErrors.DoesNotExist();
|
|
}
|
|
return _localNames[tokenId];
|
|
}
|
|
|
|
function getHandle(uint256 tokenId) public view returns (string memory) {
|
|
string memory localName = getLocalName(tokenId);
|
|
return string.concat(NAMESPACE, '/@', localName);
|
|
}
|
|
|
|
function getTokenId(string memory localName) public pure returns (uint256) {
|
|
return uint256(keccak256(bytes(localName)));
|
|
}
|
|
|
|
function getTokenGuardianDisablingTimestamp(address wallet) external view override returns (uint256) {
|
|
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);
|
|
return tokenId;
|
|
}
|
|
|
|
/// @dev This function is used to validate the local name when migrating from V1 to V2.
|
|
/// As in V1 we also allowed the Hyphen '-' character, we need to allow it here as well and use a separate
|
|
/// validation function for migration VS newly created handles.
|
|
function _validateLocalNameMigration(string memory localName) internal pure {
|
|
bytes memory localNameAsBytes = bytes(localName);
|
|
uint256 localNameLength = localNameAsBytes.length;
|
|
|
|
if (localNameLength == 0 || localNameLength > MAX_LOCAL_NAME_LENGTH) {
|
|
revert HandlesErrors.HandleLengthInvalid();
|
|
}
|
|
|
|
bytes1 firstByte = localNameAsBytes[0];
|
|
if (firstByte == '-' || firstByte == '_') {
|
|
revert HandlesErrors.HandleFirstCharInvalid();
|
|
}
|
|
|
|
uint256 i;
|
|
while (i < localNameLength) {
|
|
if (!_isAlphaNumeric(localNameAsBytes[i]) && localNameAsBytes[i] != '-' && localNameAsBytes[i] != '_') {
|
|
revert HandlesErrors.HandleContainsInvalidCharacters();
|
|
}
|
|
unchecked {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// @dev In V2 we only accept the following characters: [a-z0-9_] to be used in newly created handles.
|
|
/// We also disallow the first character to be an underscore '_'.
|
|
function _validateLocalName(string memory localName) internal pure {
|
|
bytes memory localNameAsBytes = bytes(localName);
|
|
uint256 localNameLength = localNameAsBytes.length;
|
|
|
|
if (localNameLength == 0 || localNameLength > MAX_LOCAL_NAME_LENGTH) {
|
|
revert HandlesErrors.HandleLengthInvalid();
|
|
}
|
|
|
|
if (localNameAsBytes[0] == '_') {
|
|
revert HandlesErrors.HandleFirstCharInvalid();
|
|
}
|
|
|
|
uint256 i;
|
|
while (i < localNameLength) {
|
|
if (!_isAlphaNumeric(localNameAsBytes[i]) && localNameAsBytes[i] != '_') {
|
|
revert HandlesErrors.HandleContainsInvalidCharacters();
|
|
}
|
|
unchecked {
|
|
++i;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// @dev We only accept lowercase characters to avoid confusion.
|
|
/// @param char The character to check.
|
|
/// @return True if the character is alphanumeric, false otherwise.
|
|
function _isAlphaNumeric(bytes1 char) internal pure returns (bool) {
|
|
return (char >= '0' && char <= '9') || (char >= 'a' && char <= 'z');
|
|
}
|
|
|
|
function _hasTokenGuardianEnabled(address wallet) internal view returns (bool) {
|
|
return
|
|
!wallet.isContract() &&
|
|
(_tokenGuardianDisablingTimestamp[wallet] == GUARDIAN_ENABLED ||
|
|
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) {
|
|
revert OnlyOwner();
|
|
}
|
|
}
|
|
|
|
function _beforeTokenTransfer(
|
|
address from,
|
|
address to,
|
|
uint256 /* firstTokenId */,
|
|
uint256 batchSize
|
|
) internal override {
|
|
if (from != address(0) && _hasTokenGuardianEnabled(from)) {
|
|
// Cannot transfer handle if the guardian is enabled, except at minting time.
|
|
revert HandlesErrors.GuardianEnabled();
|
|
}
|
|
|
|
super._beforeTokenTransfer(from, to, 0, batchSize);
|
|
}
|
|
}
|