misc: Merge conflicts

This commit is contained in:
donosonaumczuk
2023-07-13 13:39:45 +01:00
33 changed files with 528 additions and 398 deletions

2
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "lib/seadrop"]
path = lib/seadrop
url = https://github.com/donosonaumczuk/seadrop
url = https://github.com/lens-protocol/seadrop.git
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std

View File

@@ -3,4 +3,6 @@ cache
node_modules
coverage
typechain-types
lib
out
contracts/libraries/constants/Typehash.sol

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Avara Labs Ltd
Copyright (c) 2023 Avara Labs Ltd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

167
README.md
View File

@@ -37,165 +37,42 @@
The Lens Protocol is a decentralized, non-custodial social graph. Lens implements unique, on-chain social interaction mechanisms analogous to commonly understood Web2 social media interactions, but significantly expanded with unique functionality that empower communities to form and participants to own their own social graph.
## Cloning
You must clone the repo using SSH:
```bash
git clone git@github.com:lens-protocol/core-private.git
```
## Setup
> For now only Linux and macOS are known to work
>
> We are now figuring out what works for Windows, instructions will be updated soon
>
> (feel free to experiment and submit PR's)
1. Install Foundry by following the instructions from [their repository](https://book.getfoundry.sh/getting-started/installation).
- curl -L https://foundry.paradigm.xyz | bash
- foundryup
- done
2. Install the dependencies by running : `npm i && forge install`
The environment is built using Docker Compose, note that your `.env` file must have the RPC URL of the network you want to use, and an optional `MNEMONIC` and `BLOCK_EXPLORER_KEY`, defined like so, assuming you choose to use Mumbai network:
```
MNEMONIC="MNEMONIC YOU WANT TO DERIVE WALLETS FROM HERE"
MUMBAI_RPC_URL="YOUR RPC URL HERE"
BLOCK_EXPLORER_KEY="YOUR BLOCK EXPLORER API KEY HERE"
```
With the environment file set up, you can move on to using Docker:
You can now build it using:
```bash
export USERID=$UID && docker-compose build && docker-compose run --name lens contracts-env bash
npm run build
```
If you need additional terminals:
## Testing
```bash
docker exec -it lens bash
```
From there, have fun!
Here are a few self-explanatory scripts:
You can run unit tests using
```bash
npm run test
npm run coverage
npm run compile
```
Cleanup leftover Docker containers:
## Coverage
You can run coverage using
```bash
USERID=$UID docker-compose down
npm run coverage
```
## Protocol Overview
The Lens Protocol transfers ownership of social graphs to the participants of that graph themselves. This is achieved by creating direct links between `profiles` and their `followers`, while allowing fine-grained control of additional logic, including monetization, to be executed during those interactions on a profile-by-profile basis.
Here's how it works...
### Profiles
Any address can create a profile and receive an ERC-721 `Lens Profile` NFT. Profiles are represented by a `ProfileStruct`:
```
/**
* @notice A struct containing profile data.
*
* @param pubCount The number of publications made to this profile.
* @param followNFT The address of the followNFT associated with this profile, can be empty..
* @param followModule The address of the current follow module in use by this profile, can be empty.
* @param handle The profile's associated handle.
* @param uri The URI to be displayed for the profile NFT.
*/
struct ProfileStruct {
uint256 pubCount;
address followNFT;
address followModule;
string handle;
string uri;
}
```
Profiles have a specific URI associated with them, which is meant to include metadata, such as a link to a profile picture or a display name for instance, the JSON standard for this URI is not yet determined. Profile owners can always change their follow module or profile URI.
#### Publications
Profile owners can `publish` to any profile they own. There are three `publication` types: `Post`, `Comment` and `Mirror`. Profile owners can also set and initialize the `Follow Module` associated with their profile.
Publications are on-chain content created and published via profiles. Profile owners can create (publish) three publication types, outlined below. They are represented by a `PublicationStruct`:
```
/**
* @notice A struct containing data associated with each new publication.
*
* @param pointedProfileId The profile token ID this publication points to, for mirrors and comments.
* @param pointedPubId The publication ID this publication points to, for mirrors and comments.
* @param contentURI The URI associated with this publication.
* @param referenceModule The address of the current reference module in use by this profile, can be empty.
* @param collectModule The address of the collect module associated with this publication, this exists for all publication.
* @param collectNFT The address of the collectNFT associated with this publication, if any.
*/
struct PublicationStruct {
uint256 pointedProfileId;
uint256 pointedPubId;
string contentURI;
address referenceModule;
address collectModule;
address collectNFT;
}
```
#### Publication Types
##### Post
This is the standard publication type, akin to a regular post on traditional social media platforms. Posts contain:
1. A URI, pointing to the actual publication body's metadata JSON, including any images or text.
2. An uninitialized pointer, since pointers are only needed in mirrors and comments.
##### Comment
This is a publication type that points back to another publication, whether it be a post, comment or mirror, akin to a regular comment on traditional social media. Comments contain:
1. A URI, just like posts, pointing to the publication body's metadata JSON.
2. An initialized pointer, containing the profile ID and the publication ID of the publication commented on.
##### Mirror
This is a publication type that points to another publication, note that mirrors cannot, themselves, be mirrored (doing so instead mirrors the pointed content). Mirrors have no original content of its own. Akin to a "share" on traditional social media. Mirrors contain:
1. An empty URI, since they cannot have content associated with them.
2. An initialized pointer, containing the profile ID and the publication ID of the mirrored publication.
### Profile Interaction
There are two types of profile interactions: follows and collects.
#### Follows
Wallets can follow profiles, executing modular follow processing logic (in that profile's selected follow module) and receiving a `Follow NFT`. Each profile has a connected, unique `FollowNFT` contract, which is first deployed upon successful follow. Follow NFTs are NFTs with integrated voting and delegation capability.
The inclusion of voting and delegation right off the bat means that follow NFTs have the built-in capability to create a spontaneous DAO around any profile. Furthermore, holding follow NFTs allows followers to `collect` publications from the profile they are following (except mirrors, which are equivalent to shares in Web2 social media, and require following the original publishing profile to collect).
#### Collects
Collecting works in a modular fashion as well, every publication (except mirrors) requires a `Collect Module` to be selected and initialized. This module, similarly to follow modules, can contain any arbitrary logic to be executed upon collects. Successful collects result in a new, unique NFT being minted, essentially as a saved copy of the original publication. There is one deployed collect NFT contract per publication, and it's deployed upon the first successful collect.
When a mirror is collected, what happens behind the scenes is the original, mirrored publication is collected, and the mirror publisher's profile ID is passed as a "referrer." This allows neat functionality where collect modules that incur a fee can, for instance, reward referrals. Note that the `Collected` event, which is emitted upon collection, indexes the profile and publication directly being passed, which, in case of a mirror, is different than the actual original publication getting collected (which is emitted unindexed).
Alright, that was a mouthful! Let's move on to more specific details about Lens's core principle: Modularity.
## Lens Modularity
Stepping back for a moment, the core concept behind modules is to allow as much freedom as possible to the community to come up with new, innovative interaction mechanisms between social graph participants. For security purposes, this is achieved by including a whitelisted list of modules controlled by governance.
To recap, the Lens Protocol has three types of modules:
1. `Follow Modules` contain custom logic to be executed upon follow.
2. `Collect Modules` contain custom logic to be executed upon collect. Typically, these modules include at least a check that the collector is a follower.
3. `Reference Modules` contain custom logic to be executed upon comment and mirror. These modules can be used to limit who is able to comment and interact with a profile.
Note that collect and reference modules should _not_ assume that a publication cannot be re-initialized, and thus should include front-running protection as a security measure if needed, as if the publication data was not static. This is even more prominent in follow modules, where it can absolutely be changed for a given profile.
Lastly, there is also a `ModuleGlobals` contract which acts as a central data provider for modules. It is controlled by a specific governance address which can be set to a different executor compared to the Hub's governance. It's expected that modules will fetch dynamically changing data, such as the module globals governance address, the treasury address, the treasury fee as well as a list of whitelisted currencies.
### Upgradeability
This iteration of the Lens Protocol implements a transparent upgradeable proxy for the central hub to be controlled by governance. There are no other aspects of the protocol that are upgradeable. In an ideal world, the hub will not require upgrades due to the system's inherent modularity and openness, upgradeability is there only to implement new, breaking changes that would be impossible, or unreasonable to implement otherwise.
This does come with a few caveats, for instance, the `ModuleGlobals` contract implements a currency whitelist, but it is not upgradeable, so the "removal" of a currency whitelist in a module would require a specific new module that does not query the `ModuleGlobals` contract for whitelisted currencies.
You can go to our [docs](https://docs.lens.xyz/docs) to learn more about Lens Protocol.

Binary file not shown.

View File

@@ -152,27 +152,36 @@ contract FollowNFT is HubRestricted, LensBaseERC721, ERC2981CollectionRoyalties,
_approveFollow(followerProfileId, followTokenId);
}
/// @inheritdoc IFollowNFT
function wrap(uint256 followTokenId, address wrappedTokenReceiver) external override {
if (wrappedTokenReceiver == address(0)) {
revert Errors.InvalidParameter();
}
_wrap(followTokenId, wrappedTokenReceiver);
}
/// @inheritdoc IFollowNFT
function wrap(uint256 followTokenId) external override {
_wrap(followTokenId, address(0));
}
function _wrap(uint256 followTokenId, address wrappedTokenReceiver) internal {
if (_isFollowTokenWrapped(followTokenId)) {
revert AlreadyWrapped();
}
uint256 followerProfileId = _followDataByFollowTokenId[followTokenId].followerProfileId;
address wrappedTokenReceiver;
if (followerProfileId == 0) {
uint256 profileIdAllowedToRecover = _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
if (profileIdAllowedToRecover == 0) {
followerProfileId = _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
if (followerProfileId == 0) {
revert FollowTokenDoesNotExist();
}
wrappedTokenReceiver = IERC721(HUB).ownerOf(profileIdAllowedToRecover);
delete _followDataByFollowTokenId[followTokenId].profileIdAllowedToRecover;
} else {
wrappedTokenReceiver = IERC721(HUB).ownerOf(followerProfileId);
}
if (msg.sender != wrappedTokenReceiver) {
address followerProfileOwner = IERC721(HUB).ownerOf(followerProfileId);
if (msg.sender != followerProfileOwner) {
revert DoesNotHavePermissions();
}
_mint(wrappedTokenReceiver, followTokenId);
_mint(wrappedTokenReceiver == address(0) ? followerProfileOwner : wrappedTokenReceiver, followTokenId);
}
/// @inheritdoc IFollowNFT
@@ -251,9 +260,13 @@ contract FollowNFT is HubRestricted, LensBaseERC721, ERC2981CollectionRoyalties,
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(
bytes4 interfaceId
) public view virtual override(LensBaseERC721, ERC2981CollectionRoyalties) returns (bool) {
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(LensBaseERC721, ERC2981CollectionRoyalties)
returns (bool)
{
return
LensBaseERC721.supportsInterface(interfaceId) || ERC2981CollectionRoyalties.supportsInterface(interfaceId);
}
@@ -370,7 +383,11 @@ contract FollowNFT is HubRestricted, LensBaseERC721, ERC2981CollectionRoyalties,
_baseFollow({followerProfileId: newFollowerProfileId, followTokenId: followTokenId, isOriginalFollow: false});
}
function _baseFollow(uint256 followerProfileId, uint256 followTokenId, bool isOriginalFollow) internal {
function _baseFollow(
uint256 followerProfileId,
uint256 followTokenId,
bool isOriginalFollow
) internal {
_followTokenIdByFollowerProfileId[followerProfileId] = followTokenId;
_followDataByFollowTokenId[followTokenId].followerProfileId = uint160(followerProfileId);
_followDataByFollowTokenId[followTokenId].followTimestamp = uint48(block.timestamp);
@@ -416,7 +433,11 @@ contract FollowNFT is HubRestricted, LensBaseERC721, ERC2981CollectionRoyalties,
/**
* @dev Upon transfers, we clear follow approvals and emit the transfer event in the hub.
*/
function _beforeTokenTransfer(address from, address to, uint256 followTokenId) internal override {
function _beforeTokenTransfer(
address from,
address to,
uint256 followTokenId
) internal override {
if (from != address(0)) {
// It is cleared on unwrappings and transfers, and it can not be set on unwrapped tokens.
// As a consequence, there is no need to clear it on wrappings.
@@ -425,11 +446,15 @@ contract FollowNFT is HubRestricted, LensBaseERC721, ERC2981CollectionRoyalties,
super._beforeTokenTransfer(from, to, followTokenId);
}
function _getReceiver(uint256 /* followTokenId */) internal view override returns (address) {
function _getReceiver(
uint256 /* followTokenId */
) internal view override returns (address) {
return IERC721(HUB).ownerOf(_followedProfileId);
}
function _beforeRoyaltiesSet(uint256 /* royaltiesInBasisPoints */) internal view override {
function _beforeRoyaltiesSet(
uint256 /* royaltiesInBasisPoints */
) internal view override {
if (IERC721(HUB).ownerOf(_followedProfileId) != msg.sender) {
revert Errors.NotProfileOwner();
}

View File

@@ -17,10 +17,8 @@ interface ICollectNFT {
*
* @param profileId The token ID of the profile in the hub that this Collect NFT points to.
* @param pubId The profile publication ID in the hub that this Collect NFT points to.
* @param name The name to set for this NFT.
* @param symbol The symbol to set for this NFT.
*/
function initialize(uint256 profileId, uint256 pubId, string calldata name, string calldata symbol) external;
function initialize(uint256 profileId, uint256 pubId) external;
/**
* @notice Mints a collect NFT to the specified address. This can only be called by the hub and is called

View File

@@ -93,6 +93,18 @@ interface IFollowNFT {
*/
function wrap(uint256 followTokenId) external;
/**
* @notice Unties the follow token from the follower's profile one, and wraps it into the ERC-721 untied follow
* tokens collection. Untied follow tokens will NOT be automatically transferred with their follower profile.
* @custom:permissions Follower profile owner.
*
* @dev Only on unwrapped follow tokens.
*
* @param followTokenId The ID of the follow token to untie and wrap.
* @param wrappedTokenReceiver The address where the follow token is minted to when being wrapped as ERC-721.
*/
function wrap(uint256 followTokenId, address wrappedTokenReceiver) external;
/**
* @notice Unwraps the follow token from the ERC-721 untied follow tokens collection, and ties it to the follower's
* profile token. Tokens that are tied to the follower profile will be automatically transferred with it.

View File

@@ -10,7 +10,7 @@ import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
*
* @notice This is the interface for the LensHandles contract that is responsible for minting and burning handle NFTs.
* A handle is composed of a local name and a namespace, separated by a dot.
* Example: `john.lens` is a handle composed of the local name `john` and the namespace `lens`.
* Example: `satoshi.lens` is a handle composed of the local name `satoshi` and the namespace `lens`.
*/
interface ILensHandles is IERC721 {
/**

View File

@@ -22,9 +22,6 @@ import {PublicationLib} from 'contracts/libraries/PublicationLib.sol';
library LegacyCollectLib {
using Strings for uint256;
string constant COLLECT_NFT_NAME_INFIX = '-Collect-';
string constant COLLECT_NFT_SYMBOL_INFIX = '-Cl-';
/**
* @dev Emitted upon a successful legacy collect action.
*
@@ -143,14 +140,7 @@ library LegacyCollectLib {
function _deployCollectNFT(uint256 profileId, uint256 pubId, address collectNFTImpl) private returns (address) {
address collectNFT = Clones.clone(collectNFTImpl);
string memory collectNFTName = string(
abi.encodePacked(profileId.toString(), COLLECT_NFT_NAME_INFIX, pubId.toString())
);
string memory collectNFTSymbol = string(
abi.encodePacked(profileId.toString(), COLLECT_NFT_SYMBOL_INFIX, pubId.toString())
);
ICollectNFT(collectNFT).initialize(profileId, pubId, collectNFTName, collectNFTSymbol);
ICollectNFT(collectNFT).initialize(profileId, pubId);
emit Events.CollectNFTDeployed(profileId, pubId, collectNFT, block.timestamp);
return collectNFT;

View File

@@ -80,7 +80,7 @@ library MigrationLib {
}
}
// We mint a new handle on the LensHandles contract. The resulting handle NFT is sent to the profile owner.
uint256 handleId = lensHandles.mintHandle(profileOwner, handle);
uint256 handleId = lensHandles.migrateHandle(profileOwner, handle);
// We link it to the profile in the TokenHandleRegistry contract.
tokenHandleRegistry.migrationLink(handleId, profileId);
emit ProfileMigrated(profileId, profileOwner, handle, handleId);

View File

@@ -350,23 +350,6 @@ library Events {
*/
event Unblocked(uint256 indexed byProfileId, uint256 idOfProfileUnblocked, uint256 timestamp);
/**
* @dev Emitted via callback when a followNFT is transferred.
*
* @param profileId The token ID of the profile associated with the followNFT being transferred.
* @param followNFTId The followNFT being transferred's token ID.
* @param from The address the followNFT is being transferred from.
* @param to The address the followNFT is being transferred to.
* @param timestamp The current block timestamp.
*/
event FollowNFTTransferred(
uint256 indexed profileId,
uint256 indexed followNFTId,
address from,
address to,
uint256 timestamp
);
/**
* @dev Emitted via callback when a collectNFT is transferred.
*
@@ -428,24 +411,6 @@ library Events {
uint256 timestamp
);
/**
* @notice Emitted when one or multiple addresses are approved (or disapproved) for following in
* the `ApprovalFollowModule`.
*
* @param owner The profile owner who executed the approval.
* @param profileId The profile ID that the follow approvals are granted/revoked for.
* @param addresses The addresses that have had the follow approvals granted/revoked.
* @param approved Whether each corresponding address is now approved or disapproved.
* @param timestamp The current block timestamp.
*/
event FollowsApproved(
address indexed owner,
uint256 indexed profileId,
address[] addresses,
bool[] approved,
uint256 timestamp
);
/**
* @dev Emitted when the metadata associated with a profile is set in the `LensPeriphery`.
*

View File

@@ -8,6 +8,7 @@ import {ICollectNFT} from 'contracts/interfaces/ICollectNFT.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import {ILensHub} from 'contracts/interfaces/ILensHub.sol';
import {LensBaseERC721} from 'contracts/base/LensBaseERC721.sol';
import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
/**
* @title CollectNFT
@@ -20,6 +21,8 @@ import {LensBaseERC721} from 'contracts/base/LensBaseERC721.sol';
* the first collect for a given publication, and the token URI points to the original publication's contentURI.
*/
contract LegacyCollectNFT is LensBaseERC721, ERC2981CollectionRoyalties, ICollectNFT {
using Strings for uint256;
address public immutable HUB;
uint256 internal _profileId;
@@ -39,18 +42,13 @@ contract LegacyCollectNFT is LensBaseERC721, ERC2981CollectionRoyalties, ICollec
}
/// @inheritdoc ICollectNFT
function initialize(
uint256 profileId,
uint256 pubId,
string calldata name,
string calldata symbol
) external override {
function initialize(uint256 profileId, uint256 pubId) external override {
if (_initialized) revert Errors.Initialized();
_initialized = true;
_setRoyalty(1000); // 10% of royalties
_profileId = profileId;
_pubId = pubId;
super._initialize(name, symbol);
// _name and _symbol remain uninitialized because we override the getters below
}
/// @inheritdoc ICollectNFT
@@ -73,6 +71,20 @@ contract LegacyCollectNFT is LensBaseERC721, ERC2981CollectionRoyalties, ICollec
return ILensHub(HUB).getContentURI(_profileId, _pubId);
}
/**
* @dev See {IERC721Metadata-name}.
*/
function name() public view override returns (string memory) {
return string.concat('Lens Collect | Profile #', _profileId.toString(), ' - Publication #', _pubId.toString());
}
/**
* @dev See {IERC721Metadata-symbol}.
*/
function symbol() public pure override returns (string memory) {
return 'LENS-COLLECT';
}
/**
* @dev See {IERC165-supportsInterface}.
*/

View File

@@ -36,11 +36,7 @@ contract ModuleGlobals is IModuleGlobals {
* @param treasury The treasury address to direct fees to.
* @param treasuryFee The treasury fee in BPS to levy on collects.
*/
constructor(
address governance,
address treasury,
uint16 treasuryFee
) {
constructor(address governance, address treasury, uint16 treasuryFee) {
_setGovernance(governance);
_setTreasury(treasury);
_setTreasuryFee(treasuryFee);

View File

@@ -9,6 +9,7 @@ import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
import {ILensHub} from 'contracts/interfaces/ILensHub.sol';
import {LensBaseERC721} from 'contracts/base/LensBaseERC721.sol';
import {ActionRestricted} from 'contracts/modules/ActionRestricted.sol';
import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
/**
* @title CollectNFT
@@ -21,6 +22,8 @@ import {ActionRestricted} from 'contracts/modules/ActionRestricted.sol';
* the first collect for a given publication, and the token URI points to the original publication's contentURI.
*/
contract CollectNFT is LensBaseERC721, ERC2981CollectionRoyalties, ActionRestricted, ICollectNFT {
using Strings for uint256;
address public immutable HUB;
uint256 internal _profileId;
@@ -39,18 +42,13 @@ contract CollectNFT is LensBaseERC721, ERC2981CollectionRoyalties, ActionRestric
}
/// @inheritdoc ICollectNFT
function initialize(
uint256 profileId,
uint256 pubId,
string calldata name,
string calldata symbol
) external override {
function initialize(uint256 profileId, uint256 pubId) external override {
if (_initialized) revert Errors.Initialized();
_initialized = true;
_setRoyalty(1000); // 10% of royalties
_profileId = profileId;
_pubId = pubId;
super._initialize(name, symbol);
// _name and _symbol remain uninitialized because we override the getters below
}
/// @inheritdoc ICollectNFT
@@ -72,6 +70,20 @@ contract CollectNFT is LensBaseERC721, ERC2981CollectionRoyalties, ActionRestric
return ILensHub(HUB).getContentURI(_profileId, _pubId);
}
/**
* @dev See {IERC721Metadata-name}.
*/
function name() public view override returns (string memory) {
return string.concat('Lens Collect | Profile #', _profileId.toString(), ' - Publication #', _pubId.toString());
}
/**
* @dev See {IERC721Metadata-symbol}.
*/
function symbol() public pure override returns (string memory) {
return 'LENS-COLLECT';
}
/**
* @dev See {IERC165-supportsInterface}.
*/

View File

@@ -8,14 +8,11 @@ import {ICollectNFT} from 'contracts/interfaces/ICollectNFT.sol';
import {Types} from 'contracts/libraries/constants/Types.sol';
import {Events} from 'contracts/libraries/constants/Events.sol';
import {Clones} from '@openzeppelin/contracts/proxy/Clones.sol';
import {Strings} from '@openzeppelin/contracts/utils/Strings.sol';
import {Errors} from 'contracts/libraries/constants/Errors.sol';
import {HubRestricted} from 'contracts/base/HubRestricted.sol';
import {IModuleGlobals} from 'contracts/interfaces/IModuleGlobals.sol';
contract CollectPublicationAction is HubRestricted, IPublicationActionModule {
using Strings for uint256;
struct CollectData {
address collectModule;
address collectNFT;
@@ -26,9 +23,6 @@ contract CollectPublicationAction is HubRestricted, IPublicationActionModule {
address public immutable COLLECT_NFT_IMPL;
address public immutable MODULE_GLOBALS;
string constant COLLECT_NFT_NAME_INFIX = '-Collect-';
string constant COLLECT_NFT_SYMBOL_INFIX = '-Cl-';
mapping(address collectModule => bool isWhitelisted) internal _collectModuleWhitelisted;
mapping(uint256 profileId => mapping(uint256 pubId => CollectData collectData)) internal _collectDataByPub;
@@ -135,14 +129,7 @@ contract CollectPublicationAction is HubRestricted, IPublicationActionModule {
function _deployCollectNFT(uint256 profileId, uint256 pubId, address collectNFTImpl) private returns (address) {
address collectNFT = Clones.clone(collectNFTImpl);
string memory collectNFTName = string(
abi.encodePacked(profileId.toString(), COLLECT_NFT_NAME_INFIX, pubId.toString())
);
string memory collectNFTSymbol = string(
abi.encodePacked(profileId.toString(), COLLECT_NFT_SYMBOL_INFIX, pubId.toString())
);
ICollectNFT(collectNFT).initialize(profileId, pubId, collectNFTName, collectNFTSymbol);
ICollectNFT(collectNFT).initialize(profileId, pubId);
emit Events.CollectNFTDeployed(profileId, pubId, collectNFT, block.timestamp);
return collectNFT;

View File

@@ -51,7 +51,7 @@ contract FeeFollowModule is FeeModuleBase, HubRestricted, IFollowModule {
// We allow address(0) to allow burning the currency. But the token has to support transfers to address(0).
//
// We don't introduce the upper limit to the amount, even though it might overflow if the amount * treasuryFee
// during processFollow. But this is a safe behavior, and a case that should never happen, cause amounts close
// during processFollow. But this is a safe behavior, and a case that should never happen, because amounts close
// to type(uint256).max don't make any sense from the economic standpoint.
if (!_currencyWhitelisted(feeConfig.currency) || feeConfig.amount == 0) {
revert Errors.InitParamsInvalid();

View File

@@ -65,7 +65,7 @@ contract TokenGatedReferenceModule is HubRestricted, IReferenceModule {
(bool success, bytes memory result) = gateParams.tokenAddress.staticcall(
abi.encodeWithSelector(IToken.balanceOf.selector, address(this))
);
// We don't check if the contract exists cause we expect the return data anyway.
// We don't check if the contract exists because we expect the return data anyway.
if (gateParams.minThreshold == 0 || !success || result.length != UINT256_BYTES) {
revert Errors.InitParamsInvalid();
}

View File

@@ -30,14 +30,15 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
uint256 internal immutable NAMESPACE_LENGTH = bytes(NAMESPACE).length;
uint256 internal constant SEPARATOR_LENGTH = 1; // bytes('.').length;
bytes32 internal constant NAMESPACE_HASH = keccak256(bytes(NAMESPACE));
uint256 internal immutable TOKEN_GUARDIAN_COOLDOWN;
mapping(address => uint256) internal _tokenGuardianDisablingTimestamp;
modifier onlyOwnerOrHubOrWhitelistedProfileCreator() {
mapping(uint256 tokenId => string localName) internal _localNames;
modifier onlyOwnerOrWhitelistedProfileCreator() {
if (
msg.sender != OWNER && msg.sender != LENS_HUB && !ILensHub(LENS_HUB).isProfileCreatorWhitelisted(msg.sender)
msg.sender != OWNER && !ILensHub(LENS_HUB).isProfileCreatorWhitelisted(msg.sender)
) {
revert HandlesErrors.NotOwnerNorWhitelisted();
}
@@ -51,7 +52,12 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
_;
}
mapping(uint256 tokenId => string localName) internal _localNames;
modifier onlyHub() {
if (msg.sender != LENS_HUB) {
revert HandlesErrors.NotHub();
}
_;
}
constructor(
address owner,
@@ -78,16 +84,18 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
}
/// @inheritdoc ILensHandles
function mintHandle(
address to,
string calldata localName
) external onlyOwnerOrHubOrWhitelistedProfileCreator returns (uint256) {
function mintHandle(address to, string calldata localName)
external
onlyOwnerOrWhitelistedProfileCreator
returns (uint256)
{
_validateLocalName(localName);
uint256 tokenId = getTokenId(localName);
_mint(to, tokenId);
_localNames[tokenId] = localName;
emit HandlesEvents.HandleMinted(localName, NAMESPACE, tokenId, to, block.timestamp);
return tokenId;
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 {
@@ -183,7 +191,15 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
/// INTERNAL FUNCTIONS ///
//////////////////////////////////////
function _validateLocalName(string memory localName) internal view {
function _mintHandle(address to, string calldata localName) internal returns (uint256) {
uint256 tokenId = getTokenId(localName);
_mint(to, tokenId);
_localNames[tokenId] = localName;
emit HandlesEvents.HandleMinted(localName, NAMESPACE, tokenId, to, block.timestamp);
return tokenId;
}
function _validateLocalNameMigration(string memory localName) internal view {
bytes memory localNameAsBytes = bytes(localName);
uint256 localNameLength = localNameAsBytes.length;
@@ -207,6 +223,29 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles {
}
}
function _validateLocalName(string memory localName) internal view {
bytes memory localNameAsBytes = bytes(localName);
uint256 localNameLength = localNameAsBytes.length;
if (localNameLength == 0 || localNameLength + SEPARATOR_LENGTH + NAMESPACE_LENGTH > MAX_HANDLE_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;
}
}
}
function _isAlphaNumeric(bytes1 char) internal pure returns (bool) {
return (char >= '0' && char <= '9') || (char >= 'a' && char <= 'z');
}

View File

@@ -17,6 +17,7 @@ library HandlesErrors {
error HandleFirstCharInvalid();
error NotOwnerNorWhitelisted();
error NotOwner();
error NotHub();
error DoesNotExist();
error NotEOA();
error DisablingAlreadyTriggered();

View File

@@ -5,7 +5,7 @@ import 'forge-std/console2.sol';
import 'contracts/LensHub.sol';
import 'contracts/FollowNFT.sol';
import 'contracts/CollectNFT.sol';
import 'contracts/modules/act/collect/CollectNFT.sol';
import 'contracts/misc/migrations/ProfileMigration.sol';
import {LensHandles} from 'contracts/misc/namespaces/LensHandles.sol';

View File

@@ -19,4 +19,4 @@ max_test_rejects = 200000
[profile.ci]
via_ir = true
[profile.ci.fuzz]
runs = 10000
runs = 100

12
package-lock.json generated
View File

@@ -9964,7 +9964,7 @@
},
"node_modules/ganache-core/node_modules/keccak": {
"version": "3.0.1",
"dev": true,
"extraneous": true,
"hasInstallScript": true,
"inBundle": true,
"license": "MIT",
@@ -10537,7 +10537,7 @@
},
"node_modules/ganache-core/node_modules/node-addon-api": {
"version": "2.0.2",
"dev": true,
"extraneous": true,
"inBundle": true,
"license": "MIT"
},
@@ -10551,7 +10551,7 @@
},
"node_modules/ganache-core/node_modules/node-gyp-build": {
"version": "4.2.3",
"dev": true,
"extraneous": true,
"inBundle": true,
"license": "MIT",
"bin": {
@@ -26809,7 +26809,7 @@
"keccak": {
"version": "3.0.1",
"bundled": true,
"dev": true,
"extraneous": true,
"requires": {
"node-addon-api": "^2.0.0",
"node-gyp-build": "^4.2.0"
@@ -27240,7 +27240,7 @@
"node-addon-api": {
"version": "2.0.2",
"bundled": true,
"dev": true
"extraneous": true
},
"node-fetch": {
"version": "2.1.2",
@@ -27249,7 +27249,7 @@
"node-gyp-build": {
"version": "4.2.3",
"bundled": true,
"dev": true
"extraneous": true
},
"normalize-url": {
"version": "4.5.0",

View File

@@ -1,15 +1,14 @@
{
"name": "@aave/lens-protocol",
"version": "1.0.2",
"version": "2.0.0",
"description": "Composable and decentralized social graph",
"main": "index.js",
"scripts": {
"full-deploy-local": "hardhat full-deploy --network localhost",
"full-deploy-mumbai": "hardhat full-deploy --network mumbai",
"run-env": "npm i && tail -f /dev/null",
"compile": "SKIP_LOAD=true hardhat clean && SKIP_LOAD=true hardhat compile",
"format": "prettier --write .",
"lint": "eslint ."
"build": "forge build --sizes",
"coverage": "forge coverage --report lcov && genhtml lcov.info -o report --branch-coverage",
"test": "forge test",
"test:gas": "forge test -vvv --gas-report",
"deploy:local": "hardhat full-deploy --network localhost",
"deploy:mumbai": "hardhat full-deploy --network mumbai"
},
"repository": {
"type": "git",
@@ -63,13 +62,14 @@
"author": "Lens",
"contributors": [
"Alan Donoso Naumczuk",
"Victor Naumik",
"Josh Stevens",
"Peter Michael (Zer0dot)",
"David Racero",
"Emilio Frangella",
"Lasse Herskind",
"Miguel Martinez",
"Peter Michael (Zer0dot)",
"Steven Valeri",
"Victor Naumik"
"Steven Valeri"
],
"license": "MIT",
"keywords": [

View File

@@ -27,7 +27,7 @@ contract ActTest is ReferralSystemTest {
function _referralSystem_ExpectRevertsIfNeeded(
TestPublication memory target,
uint256[] memory /* referrerProfileIds */,
uint256[] memory, /* referrerProfileIds */
uint256[] memory /* referrerPubIds */
) internal virtual override returns (bool) {
if (_isV1LegacyPub(hub.getPublication(target.profileId, target.pubId))) {
@@ -41,10 +41,11 @@ contract ActTest is ReferralSystemTest {
_act(actor.ownerPk, actionParams);
}
function _act(
uint256 pk,
Types.PublicationActionParams memory publicationActionParams
) internal virtual returns (bytes memory) {
function _act(uint256 pk, Types.PublicationActionParams memory publicationActionParams)
internal
virtual
returns (bytes memory)
{
vm.prank(vm.addr(pk));
return hub.act(publicationActionParams);
}
@@ -183,7 +184,7 @@ contract ActTest is ReferralSystemTest {
assertEq(hub.getActionModuleById(secondActionModuleId), secondActionModule);
}
// Will not work on fork with more complicated action modules cause we don't know how to initialize them
// Will not work on fork with more complicated action modules because we don't know how to initialize them
function testGetEnabledActionModulesBitmap(uint8 enabledActionModulesBitmap) public {
vm.assume(enabledActionModulesBitmap != 0);
@@ -251,10 +252,11 @@ contract ActMetaTxTest is ActTest, MetaTxNegatives {
cachedNonceByAddress[actor.owner] = hub.nonces(actor.owner);
}
function _act(
uint256 pk,
Types.PublicationActionParams memory publicationActionParams
) internal override returns (bytes memory) {
function _act(uint256 pk, Types.PublicationActionParams memory publicationActionParams)
internal
override
returns (bytes memory)
{
address signer = vm.addr(pk);
return
hub.actWithSig({
@@ -271,7 +273,11 @@ contract ActMetaTxTest is ActTest, MetaTxNegatives {
});
}
function _executeMetaTx(uint256 signerPk, uint256 nonce, uint256 deadline) internal virtual override {
function _executeMetaTx(
uint256 signerPk,
uint256 nonce,
uint256 deadline
) internal virtual override {
hub.actWithSig({
publicationActionParams: actionParams,
signature: _getSigStruct({

View File

@@ -4,7 +4,7 @@ pragma solidity ^0.8.13;
import 'test/base/BaseTest.t.sol';
import 'test/LensBaseERC721Test.t.sol';
import {CollectPublicationAction} from 'contracts/modules/act/collect/CollectPublicationAction.sol';
import {CollectNFT} from 'contracts/CollectNFT.sol';
import {CollectNFT} from 'contracts/modules/act/collect/CollectNFT.sol';
import {MockCollectModule} from 'test/mocks/MockCollectModule.sol';
import {Clones} from '@openzeppelin/contracts/proxy/Clones.sol';
import {Errors as ModulesErrors} from 'contracts/modules/constants/Errors.sol';
@@ -199,7 +199,7 @@ contract CollectNFTTest is BaseTest, LensBaseERC721Test {
function testCannotInitializeTwoTimes(uint256 profileId, uint256 pubId) public {
vm.expectRevert(Errors.Initialized.selector);
collectNFT.initialize(profileId, pubId, 'someName', 'someSymbol');
collectNFT.initialize(profileId, pubId);
}
function testTokenURI() public {
@@ -236,7 +236,7 @@ contract CollectNFTTest is BaseTest, LensBaseERC721Test {
collectNFT = CollectNFT(Clones.clone(collectNFTImpl));
// Initializes the clone
collectNFT.initialize(profileId, pubId, 'Name', 'SYMBOL');
collectNFT.initialize(profileId, pubId);
(uint256 sourceProfileId, uint256 sourcePubId) = collectNFT.getSourcePublicationPointer();

View File

@@ -1064,7 +1064,7 @@ contract FollowNFTTest is BaseTest, LensBaseERC721Test {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
// Wrap - Negatives
// Wrap (tokenId) - Negatives
//////////////////////////////////////////////////////////
function testCannotWrapIfAlreadyWrapped() public {
@@ -1119,9 +1119,9 @@ contract FollowNFTTest is BaseTest, LensBaseERC721Test {
followNFT.wrap(followTokenId);
}
function testCannotWrapRecoveringWhenTheSenderDoesNotOwnTheProfileAllowedToRecover(
address unrelatedAddress
) public {
function testCannotWrapRecoveringWhenTheSenderDoesNotOwnTheProfileAllowedToRecover(address unrelatedAddress)
public
{
vm.assume(unrelatedAddress != address(0));
vm.assume(unrelatedAddress != alreadyFollowingProfileOwner);
@@ -1150,7 +1150,7 @@ contract FollowNFTTest is BaseTest, LensBaseERC721Test {
}
//////////////////////////////////////////////////////////
// Wrap - Scenarios
// Wrap (tokenId) - Scenarios
//////////////////////////////////////////////////////////
function testWrappedTokenOwnerIsFollowerProfileOwnerAfterUntyingAndWrapping() public {
@@ -1215,9 +1215,9 @@ contract FollowNFTTest is BaseTest, LensBaseERC721Test {
assertEq(followNFT.getProfileIdAllowedToRecover(followTokenId), 0);
}
function testRecoveringTokenThroughWrappingItAfterProfileAllowedToRecoverWasTransferred(
address unrelatedAddress
) public {
function testRecoveringTokenThroughWrappingItAfterProfileAllowedToRecoverWasTransferred(address unrelatedAddress)
public
{
vm.assume(unrelatedAddress != address(0));
vm.assume(unrelatedAddress != alreadyFollowingProfileOwner);
@@ -1247,6 +1247,203 @@ contract FollowNFTTest is BaseTest, LensBaseERC721Test {
assertEq(followNFT.getProfileIdAllowedToRecover(followTokenId), 0);
}
//////////////////////////////////////////////////////////
// Wrap (tokenId, receiver) - Negatives
//////////////////////////////////////////////////////////
function testCannotWrapIfTokenReceiverIsAddressZero() public {
uint256 followTokenId = followNFT.getFollowTokenId(alreadyFollowingProfileId);
vm.expectRevert(Errors.InvalidParameter.selector);
vm.prank(alreadyFollowingProfileOwner);
followNFT.wrap(followTokenId, address(0));
}
function testCannotWrapIfAlreadyWrapped(address receiver) public {
vm.assume(receiver != address(0));
uint256 followTokenId = followNFT.getFollowTokenId(alreadyFollowingProfileId);
vm.prank(alreadyFollowingProfileOwner);
followNFT.wrap(followTokenId, receiver);
vm.prank(alreadyFollowingProfileOwner);
vm.expectRevert(IFollowNFT.AlreadyWrapped.selector);
followNFT.wrap(followTokenId, receiver);
}
function testCannotWrapIfTokenDoesNotExist(uint256 unexistentTokenId, address receiver) public {
vm.assume(receiver != address(0));
vm.assume(followNFT.getFollowerProfileId(unexistentTokenId) == 0);
vm.assume(!followNFT.exists(unexistentTokenId));
vm.expectRevert(IFollowNFT.FollowTokenDoesNotExist.selector);
followNFT.wrap(unexistentTokenId, receiver);
}
function testCannotWrapIfSenderIsNotFollowerOwner(address notFollowerOwner, address receiver) public {
vm.assume(receiver != address(0));
vm.assume(notFollowerOwner != alreadyFollowingProfileOwner);
vm.assume(notFollowerOwner != address(0));
uint256 followTokenId = followNFT.getFollowTokenId(alreadyFollowingProfileId);
vm.prank(notFollowerOwner);
vm.expectRevert(IFollowNFT.DoesNotHavePermissions.selector);
followNFT.wrap(followTokenId, receiver);
}
function testCannotWrapRecoveringWhenTheProfileAllowedToRecoverDoesNotExistAnymore(address receiver) public {
vm.assume(receiver != address(0));
uint256 followTokenId = followNFT.getFollowTokenId(alreadyFollowingProfileId);
vm.prank(address(hub));
followNFT.unfollow({
unfollowerProfileId: alreadyFollowingProfileId,
transactionExecutor: alreadyFollowingProfileOwner
});
assertEq(followNFT.getProfileIdAllowedToRecover(followTokenId), alreadyFollowingProfileId);
vm.prank(alreadyFollowingProfileOwner);
hub.burn(alreadyFollowingProfileId);
vm.prank(alreadyFollowingProfileOwner);
vm.expectRevert(Errors.TokenDoesNotExist.selector);
followNFT.wrap(followTokenId, receiver);
}
function testCannotWrapRecoveringWhenTheSenderDoesNotOwnTheProfileAllowedToRecover(
address unrelatedAddress,
address receiver
) public {
vm.assume(receiver != address(0));
vm.assume(unrelatedAddress != address(0));
vm.assume(unrelatedAddress != alreadyFollowingProfileOwner);
uint256 followTokenId = followNFT.getFollowTokenId(alreadyFollowingProfileId);
vm.prank(address(hub));
followNFT.unfollow({
unfollowerProfileId: alreadyFollowingProfileId,
transactionExecutor: alreadyFollowingProfileOwner
});
assertEq(followNFT.getProfileIdAllowedToRecover(followTokenId), alreadyFollowingProfileId);
vm.prank(alreadyFollowingProfileOwner);
hub.transferFrom({
from: alreadyFollowingProfileOwner,
to: unrelatedAddress,
tokenId: alreadyFollowingProfileId
});
vm.prank(alreadyFollowingProfileOwner);
vm.expectRevert(IFollowNFT.DoesNotHavePermissions.selector);
followNFT.wrap(followTokenId, receiver);
}
//////////////////////////////////////////////////////////
// Wrap (tokenId, receiver) - Scenarios
//////////////////////////////////////////////////////////
function testWrappedTokenOwnerIsReceiverProfileOwnerAfterUntyingAndWrapping(address receiver) public {
vm.assume(receiver != address(0));
uint256 followTokenId = followNFT.getFollowTokenId(alreadyFollowingProfileId);
vm.prank(alreadyFollowingProfileOwner);
followNFT.wrap(followTokenId, receiver);
assertEq(followNFT.ownerOf(followTokenId), receiver);
}
function testWrappedTokenStillHeldByPreviousFollowerOwnerAfterAFollowerProfileTransfer(
address receiver,
address newFollowerProfileOwner
) public {
vm.assume(receiver != address(0));
vm.assume(newFollowerProfileOwner != followerProfileOwner);
vm.assume(newFollowerProfileOwner != address(0));
vm.prank(address(hub));
uint256 assignedTokenId = followNFT.follow({
followerProfileId: followerProfileId,
transactionExecutor: followerProfileOwner,
followTokenId: MINT_NEW_TOKEN
});
vm.prank(followerProfileOwner);
followNFT.wrap(assignedTokenId, receiver);
assertEq(followNFT.ownerOf(assignedTokenId), receiver);
assertTrue(followNFT.isFollowing(followerProfileId));
uint256 followerProfileIdSet = followNFT.getFollowerProfileId(assignedTokenId);
assertEq(followerProfileIdSet, followerProfileId);
vm.prank(followerProfileOwner);
hub.transferFrom(followerProfileOwner, newFollowerProfileOwner, followerProfileId);
assertEq(hub.ownerOf(followerProfileId), newFollowerProfileOwner);
assertEq(followNFT.ownerOf(assignedTokenId), receiver);
assertTrue(followNFT.isFollowing(followerProfileId));
assertEq(followerProfileIdSet, followNFT.getFollowerProfileId(assignedTokenId));
}
function testRecoveringTokenThroughWrappingIt(address receiver) public {
vm.assume(receiver != address(0));
uint256 followTokenId = followNFT.getFollowTokenId(alreadyFollowingProfileId);
vm.prank(address(hub));
followNFT.unfollow({
unfollowerProfileId: alreadyFollowingProfileId,
transactionExecutor: alreadyFollowingProfileOwner
});
assertEq(followNFT.getProfileIdAllowedToRecover(followTokenId), alreadyFollowingProfileId);
vm.prank(alreadyFollowingProfileOwner);
followNFT.wrap(followTokenId, receiver);
assertEq(followNFT.ownerOf(followTokenId), receiver);
assertEq(followNFT.getProfileIdAllowedToRecover(followTokenId), 0);
}
function testRecoveringTokenThroughWrappingItAfterProfileAllowedToRecoverWasTransferred(
address unrelatedAddress,
address receiver
) public {
vm.assume(receiver != address(0));
vm.assume(unrelatedAddress != address(0));
vm.assume(unrelatedAddress != alreadyFollowingProfileOwner);
uint256 followTokenId = followNFT.getFollowTokenId(alreadyFollowingProfileId);
vm.prank(address(hub));
followNFT.unfollow({
unfollowerProfileId: alreadyFollowingProfileId,
transactionExecutor: alreadyFollowingProfileOwner
});
assertEq(followNFT.getProfileIdAllowedToRecover(followTokenId), alreadyFollowingProfileId);
vm.prank(alreadyFollowingProfileOwner);
hub.transferFrom({
from: alreadyFollowingProfileOwner,
to: unrelatedAddress,
tokenId: alreadyFollowingProfileId
});
vm.prank(unrelatedAddress);
followNFT.wrap(followTokenId, receiver);
assertEq(followNFT.ownerOf(followTokenId), receiver);
assertEq(followNFT.getProfileIdAllowedToRecover(followTokenId), 0);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
@@ -1343,9 +1540,9 @@ contract FollowNFTTest is BaseTest, LensBaseERC721Test {
assertFalse(followNFT.exists(followTokenId));
}
function testUnwrappedTokenStillTiedToFollowerProfileAfterAFollowerProfileTransfer(
address newFollowerProfileOwner
) public {
function testUnwrappedTokenStillTiedToFollowerProfileAfterAFollowerProfileTransfer(address newFollowerProfileOwner)
public
{
vm.assume(newFollowerProfileOwner != followerProfileOwner);
vm.assume(newFollowerProfileOwner != address(0));
@@ -1641,7 +1838,11 @@ contract FollowNFTTest is BaseTest, LensBaseERC721Test {
assertEq(royalties, expectedRoyalties);
}
function testSetRoyalties(uint256 royaltiesInBasisPoints, uint256 tokenId, uint256 salePrice) public {
function testSetRoyalties(
uint256 royaltiesInBasisPoints,
uint256 tokenId,
uint256 salePrice
) public {
uint256 basisPoints = 10000;
royaltiesInBasisPoints = bound(royaltiesInBasisPoints, 0, basisPoints);
uint256 salePriceTimesRoyalties;

View File

@@ -137,7 +137,7 @@ contract LegacyCollectNFTTest is BaseTest, LensBaseERC721Test {
function testCannotInitializeTwoTimes(uint256 profileId, uint256 pubId) public {
vm.expectRevert(Errors.Initialized.selector);
collectNFT.initialize(profileId, pubId, 'someName', 'someSymbol');
collectNFT.initialize(profileId, pubId);
}
function testTokenURI() public {
@@ -170,7 +170,7 @@ contract LegacyCollectNFTTest is BaseTest, LensBaseERC721Test {
collectNFT = LegacyCollectNFT(Clones.clone(collectNFTImpl));
// Initializes the clone
collectNFT.initialize(profileId, pubId, 'Name', 'SYMBOL');
collectNFT.initialize(profileId, pubId);
(uint256 sourceProfileId, uint256 sourcePubId) = collectNFT.getSourcePublicationPointer();

View File

@@ -10,6 +10,7 @@ import {ILegacyCollectModule} from 'contracts/interfaces/ILegacyCollectModule.so
import {ReferralSystemTest} from 'test/ReferralSystem.t.sol';
contract LegacyCollectTest is BaseTest, ReferralSystemTest {
using Strings for uint256;
uint256 pubId;
Types.CollectParams defaultCollectParams;
TestAccount blockedProfile;
@@ -222,16 +223,6 @@ contract LegacyCollectTest is BaseTest, ReferralSystemTest {
assertTrue(pub.__DEPRECATED__collectNFT == address(0));
address predictedCollectNFT = computeCreateAddress(address(hub), vm.getNonce(address(hub)));
string memory predictedCollectNFTName = string.concat(
vm.toString(defaultAccount.profileId),
LegacyCollectLib.COLLECT_NFT_NAME_INFIX,
vm.toString(pubId)
);
string memory predictedCollectNFTSymbol = string.concat(
vm.toString(defaultAccount.profileId),
LegacyCollectLib.COLLECT_NFT_SYMBOL_INFIX,
vm.toString(pubId)
);
vm.expectEmit(true, true, true, true, address(hub));
emit Events.CollectNFTDeployed(defaultAccount.profileId, pubId, predictedCollectNFT, block.timestamp);
@@ -240,12 +231,7 @@ contract LegacyCollectTest is BaseTest, ReferralSystemTest {
predictedCollectNFT,
abi.encodeCall(
ICollectNFT.initialize,
(
defaultCollectParams.publicationCollectedProfileId,
defaultCollectParams.publicationCollectedId,
predictedCollectNFTName,
predictedCollectNFTSymbol
)
(defaultCollectParams.publicationCollectedProfileId, defaultCollectParams.publicationCollectedId)
),
1
);
@@ -288,6 +274,22 @@ contract LegacyCollectTest is BaseTest, ReferralSystemTest {
uint256 collectTokenId = _collect(defaultAccount.ownerPk, defaultCollectParams);
assertEq(collectTokenId, 1);
string memory expectedCollectNftName = string.concat(
'Lens Collect | Profile #',
defaultCollectParams.publicationCollectedProfileId.toString(),
' - Publication #',
defaultCollectParams.publicationCollectedId.toString()
);
string memory expectedCollectNftSymbol = 'LENS-COLLECT';
assertEq(LegacyCollectNFT(predictedCollectNFT).name(), expectedCollectNftName, 'Invalid collect NFT name');
assertEq(
LegacyCollectNFT(predictedCollectNFT).symbol(),
expectedCollectNftSymbol,
'Invalid collect NFT symbol'
);
_refreshCachedNonces();
pub = hub.getPublication(defaultAccount.profileId, pubId);
@@ -337,7 +339,7 @@ contract LegacyCollectTest is BaseTest, ReferralSystemTest {
function _referralSystem_ExpectRevertsIfNeeded(
TestPublication memory target,
uint256[] memory /* referrerProfileIds */,
uint256[] memory, /* referrerProfileIds */
uint256[] memory /* referrerPubIds */
) internal virtual override returns (bool) {
if (skipTest) {
@@ -422,10 +424,12 @@ contract LegacyCollectMetaTxTest is LegacyCollectTest, MetaTxNegatives {
_refreshCachedNonces();
}
function _collect(
uint256 pk,
Types.CollectParams memory collectParams
) internal virtual override returns (uint256) {
function _collect(uint256 pk, Types.CollectParams memory collectParams)
internal
virtual
override
returns (uint256)
{
address signer = vm.addr(pk);
return
@@ -443,7 +447,11 @@ contract LegacyCollectMetaTxTest is LegacyCollectTest, MetaTxNegatives {
);
}
function _executeMetaTx(uint256 signerPk, uint256 nonce, uint256 deadline) internal virtual override {
function _executeMetaTx(
uint256 signerPk,
uint256 nonce,
uint256 deadline
) internal virtual override {
hub.collectWithSig(
defaultCollectParams,
_getSigStruct({

View File

@@ -5,7 +5,7 @@ import 'test/base/BaseTest.t.sol';
import {IPublicationActionModule} from 'contracts/interfaces/IPublicationActionModule.sol';
import {ICollectModule} from 'contracts/interfaces/ICollectModule.sol';
import {CollectPublicationAction} from 'contracts/modules/act/collect/CollectPublicationAction.sol';
import {CollectNFT} from 'contracts/CollectNFT.sol';
import {CollectNFT} from 'contracts/modules/act/collect/CollectNFT.sol';
import {MockCollectModule} from 'test/mocks/MockCollectModule.sol';
import {Events} from 'contracts/libraries/constants/Events.sol';
import {IERC721} from '@openzeppelin/contracts/token/ERC721/IERC721.sol';
@@ -223,7 +223,11 @@ contract CollectPublicationActionTest is BaseTest {
);
}
function testInitializePublicationAction(uint256 profileId, uint256 pubId, address transactionExecutor) public {
function testInitializePublicationAction(
uint256 profileId,
uint256 pubId,
address transactionExecutor
) public {
vm.assume(profileId != 0);
vm.assume(pubId != 0);
vm.assume(transactionExecutor != address(0));
@@ -251,9 +255,6 @@ contract CollectPublicationActionTest is BaseTest {
assertEq(collectPublicationAction.getCollectData(profileId, pubId).collectModule, mockCollectModule);
}
string constant COLLECT_NFT_NAME_INFIX = '-Collect-';
string constant COLLECT_NFT_SYMBOL_INFIX = '-Cl-';
function testProcessPublicationAction_firstCollect(
uint256 profileId,
uint256 pubId,
@@ -288,18 +289,6 @@ contract CollectPublicationActionTest is BaseTest {
uint256 contractNonce = vm.getNonce(address(collectPublicationAction));
address collectNFT = computeCreateAddress(address(collectPublicationAction), contractNonce);
string memory expectedCollectNftName = string.concat(
profileId.toString(),
COLLECT_NFT_NAME_INFIX,
pubId.toString()
);
string memory expectedCollectNftSymbol = string.concat(
profileId.toString(),
COLLECT_NFT_SYMBOL_INFIX,
pubId.toString()
);
vm.expectEmit(true, true, true, true, address(collectPublicationAction));
emit Events.CollectNFTDeployed(profileId, pubId, collectNFT, block.timestamp);
@@ -316,11 +305,7 @@ contract CollectPublicationActionTest is BaseTest {
timestamp: block.timestamp
});
vm.expectCall(
collectNFT,
abi.encodeCall(CollectNFT.initialize, (profileId, pubId, expectedCollectNftName, expectedCollectNftSymbol)),
1
);
vm.expectCall(collectNFT, abi.encodeCall(CollectNFT.initialize, (profileId, pubId)), 1);
vm.prank(address(hub));
bytes memory returnData = collectPublicationAction.processPublicationAction(processActionParams);
@@ -328,6 +313,15 @@ contract CollectPublicationActionTest is BaseTest {
assertEq(tokenId, 1, 'Invalid tokenId');
assertEq(collectActionResult, abi.encode(true), 'Invalid collectActionResult data');
string memory expectedCollectNftName = string.concat(
'Lens Collect | Profile #',
profileId.toString(),
' - Publication #',
pubId.toString()
);
string memory expectedCollectNftSymbol = 'LENS-COLLECT';
assertEq(CollectNFT(collectNFT).name(), expectedCollectNftName, 'Invalid collect NFT name');
assertEq(CollectNFT(collectNFT).symbol(), expectedCollectNftSymbol, 'Invalid collect NFT symbol');

View File

@@ -35,7 +35,12 @@ contract FeeFollowModuleTest is BaseTest {
// Initialization - Negatives
function testCannotInitialize_NotHub(address from, uint256 profileId, uint256 amount, address recipient) public {
function testCannotInitialize_NotHub(
address from,
uint256 profileId,
uint256 amount,
address recipient
) public {
vm.assume(profileId != 0);
vm.assume(amount != 0);
vm.assume(from != address(hub));
@@ -84,7 +89,11 @@ contract FeeFollowModuleTest is BaseTest {
// Initialization - Scenarios
function testInitialize(uint256 profileId, uint256 amount, address recipient) public {
function testInitialize(
uint256 profileId,
uint256 amount,
address recipient
) public {
vm.assume(profileId != 0);
vm.assume(amount != 0);
assertTrue(moduleGlobals.isCurrencyWhitelisted(address(currency)));
@@ -109,7 +118,7 @@ contract FeeFollowModuleTest is BaseTest {
vm.assume(followerProfileId != 0);
vm.assume(targetProfileId != 0);
(, uint16 treasuryFee) = moduleGlobals.getTreasuryData();
// Overflow protection (cause treasuryAmount = amount * treasuryFee / BPS_MAX)
// Overflow protection (because treasuryAmount = amount * treasuryFee / BPS_MAX)
vm.assume(
amount != 0 && amount <= (treasuryFee == 0 ? type(uint256).max : type(uint256).max / uint256(treasuryFee))
);
@@ -148,7 +157,7 @@ contract FeeFollowModuleTest is BaseTest {
vm.assume(followerProfileId != 0);
vm.assume(targetProfileId != 0);
(, uint16 treasuryFee) = moduleGlobals.getTreasuryData();
// Overflow protection (cause treasuryAmount = amount * treasuryFee / BPS_MAX)
// Overflow protection (because treasuryAmount = amount * treasuryFee / BPS_MAX)
vm.assume(
amount != 0 && amount <= (treasuryFee == 0 ? type(uint256).max : type(uint256).max / uint256(treasuryFee))
);
@@ -234,7 +243,7 @@ contract FeeFollowModuleTest is BaseTest {
vm.prank(modulesGovernance);
moduleGlobals.setTreasuryFee(treasuryFee);
// Overflow protection (cause treasuryAmount = amount * treasuryFee / BPS_MAX)
// Overflow protection (because treasuryAmount = amount * treasuryFee / BPS_MAX)
vm.assume(
amount != 0 && amount <= (treasuryFee == 0 ? type(uint256).max : type(uint256).max / uint256(treasuryFee))
);

View File

@@ -33,7 +33,7 @@ contract LensHandlesTest is BaseTest {
string memory handle = 'handle';
vm.prank(address(hub));
vm.prank(lensHandles.OWNER());
uint256 handleId = lensHandles.mintHandle(owner, handle);
assertTrue(lensHandles.exists(handleId));
@@ -61,28 +61,28 @@ contract LensHandlesTest is BaseTest {
}
function testCannot_MintHandle_WithZeroLength() public {
vm.expectRevert(HandlesErrors.HandleLengthInvalid.selector);
vm.prank(lensHandles.OWNER());
vm.prank(address(hub));
vm.expectRevert(HandlesErrors.HandleLengthInvalid.selector);
lensHandles.mintHandle(address(this), '');
}
function testCannot_MintHandle_WithNonUniqueLocalName() public {
vm.prank(address(hub));
vm.prank(lensHandles.OWNER());
lensHandles.mintHandle(address(this), 'handle');
vm.expectRevert('ERC721: token already minted');
vm.prank(lensHandles.OWNER());
vm.prank(address(hub));
vm.expectRevert('ERC721: token already minted');
lensHandles.mintHandle(makeAddr('ANOTHER_ADDRESS'), 'handle');
}
function testCannot_MintHandle_WithLengthMoreThanMax(uint256 randomFuzz) public {
string memory randomHandle = _randomAlphanumericString(MAX_HANDLE_LENGTH + 1, randomFuzz);
vm.expectRevert(HandlesErrors.HandleLengthInvalid.selector);
vm.prank(lensHandles.OWNER());
vm.prank(address(hub));
vm.expectRevert(HandlesErrors.HandleLengthInvalid.selector);
lensHandles.mintHandle(address(this), randomHandle);
}
@@ -93,17 +93,10 @@ contract LensHandlesTest is BaseTest {
string memory invalidUnderscoreHandle = string.concat('_', randomHandle);
vm.expectRevert(HandlesErrors.HandleFirstCharInvalid.selector);
vm.prank(lensHandles.OWNER());
vm.prank(address(hub));
vm.expectRevert(HandlesErrors.HandleFirstCharInvalid.selector);
lensHandles.mintHandle(address(this), invalidUnderscoreHandle);
string memory invalidDashHandle = string.concat('-', randomHandle);
vm.expectRevert(HandlesErrors.HandleFirstCharInvalid.selector);
vm.prank(address(hub));
lensHandles.mintHandle(address(this), invalidDashHandle);
}
function testCannot_MintHandle_WithInvalidChar(
@@ -118,9 +111,7 @@ contract LensHandlesTest is BaseTest {
vm.assume(
(invalidCharCode < 48 || // '0'
invalidCharCode > 122 || // 'z'
(invalidCharCode > 57 && invalidCharCode < 97)) && // '9' and 'a'
invalidCharCode != 45 && // '-'
invalidCharCode != 95 // '_'
(invalidCharCode > 57 && invalidCharCode < 97)) && invalidCharCode != 95 // '9' and 'a' // '_'
);
string memory randomHandle = _randomAlphanumericString(length, randomFuzz);
@@ -136,8 +127,8 @@ contract LensHandlesTest is BaseTest {
console.log('invalidHandle', invalidHandle);
vm.prank(lensHandles.OWNER());
vm.expectRevert(HandlesErrors.HandleContainsInvalidCharacters.selector);
vm.prank(address(hub));
lensHandles.mintHandle(address(this), invalidHandle);
}
@@ -154,12 +145,12 @@ contract LensHandlesTest is BaseTest {
}
function testExists(uint256 number) public {
number = bound(number, 1, 10 ** (MAX_HANDLE_LENGTH) - 1);
number = bound(number, 1, 10**(MAX_HANDLE_LENGTH) - 1);
string memory numbersHandle = vm.toString(number);
uint256 expectedTokenId = lensHandles.getTokenId(numbersHandle);
vm.assume(!lensHandles.exists(expectedTokenId));
vm.prank(address(hub));
vm.prank(lensHandles.OWNER());
uint256 handleId = lensHandles.mintHandle(address(this), numbersHandle);
assertEq(handleId, expectedTokenId);
@@ -189,7 +180,7 @@ contract LensHandlesTest is BaseTest {
// TODO: Should we revert if it doesn't exist?
function testGetLocalName(uint256 number) public {
number = bound(number, 1, 10 ** (MAX_HANDLE_LENGTH) - 1);
number = bound(number, 1, 10**(MAX_HANDLE_LENGTH) - 1);
string memory numbersHandle = vm.toString(number);
uint256 expectedTokenId = lensHandles.getTokenId(numbersHandle);
vm.assume(!lensHandles.exists(expectedTokenId));
@@ -197,7 +188,7 @@ contract LensHandlesTest is BaseTest {
vm.expectRevert(HandlesErrors.DoesNotExist.selector);
lensHandles.getLocalName(expectedTokenId);
vm.prank(address(hub));
vm.prank(lensHandles.OWNER());
uint256 handleId = lensHandles.mintHandle(address(this), numbersHandle);
assertEq(handleId, expectedTokenId);
@@ -213,7 +204,7 @@ contract LensHandlesTest is BaseTest {
// TODO: Should we revert if it doesn't exist?
function testGetHandle(uint256 number) public {
number = bound(number, 1, 10 ** (MAX_HANDLE_LENGTH) - 1);
number = bound(number, 1, 10**(MAX_HANDLE_LENGTH) - 1);
string memory numbersHandle = vm.toString(number);
uint256 expectedTokenId = lensHandles.getTokenId(numbersHandle);
vm.assume(!lensHandles.exists(expectedTokenId));
@@ -221,7 +212,7 @@ contract LensHandlesTest is BaseTest {
vm.expectRevert(HandlesErrors.DoesNotExist.selector);
lensHandles.getHandle(expectedTokenId);
vm.prank(address(hub));
vm.prank(lensHandles.OWNER());
uint256 handleId = lensHandles.mintHandle(address(this), numbersHandle);
assertEq(handleId, expectedTokenId);
@@ -237,7 +228,7 @@ contract LensHandlesTest is BaseTest {
}
function testGetTokenId(uint256 number) public {
number = bound(number, 1, 10 ** (MAX_HANDLE_LENGTH) - 1);
number = bound(number, 1, 10**(MAX_HANDLE_LENGTH) - 1);
string memory numbersHandle = vm.toString(number);
uint256 expectedTokenId = uint256(keccak256(bytes(numbersHandle)));
@@ -245,13 +236,13 @@ contract LensHandlesTest is BaseTest {
}
function testTokenURI() public {
string memory handleTemplate = 'abcdefghijklmnopqrstuvwx-_';
string memory handleTemplate = 'abcdefghijklmnopqrstuvwx_';
for (uint length = 1; length <= bytes(handleTemplate).length; length++) {
for (uint256 length = 1; length <= bytes(handleTemplate).length; length++) {
string memory handle = LibString.slice(handleTemplate, 0, length);
console.log(handle);
vm.prank(address(hub));
vm.prank(lensHandles.OWNER());
uint256 handleId = lensHandles.mintHandle(address(this), handle);
string memory tokenURI = lensHandles.tokenURI(handleId);
@@ -276,7 +267,7 @@ contract LensHandlesTest is BaseTest {
string memory handle = 'handle';
vm.prank(address(hub));
vm.prank(lensHandles.OWNER());
uint256 handleId = lensHandles.mintHandle(owner, handle);
assertTrue(lensHandles.exists(handleId));
@@ -293,14 +284,14 @@ contract LensHandlesTest is BaseTest {
lensHandles.getLocalName(handleId);
}
function testMintHandle_IfOwner(address to, uint256 length, uint256 randomFuzz) public {
function testMintHandle_IfOwner(
address to,
uint256 length,
uint256 randomFuzz
) public {
_mintHandle(lensHandles.OWNER(), to, length, randomFuzz);
}
function testMintHandle_ifHub(address to, uint256 length, uint256 randomFuzz) public {
_mintHandle(address(hub), to, length, randomFuzz);
}
function testMintHandle_ifWhitelistedProfileCreator(
address whitelistedProfileCreator,
address to,
@@ -317,7 +308,12 @@ contract LensHandlesTest is BaseTest {
_mintHandle(whitelistedProfileCreator, to, length, randomFuzz);
}
function _mintHandle(address minter, address to, uint256 length, uint256 randomFuzz) internal {
function _mintHandle(
address minter,
address to,
uint256 length,
uint256 randomFuzz
) internal {
vm.assume(to != address(0));
length = bound(length, 1, MAX_HANDLE_LENGTH);
@@ -343,13 +339,13 @@ contract LensHandlesTest is BaseTest {
}
function _randomAlphanumericString(uint256 length, uint256 randomFuzz) internal view returns (string memory) {
bytes memory allowedChars = '0123456789abcdefghijklmnopqrstuvwxyz_-';
bytes memory allowedChars = '0123456789abcdefghijklmnopqrstuvwxyz_';
string memory str = '';
for (uint256 i = 0; i < length; i++) {
uint8 charCode = uint8((randomFuzz >> (i * 8)) & 0xff);
charCode = uint8(bound(charCode, 0, allowedChars.length - 1));
if (i == 0 && (allowedChars[charCode] == '_' || allowedChars[charCode] == '-')) {
if (i == 0 && (allowedChars[charCode] == '_')) {
charCode /= 2;
}
string memory char = string(abi.encodePacked(allowedChars[charCode]));